code wiki / (root) / nx_netscope_dissect.nx

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}