From e6e6703aba43783057e0630983c3a86e9160b418 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Fri, 26 Jun 2026 18:12:20 -0400 Subject: [PATCH] feat(zingela): M1 packet-layer correctness KATs + SipHash SYN-cookie SIMD @Vector internet checksum proven byte-equivalent to scalar across every length 0..256 plus the 0xb861 IPv4 KAT. RFC 1624 incremental update proven against the RFC section 4 worked example (0xDD2F/0x5555/0x3285 -> 0x0000) plus 4096 random full-recompute trials. Reusable TCP pseudo-header checksum helper; smoke.zig now reuses it. New cookie.zig: stateless SipHash64(2,4) SYN-cookie over the 4-tuple, full-128-bit randomSecure key, u64 generate with write-site u32 truncation, ack == cookie +% 1 wrapping validation. Reproduces the published SipHash reference vector plus a pinned golden cookie KAT. All green Debug + ReleaseSafe (14/14). Version bumped to 0.0.0-m1. --- .../advanced/zig-stateless-scanner/build.zig | 10 +- .../zig-stateless-scanner/src/cookie.zig | 74 +++++++++++ .../zig-stateless-scanner/src/packet.zig | 123 ++++++++++++++++++ .../zig-stateless-scanner/src/smoke.zig | 9 +- 4 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 PROJECTS/advanced/zig-stateless-scanner/src/cookie.zig diff --git a/PROJECTS/advanced/zig-stateless-scanner/build.zig b/PROJECTS/advanced/zig-stateless-scanner/build.zig index 27c6d797..8f92c563 100644 --- a/PROJECTS/advanced/zig-stateless-scanner/build.zig +++ b/PROJECTS/advanced/zig-stateless-scanner/build.zig @@ -8,7 +8,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const opts = b.addOptions(); - opts.addOption([]const u8, "version", "0.0.0-m0"); + opts.addOption([]const u8, "version", "0.0.0-m1"); const packet_mod = b.createModule(.{ .root_source_file = b.path("src/packet.zig"), @@ -30,6 +30,12 @@ pub fn build(b: *std.Build) void { }); smoke_mod.addImport("packet", packet_mod); + const cookie_mod = b.createModule(.{ + .root_source_file = b.path("src/cookie.zig"), + .target = target, + .optimize = optimize, + }); + const exe = b.addExecutable(.{ .name = "zingela", .root_module = b.createModule(.{ @@ -57,7 +63,7 @@ pub fn build(b: *std.Build) void { smoke_step.dependOn(&smoke_cmd.step); const test_step = b.step("test", "Run unit tests"); - const test_mods = [_]*std.Build.Module{ packet_mod, cli_mod, smoke_mod }; + const test_mods = [_]*std.Build.Module{ packet_mod, cli_mod, smoke_mod, cookie_mod }; for (test_mods) |mod| { const t = b.addTest(.{ .root_module = mod }); const rt = b.addRunArtifact(t); diff --git a/PROJECTS/advanced/zig-stateless-scanner/src/cookie.zig b/PROJECTS/advanced/zig-stateless-scanner/src/cookie.zig new file mode 100644 index 00000000..90c8450a --- /dev/null +++ b/PROJECTS/advanced/zig-stateless-scanner/src/cookie.zig @@ -0,0 +1,74 @@ +// ©AngelaMos | 2026 +// cookie.zig + +const std = @import("std"); + +pub const Cookie = struct { + key: [16]u8, + + pub fn init(key: [16]u8) Cookie { + return .{ .key = key }; + } + + pub fn random(io: std.Io) !Cookie { + var key: [16]u8 = undefined; + try io.randomSecure(&key); + return .{ .key = key }; + } + + pub fn generate(self: Cookie, ip_them: u32, port_them: u16, ip_me: u32, port_me: u16) u64 { + var data: [12]u8 = undefined; + std.mem.writeInt(u32, data[0..4], ip_them, .big); + std.mem.writeInt(u16, data[4..6], port_them, .big); + std.mem.writeInt(u32, data[6..10], ip_me, .big); + std.mem.writeInt(u16, data[10..12], port_me, .big); + return std.hash.SipHash64(2, 4).toInt(&data, &self.key); + } + + pub fn seq(self: Cookie, ip_them: u32, port_them: u16, ip_me: u32, port_me: u16) u32 { + return @truncate(self.generate(ip_them, port_them, ip_me, port_me)); + } + + pub fn validateSynAck(self: Cookie, ack: u32, ip_them: u32, port_them: u16, ip_me: u32, port_me: u16) bool { + return ack == self.seq(ip_them, port_them, ip_me, port_me) +% 1; + } +}; + +const test_key = [16]u8{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, +}; + +test "SipHash64(2,4) reproduces the reference empty-message vector" { + try std.testing.expectEqual( + @as(u64, 0x726fdb47dd0e0e31), + std.hash.SipHash64(2, 4).toInt("", &test_key), + ); +} + +test "cookie is deterministic for a fixed key + 4-tuple (golden KAT)" { + const c = Cookie.init(test_key); + const a = c.generate(0x0a000001, 443, 0xc0a80002, 51000); + const b = c.generate(0x0a000001, 443, 0xc0a80002, 51000); + try std.testing.expectEqual(a, b); + try std.testing.expectEqual(@as(u64, 0x559a4e08e1deb9a7), a); +} + +test "seq is the low 32 bits of the cookie" { + const c = Cookie.init(test_key); + const full = c.generate(0x0a000001, 443, 0xc0a80002, 51000); + try std.testing.expectEqual(@as(u32, @truncate(full)), c.seq(0x0a000001, 443, 0xc0a80002, 51000)); +} + +test "validateSynAck accepts ack == seq + 1 and rejects others" { + const c = Cookie.init(test_key); + const s = c.seq(0x0a000001, 443, 0xc0a80002, 51000); + try std.testing.expect(c.validateSynAck(s +% 1, 0x0a000001, 443, 0xc0a80002, 51000)); + try std.testing.expect(!c.validateSynAck(s, 0x0a000001, 443, 0xc0a80002, 51000)); + try std.testing.expect(!c.validateSynAck(s +% 2, 0x0a000001, 443, 0xc0a80002, 51000)); +} + +test "validateSynAck wraps at the u32 boundary" { + const seq_max: u32 = 0xFFFFFFFF; + try std.testing.expectEqual(@as(u32, 0), seq_max +% 1); +} diff --git a/PROJECTS/advanced/zig-stateless-scanner/src/packet.zig b/PROJECTS/advanced/zig-stateless-scanner/src/packet.zig index 1069408f..434a33c9 100644 --- a/PROJECTS/advanced/zig-stateless-scanner/src/packet.zig +++ b/PROJECTS/advanced/zig-stateless-scanner/src/packet.zig @@ -2,6 +2,7 @@ // packet.zig const std = @import("std"); +const builtin = @import("builtin"); pub const EthHdr = extern struct { dst: [6]u8, @@ -56,6 +57,67 @@ pub fn checksum(bytes: []const u8) u16 { return ~@as(u16, @truncate(sum)); } +pub fn checksumSimd(bytes: []const u8) u16 { + const lanes = comptime (std.simd.suggestVectorLength(u16) orelse 8); + const stride = lanes * 2; + const native_le = builtin.cpu.arch.endian() == .little; + + var acc: @Vector(lanes, u32) = @splat(0); + var i: usize = 0; + while (i + stride <= bytes.len) : (i += stride) { + const block: [stride]u8 = bytes[i..][0..stride].*; + var words: @Vector(lanes, u16) = @bitCast(block); + if (native_le) words = @byteSwap(words); + acc += @as(@Vector(lanes, u32), words); + } + + var sum: u32 = @reduce(.Add, acc); + while (i + 1 < bytes.len) : (i += 2) { + sum += (@as(u32, bytes[i]) << 8) | @as(u32, bytes[i + 1]); + } + if (i < bytes.len) { + sum += @as(u32, bytes[i]) << 8; + } + while (sum >> 16 != 0) { + sum = (sum & 0xffff) + (sum >> 16); + } + return ~@as(u16, @truncate(sum)); +} + +pub fn incrementalUpdate(old_check: u16, old_word: u16, new_word: u16) u16 { + var sum: u32 = @as(u32, ~old_check) + @as(u32, ~old_word) + @as(u32, new_word); + while (sum >> 16 != 0) { + sum = (sum & 0xffff) + (sum >> 16); + } + return ~@as(u16, @truncate(sum)); +} + +pub fn tcpChecksum(src_be: u32, dst_be: u32, segment: []const u8) u16 { + var pseudo: [12]u8 = undefined; + @memcpy(pseudo[0..4], std.mem.asBytes(&src_be)); + @memcpy(pseudo[4..8], std.mem.asBytes(&dst_be)); + pseudo[8] = 0; + pseudo[9] = 6; + std.mem.writeInt(u16, pseudo[10..12], @intCast(segment.len), .big); + + var sum: u32 = 0; + var i: usize = 0; + while (i + 1 < pseudo.len) : (i += 2) { + sum += (@as(u32, pseudo[i]) << 8) | @as(u32, pseudo[i + 1]); + } + i = 0; + while (i + 1 < segment.len) : (i += 2) { + sum += (@as(u32, segment[i]) << 8) | @as(u32, segment[i + 1]); + } + if (i < segment.len) { + sum += @as(u32, segment[i]) << 8; + } + while (sum >> 16 != 0) { + sum = (sum & 0xffff) + (sum >> 16); + } + return ~@as(u16, @truncate(sum)); +} + test "header sizes are wire-exact" { try std.testing.expectEqual(@as(usize, 14), @sizeOf(EthHdr)); try std.testing.expectEqual(@as(usize, 20), @sizeOf(Ipv4Hdr)); @@ -70,3 +132,64 @@ test "RFC 1071 checksum matches the canonical IPv4 KAT (0xb861)" { }; try std.testing.expectEqual(@as(u16, 0xb861), checksum(&hdr)); } + +test "SIMD checksum matches the canonical IPv4 KAT (0xb861)" { + const hdr = [_]u8{ + 0x45, 0x00, 0x00, 0x73, 0x00, 0x00, 0x40, 0x00, + 0x40, 0x11, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x01, + 0xc0, 0xa8, 0x00, 0xc7, + }; + try std.testing.expectEqual(@as(u16, 0xb861), checksumSimd(&hdr)); +} + +test "SIMD checksum equals scalar checksum for every length 0..256" { + var prng = std.Random.DefaultPrng.init(0xC0FFEE_1624_517A); + const rand = prng.random(); + var buf: [256]u8 = undefined; + var len: usize = 0; + while (len <= 256) : (len += 1) { + rand.bytes(buf[0..len]); + try std.testing.expectEqual(checksum(buf[0..len]), checksumSimd(buf[0..len])); + } +} + +test "RFC 1624 incremental update matches the RFC section 4 worked example" { + try std.testing.expectEqual(@as(u16, 0x0000), incrementalUpdate(0xDD2F, 0x5555, 0x3285)); +} + +test "incremental update equals a full recompute for random single-word edits" { + var prng = std.Random.DefaultPrng.init(0x1624_DEAD_BEEF_0001); + const rand = prng.random(); + var hdr: [20]u8 = undefined; + var trial: usize = 0; + while (trial < 4096) : (trial += 1) { + rand.bytes(&hdr); + std.mem.writeInt(u16, hdr[10..12], 0, .big); + const old_check = checksum(&hdr); + const word_index = rand.uintLessThan(usize, 9) * 2; + const off = if (word_index >= 10) word_index + 2 else word_index; + const old_word = std.mem.readInt(u16, hdr[off..][0..2], .big); + const new_word = rand.int(u16); + std.mem.writeInt(u16, hdr[off..][0..2], new_word, .big); + const full = checksum(&hdr); + try std.testing.expectEqual(full, incrementalUpdate(old_check, old_word, new_word)); + } +} + +test "tcpChecksum self-verifies: a segment with its correct checksum folds to 0" { + var tcp = TcpHdr{ + .src_port = std.mem.nativeToBig(u16, 54321), + .dst_port = std.mem.nativeToBig(u16, 80), + .seq = std.mem.nativeToBig(u32, 0xdead_beef), + .ack = 0, + .data_off_ns = 0x50, + .flags = 0x02, + .window = std.mem.nativeToBig(u16, 1024), + .checksum = 0, + .urgent = 0, + }; + const src = std.mem.nativeToBig(u32, 0x7f000001); + const dst = std.mem.nativeToBig(u32, 0x7f000001); + tcp.checksum = std.mem.nativeToBig(u16, tcpChecksum(src, dst, std.mem.asBytes(&tcp))); + try std.testing.expectEqual(@as(u16, 0), tcpChecksum(src, dst, std.mem.asBytes(&tcp))); +} diff --git a/PROJECTS/advanced/zig-stateless-scanner/src/smoke.zig b/PROJECTS/advanced/zig-stateless-scanner/src/smoke.zig index 73ecfb7f..320eb0b6 100644 --- a/PROJECTS/advanced/zig-stateless-scanner/src/smoke.zig +++ b/PROJECTS/advanced/zig-stateless-scanner/src/smoke.zig @@ -42,14 +42,7 @@ fn build_syn(dst_port: u16) [54]u8 { .urgent = 0, }; - var pseudo: [12 + 20]u8 = undefined; - @memcpy(pseudo[0..4], std.mem.asBytes(&ip.src)); - @memcpy(pseudo[4..8], std.mem.asBytes(&ip.dst)); - pseudo[8] = 0; - pseudo[9] = 6; - std.mem.writeInt(u16, pseudo[10..12], 20, .big); - @memcpy(pseudo[12..32], std.mem.asBytes(&tcp)); - tcp.checksum = std.mem.nativeToBig(u16, packet.checksum(&pseudo)); + tcp.checksum = std.mem.nativeToBig(u16, packet.tcpChecksum(ip.src, ip.dst, std.mem.asBytes(&tcp))); @memcpy(frame[34..54], std.mem.asBytes(&tcp)); return frame;