Files
BackOne/tools/zt-dissector.lua
T
dedysutanto fa08792c78
build.yml / build_ubuntu (push) Canceled after 0s
build.yml / build_macos (push) Canceled after 0s
build.yml / build_windows (push) Canceled after 0s
Lint / clang-format (push) Canceled after 0s
validate.yml / build_ubuntu (push) Canceled after 0s
feat(pqc): plumb pqcMode to identity path, hybrid HELLO ct, spec review fixes
- Node stores _pqcMode; Identity::generate(bool,int) derives pq from daemon mode
- OneService reads local.conf settings.pqcMode before new Node (opt-in, absent=classic)
- Peer/IncomingPacket: hybrid KEM ct in HELLO/OK, capability-bit fallback classic
- tools/pqc-lab.sh two-node E2E lab + tools/zt-dissector.lua capture dissector
- docs/pqc-test-scenario.md test plan
- SPEC.md review pass: V12 evidence -> live daemon check, V14/V15 cite realign,
  T24/T25 tasks (pqconly preset gap documented)
2026-10-03 07:01:52 +07:00

109 lines
4.4 KiB
Lua

-- BackOne / ZeroTier wire dissector for Wireshark / tshark 4.x
-- Usage: tshark -X lua_script:tools/zt-dissector.lua -r capture.pcap -Y zt
-- Offsets per node/Packet.hpp:224-258 and node/Peer.cpp:463-517.
local zt = Proto("zt", "BackOne / ZeroTier P2P")
local VERB_HELLO = 1 -- Packet.hpp:592 (HELLO is 1, NOT 0)
local FRAGMENT_INDICATOR = 0xff -- Packet.hpp:253 (ZT_ADDRESS_RESERVED_PREFIX)
local IDENTITY_TYPE_PQ_HYBRID = 1
local IDENTITY_SIZE_CLASSIC = 39 -- 5 addr + 1 type + 32 pub + 1 privlen
local IDENTITY_SIZE_PQ = 3175 -- classic + 1184 ML-KEM-768 pk + 1952 ML-DSA-65 pk
local HELLO_IDENTITY_OFF = 13 -- payload + (pv1+maj1+min1+rev2+timestamp8)
local f = {
iv = ProtoField.bytes("zt.iv", "Packet ID / IV"),
dest = ProtoField.string("zt.dest", "Destination"),
src = ProtoField.string("zt.src", "Source"),
flags = ProtoField.uint8("zt.flags", "Flags", base.HEX),
verb = ProtoField.uint8("zt.verb", "Verb", base.DEC),
frag = ProtoField.uint8("zt.frag", "Fragment indicator", base.HEX),
fragno = ProtoField.uint8("zt.fragno", "Fragment #", base.DEC),
fragtot = ProtoField.uint8("zt.fragtot", "Total fragments", base.DEC),
hello = ProtoField.none("zt.hello", "HELLO"),
helloPv = ProtoField.uint8("zt.hello.pv", "Protocol version", base.DEC),
helloMaj = ProtoField.uint8("zt.hello.major", "Major", base.DEC),
helloMin = ProtoField.uint8("zt.hello.minor", "Minor", base.DEC),
helloRev = ProtoField.uint16("zt.hello.rev", "Revision", base.HEX),
helloCp = ProtoField.bool("zt.hello.cap", "PQC capability bit", 16, nil, 0x8000),
helloIdt = ProtoField.uint8("zt.hello.idtype", "Identity type", base.DEC),
helloIds = ProtoField.uint32("zt.hello.idsize", "Identity size", base.DEC),
}
zt.fields = f
local function addr(buf, off) return tostring(buf(off, 5):bytes():tohex()) end
-- Parse the HELLO verb body starting at payload offset `p`. `limit` is the number
-- of bytes actually present in this frame (a head fragment may truncate it).
local function parseHello(t, buf, p, limit, pinfo)
if limit < p + 3 then return end
local ty
local ht = t:add(f.hello, buf(p, limit - p))
ht:add(f.helloPv, buf(p + 0, 1))
ht:add(f.helloMaj, buf(p + 1, 1))
ht:add(f.helloMin, buf(p + 2, 1))
if limit >= p + 5 then
ht:add(f.helloRev, buf(p + 3, 2))
ht:add(f.helloCp, buf(p + 3, 2))
end
local id = p + HELLO_IDENTITY_OFF
if limit >= id + 6 then
ty = buf(id + 5, 1):uint()
ht:add(f.helloIdt, buf(id + 5, 1))
ht:add(f.helloIds, (ty == IDENTITY_TYPE_PQ_HYBRID) and IDENTITY_SIZE_PQ or IDENTITY_SIZE_CLASSIC)
end
-- The [moonCount][moons][hybridFlag(1)][ML-KEM ct] tail is cryptField()'d
-- with the classical key (Peer.cpp:500), so it is opaque to a passive
-- capture: the dissector never guesses the ciphertext length.
if ty then
pinfo.cols.info = ("ZT HELLO type=%d %s cap=%s"):format(
ty, addr(buf, id),
(limit >= p + 4) and (bit.band(buf(p + 3, 2):uint(), 0x8000) ~= 0 and "1" or "0") or "?")
else
pinfo.cols.info = "ZT HELLO"
end
end
function zt.dissector(buf, pinfo, tree)
if buf:len() < 16 then return end
pinfo.cols.protocol = "ZT"
-- Fragment frame: [8 pkID][5 dest][1 0xff][1 {total<<4 | no}][1 hops][data]
if buf(13, 1):uint() == FRAGMENT_INDICATOR then
local t = tree:add(zt, buf(), "BackOne fragment")
t:add(f.iv, buf(0, 8))
t:add(f.dest, addr(buf, 8))
t:add(f.frag, buf(13, 1))
local total = bit.rshift(buf(14, 1):uint(), 4)
local no = bit.band(buf(14, 1):uint(), 0x0f)
t:add(f.fragtot, buf(14, 1), total)
t:add(f.fragno, buf(14, 1), no)
pinfo.cols.info = ("BackOne fragment %d/%d"):format(no, total)
return
end
if buf:len() < 28 then return end
local verb = buf(27, 1):uint()
local encrypted = bit.band(buf(18, 1):uint(), 0x80) ~= 0
local t = tree:add(zt, buf(), "BackOne, verb " .. verb)
t:add(f.iv, buf(0, 8))
t:add(f.dest, addr(buf, 8))
t:add(f.src, addr(buf, 13))
t:add(f.flags, buf(18, 1))
t:add(f.verb, buf(27, 1))
-- Head of a fragmented packet still carries the original header, so the
-- verb and the leading payload bytes are parseable.
if bit.band(buf(18, 1):uint(), 0x40) ~= 0 then
t:add(f.frag, buf(18, 1))
end
if verb == VERB_HELLO and not encrypted then
parseHello(t, buf, 28, buf:len(), pinfo)
end
end
-- Default ZT port and the lab port used by docs/pqc-test-scenario.md.
DissectorTable.get("udp.port"):add(9993, zt)
DissectorTable.get("udp.port"):add(19993, zt)