nx_netscope_dissect.nx source
↩ module page · 172 lines · 6560 B
1// nx_netscope_dissect.nx -- NX-NETSCOPE L4: layered PURE typed dissectors
2// (Wireshark/Zeek-EXCEED) + the canonical hashable dissection digest.
3//
4// Charter principles honored: pure bytes->typed-tree functions (no state,
5// no syscalls in the dissectors themselves), DETERMINISTIC (same bytes +
6// same ruleset -> byte-identical digest -- the headline determinism
7// EXCEED no incumbent gates), LOUD-fail on unresolved cases (a clear
8// UNRESOLVED verdict in the tree, never a silent mis-parse).
9//
10// REUSES nx_dns_get_u16_be (big-endian u16) from nx_dns.nx and
11// nx_sha256_one_shot from nx_sha256_wasm.nx (now compilable on the native
12// lane after the `~` parser fix) for the canonical digest.
13//
14// Stack: Ethernet II -> IPv4 -> {TCP, UDP, ICMP}. IPv6/ARP tagged but
15// not deep-parsed yet (LOUD UNRESOLVED, not silent).
16//
17// license_tier: ORIGINAL
18// genealogy_id: nishi_nx_netscope_l4_dissect
19
20import "nx_syscalls.nx"
21import "nx_dns.nx"
22import "nx_sha256_wasm.nx"
23
24// ---- ethertypes / IP protocols ----
25const NXD_ETHERTYPE_IPV4: i64 = 0x0800
26const NXD_ETHERTYPE_ARP: i64 = 0x0806
27const NXD_ETHERTYPE_IPV6: i64 = 0x86dd
28const NXD_PROTO_ICMP: i64 = 1
29const NXD_PROTO_TCP: i64 = 6
30const NXD_PROTO_UDP: i64 = 17
31
32// ---- deepest-layer tags (sealed) ----
33const NXD_L_UNRESOLVED: i64 = 0
34const NXD_L_ETH: i64 = 1
35const NXD_L_IPV4: i64 = 2
36const NXD_L_TCP: i64 = 3
37const NXD_L_UDP: i64 = 4
38const NXD_L_ICMP: i64 = 5
39const NXD_L_ARP: i64 = 6
40const NXD_L_IPV6: i64 = 7
41
42// ---- TCP flag bits ----
43const NXD_TCP_FIN: i64 = 0x01
44const NXD_TCP_SYN: i64 = 0x02
45const NXD_TCP_RST: i64 = 0x04
46const NXD_TCP_PSH: i64 = 0x08
47const NXD_TCP_ACK: i64 = 0x10
48
49// Dissection: the typed tree, flattened (11 i64 = 88 bytes).
50struct Dissection {
51 deepest: i64, // NXD_L_* -- the deepest layer successfully parsed
52 l2_ethertype: i64,
53 l3_proto: i64, // IPv4 protocol byte, -1 if not IPv4
54 ip_src: i64, // IPv4 src packed BE (low 32), 0 if n/a
55 ip_dst: i64,
56 l4_sport: i64, // -1 if no L4 port
57 l4_dport: i64,
58 tcp_flags: i64, // TCP flag byte, 0 if not TCP
59 payload_off: i64, // byte offset of the L4 payload
60 payload_len: i64,
61 unresolved: i64, // 1 if a layer was present but couldn't be parsed (LOUD)
62}
63const NXD_DISSECTION_BYTES: i64 = 88
64
65// big-endian u32 read (IP addresses).
66func nxd_be32(b: *u8, o: i64) -> i64 {
67 return ((b[o] & 0xff) << 24) | ((b[o+1] & 0xff) << 16)
68 | ((b[o+2] & 0xff) << 8) | (b[o+3] & 0xff)
69}
70
71// nx_packet_dissect: Ethernet II frame bytes -> typed tree. Pure.
72// Sets d.unresolved=1 (LOUD) when a layer is present but unparsable
73// (short frame, unknown ethertype/proto) rather than silently guessing.
74func nx_packet_dissect(buf: *u8, len: i64, d: *Dissection) -> i64 {
75 d.deepest = NXD_L_UNRESOLVED
76 d.l2_ethertype = 0
77 d.l3_proto = 0 - 1
78 d.ip_src = 0
79 d.ip_dst = 0
80 d.l4_sport = 0 - 1
81 d.l4_dport = 0 - 1
82 d.tcp_flags = 0
83 d.payload_off = 0
84 d.payload_len = 0
85 d.unresolved = 0
86
87 if len < 14 { d.unresolved = 1; return 0 } // not even an Ethernet header
88 let et: i64 = nx_dns_get_u16_be(buf, 12)
89 d.l2_ethertype = et
90 d.deepest = NXD_L_ETH
91
92 if et == NXD_ETHERTYPE_ARP { d.deepest = NXD_L_ARP; d.unresolved = 1; return 0 }
93 if et == NXD_ETHERTYPE_IPV6 { d.deepest = NXD_L_IPV6; d.unresolved = 1; return 0 }
94 if et != NXD_ETHERTYPE_IPV4 { d.unresolved = 1; return 0 } // unknown L3
95
96 // ---- IPv4 ----
97 let ip_off: i64 = 14
98 if len < ip_off + 20 { d.unresolved = 1; return 0 }
99 let vihl: i64 = buf[ip_off] & 0xff
100 let ihl: i64 = (vihl & 0x0f) * 4
101 if ihl < 20 { d.unresolved = 1; return 0 }
102 let total_len: i64 = nx_dns_get_u16_be(buf, ip_off + 2)
103 let proto: i64 = buf[ip_off + 9] & 0xff
104 d.l3_proto = proto
105 d.ip_src = nxd_be32(buf, ip_off + 12)
106 d.ip_dst = nxd_be32(buf, ip_off + 16)
107 d.deepest = NXD_L_IPV4
108 let l4_off: i64 = ip_off + ihl
109 if len < l4_off { d.unresolved = 1; return 0 }
110
111 if proto == NXD_PROTO_UDP {
112 if len < l4_off + 8 { d.unresolved = 1; return 0 }
113 d.l4_sport = nx_dns_get_u16_be(buf, l4_off)
114 d.l4_dport = nx_dns_get_u16_be(buf, l4_off + 2)
115 d.payload_off = l4_off + 8
116 let udp_len: i64 = nx_dns_get_u16_be(buf, l4_off + 4)
117 if udp_len >= 8 { d.payload_len = udp_len - 8 } else { d.payload_len = 0 }
118 d.deepest = NXD_L_UDP
119 return 0
120 }
121 if proto == NXD_PROTO_TCP {
122 if len < l4_off + 20 { d.unresolved = 1; return 0 }
123 d.l4_sport = nx_dns_get_u16_be(buf, l4_off)
124 d.l4_dport = nx_dns_get_u16_be(buf, l4_off + 2)
125 let data_off: i64 = ((buf[l4_off + 12] & 0xf0) >> 4) * 4
126 if data_off < 20 { d.unresolved = 1; return 0 }
127 d.tcp_flags = buf[l4_off + 13] & 0xff
128 d.payload_off = l4_off + data_off
129 if total_len >= ihl + data_off { d.payload_len = total_len - ihl - data_off } else { d.payload_len = 0 }
130 d.deepest = NXD_L_TCP
131 return 0
132 }
133 if proto == NXD_PROTO_ICMP {
134 d.deepest = NXD_L_ICMP
135 d.payload_off = l4_off
136 return 0
137 }
138 // IPv4 parsed, but an L4 protocol we don't deep-parse: LOUD, not silent.
139 d.unresolved = 1
140 return 0
141}
142
143// nx_dissect_digest: CANONICAL hashable digest of the typed tree -- the
144// determinism EXCEED. Hashes the NORMALIZED 7-tuple (ethertype, proto,
145// ip_src, ip_dst, sport, dport, tcp_flags) as fixed big-endian fields, so
146// two captures of the same logical packet (different framing padding /
147// link headers) hash IDENTICALLY, and any field change flips the digest.
148// out32 = caller-provided 32-byte buffer. sc = >=512-byte SHA scratch.
149func nx_dissect_digest(d: *Dissection, sc: *u8, out32: *u8) -> i64 {
150 let canon: *u8 = sys_mmap(64)
151 nxd_put_be64(canon, 0, d.l2_ethertype)
152 nxd_put_be64(canon, 8, d.l3_proto)
153 nxd_put_be64(canon, 16, d.ip_src)
154 nxd_put_be64(canon, 24, d.ip_dst)
155 nxd_put_be64(canon, 32, d.l4_sport)
156 nxd_put_be64(canon, 40, d.l4_dport)
157 nxd_put_be64(canon, 48, d.tcp_flags)
158 nx_sha256_one_shot(canon, 56, sc, out32)
159 return 0
160}
161
162func nxd_put_be64(b: *u8, o: i64, v: i64) -> i64 {
163 b[o] = ((v >> 56) & 0xff) as u8
164 b[o+1] = ((v >> 48) & 0xff) as u8
165 b[o+2] = ((v >> 40) & 0xff) as u8
166 b[o+3] = ((v >> 32) & 0xff) as u8
167 b[o+4] = ((v >> 24) & 0xff) as u8
168 b[o+5] = ((v >> 16) & 0xff) as u8
169 b[o+6] = ((v >> 8) & 0xff) as u8
170 b[o+7] = (v & 0xff) as u8
171 return 0
172}