Created
June 20, 2026 18:51
-
-
Save JJTech0130/742fe75f0f2a06d74d82445d4e812558 to your computer and use it in GitHub Desktop.
Lutron Clear Connect - Type X dissector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Wireshark Lua dissector for Lutron Clear Connect - Type X (CCX) | |
| -- UDP/9190 over a Thread/802.15.4 mesh | |
| local cbor = require("simple_cbor") | |
| local ccx_proto = Proto("clearconnectx", "Lutron Clear Connect - Type X") | |
| local f_msg_id = ProtoField.uint32("clearconnectx.msg_id", "Message ID", base.DEC) | |
| local f_note = ProtoField.string("clearconnectx.note", "Note") | |
| ccx_proto.fields = { f_msg_id, f_note } | |
| local cbor_dissector = Dissector.get("cbor") | |
| function ccx_proto.dissector(buffer, pinfo, tree) | |
| local len = buffer:len() | |
| if len == 0 then return end | |
| pinfo.cols.protocol = "CCX" | |
| local subtree = tree:add(ccx_proto, buffer(), "Clear Connect X") | |
| -- Field extraction via our own decoder | |
| local raw = buffer:raw() | |
| local ok, value, _, ctype = pcall(cbor.decode, raw, 1) | |
| local msg_id = nil | |
| if ok and ctype == "ARRAY" and type(value[1]) == "number" then | |
| msg_id = value[1] | |
| elseif not ok then | |
| subtree:add(f_note, "ccx_cbor decode error: " .. tostring(value)) | |
| elseif ctype == nil or tostring(ctype):match("^ERROR") then | |
| subtree:add(f_note, "ccx_cbor decode failed: " .. tostring(ctype)) | |
| end | |
| if msg_id ~= nil then | |
| subtree:add(f_msg_id, msg_id) | |
| pinfo.cols.info:set(string.format("Message (%d) len=%d", msg_id, len)) | |
| else | |
| pinfo.cols.info:set(string.format("Message ??? len=%d", len)) | |
| end | |
| -- Display tree via the built-in cbor dissector | |
| local disp_ok, disp_err = pcall(function() | |
| cbor_dissector:call(buffer, pinfo, subtree) | |
| end) | |
| if not disp_ok then | |
| subtree:add(f_note, "cbor dissector call failed: " .. tostring(disp_err)) | |
| end | |
| end | |
| DissectorTable.get("udp.port"):add(9190, ccx_proto) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- there isn't really a good way to call Wireshark's built-in CBOR dissector from Lua | |
| local M = {} | |
| local MAX_DEPTH = 32 -- guard against malformed/adversarial nesting | |
| -- Reads a big-endian unsigned integer of `n` bytes from `data` starting | |
| -- at 1-based position `pos`. Returns value, next_pos, or nil on | |
| -- out-of-range. | |
| local function read_uint(data, pos, n) | |
| if pos + n - 1 > #data then return nil end | |
| local v = 0 | |
| for i = 0, n - 1 do | |
| v = v * 256 + data:byte(pos + i) | |
| end | |
| return v, pos + n | |
| end | |
| -- Decodes the "additional info" length/value encoding shared by several | |
| -- major types (uint, negint, bstr/tstr length, array/map count, tag). | |
| -- Returns value, next_pos, or nil, nil on failure / unsupported width. | |
| local function read_arg(data, pos, info) | |
| if info < 24 then | |
| return info, pos | |
| elseif info == 24 then | |
| return read_uint(data, pos, 1) | |
| elseif info == 25 then | |
| return read_uint(data, pos, 2) | |
| elseif info == 26 then | |
| return read_uint(data, pos, 4) | |
| elseif info == 27 then | |
| return read_uint(data, pos, 8) -- may exceed Lua's exact integer range on some builds | |
| else | |
| return nil, nil -- info 28-30 reserved, 31 = indefinite length (not supported) | |
| end | |
| end | |
| local decode -- forward declaration (recursive) | |
| decode = function(data, pos, depth) | |
| depth = depth or 0 | |
| if depth > MAX_DEPTH then | |
| return nil, nil, "ERROR_DEPTH" | |
| end | |
| if pos > #data then | |
| return nil, nil, "ERROR_EOF" | |
| end | |
| local b0 = data:byte(pos) | |
| local major = math.floor(b0 / 32) | |
| local info = b0 % 32 | |
| pos = pos + 1 | |
| if major == 0 then | |
| local v, npos = read_arg(data, pos, info) | |
| if not v then return nil, nil, "ERROR_UINT" end | |
| return v, npos, "UINT" | |
| elseif major == 1 then | |
| local v, npos = read_arg(data, pos, info) | |
| if not v then return nil, nil, "ERROR_NINT" end | |
| return -1 - v, npos, "NINT" | |
| elseif major == 2 then | |
| local len, npos = read_arg(data, pos, info) | |
| if not len then return nil, nil, "ERROR_BIN" end | |
| if npos + len - 1 > #data then return nil, nil, "ERROR_BIN_RANGE" end | |
| return data:sub(npos, npos + len - 1), npos + len, "BIN" | |
| elseif major == 3 then | |
| local len, npos = read_arg(data, pos, info) | |
| if not len then return nil, nil, "ERROR_TEXT" end | |
| if npos + len - 1 > #data then return nil, nil, "ERROR_TEXT_RANGE" end | |
| return data:sub(npos, npos + len - 1), npos + len, "TEXT" | |
| elseif major == 4 then | |
| local count, npos = read_arg(data, pos, info) | |
| if not count then return nil, nil, "ERROR_ARRAY" end | |
| local acc = {} | |
| for i = 1, count do | |
| local v, p2, ct = decode(data, npos, depth + 1) | |
| if ct == nil or ct:match("^ERROR") then | |
| return nil, nil, ct or "ERROR_ARRAY_ITEM" | |
| end | |
| acc[i] = v | |
| npos = p2 | |
| end | |
| return acc, npos, "ARRAY" | |
| elseif major == 5 then | |
| local count, npos = read_arg(data, pos, info) | |
| if not count then return nil, nil, "ERROR_MAP" end | |
| local acc = {} | |
| for _ = 1, count do | |
| local k, p2, kct = decode(data, npos, depth + 1) | |
| if kct == nil or kct:match("^ERROR") then | |
| return nil, nil, kct or "ERROR_MAP_KEY" | |
| end | |
| local v, p3, vct = decode(data, p2, depth + 1) | |
| if vct == nil or vct:match("^ERROR") then | |
| return nil, nil, vct or "ERROR_MAP_VALUE" | |
| end | |
| acc[k] = v | |
| npos = p3 | |
| end | |
| return acc, npos, "MAP" | |
| elseif major == 6 then | |
| -- Tag: decode and return the wrapped value, with ctype "TAG" and | |
| -- the tag number attached via a wrapper table, since we don't | |
| -- special-case any specific tag semantics (no bignums, dates, | |
| -- etc. -- none observed in ClearConnectX traffic so far). | |
| local tagnum, npos = read_arg(data, pos, info) | |
| if not tagnum then return nil, nil, "ERROR_TAG" end | |
| local v, p2, ct = decode(data, npos, depth + 1) | |
| if ct == nil or ct:match("^ERROR") then | |
| return nil, nil, ct or "ERROR_TAG_VALUE" | |
| end | |
| return { tag = tagnum, value = v, inner_ctype = ct }, p2, "TAG" | |
| elseif major == 7 then | |
| if info < 20 then | |
| return info, pos, "SIMPLE" | |
| elseif info == 20 then | |
| return false, pos, "SIMPLE" | |
| elseif info == 21 then | |
| return true, pos, "SIMPLE" | |
| elseif info == 22 then | |
| return nil, pos, "NULL" | |
| elseif info == 23 then | |
| return nil, pos, "UNDEFINED" | |
| elseif info == 25 or info == 26 or info == 27 then | |
| -- half/single/double float -- not needed for ClearConnectX so | |
| -- far; skip the right number of bytes and report as unsupported | |
| -- rather than guessing at a bit-accurate float decode. | |
| local nbytes = (info == 25) and 2 or (info == 26) and 4 or 8 | |
| local npos = pos + nbytes | |
| if npos - 1 > #data then return nil, nil, "ERROR_FLOAT_RANGE" end | |
| return nil, npos, "FLOAT_UNSUPPORTED" | |
| else | |
| return nil, nil, "ERROR_SIMPLE" | |
| end | |
| end | |
| return nil, nil, "ERROR_UNKNOWN_MAJOR_TYPE" | |
| end | |
| -- Public entry point. | |
| M.decode = function(data, pos) | |
| return decode(data, pos or 1, 0) | |
| end | |
| return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment