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.
This commit is contained in:
CarterPerez-dev 2026-06-26 18:12:20 -04:00
parent 79e155ae94
commit e6e6703aba
4 changed files with 206 additions and 10 deletions

View File

@ -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);

View File

@ -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);
}

View File

@ -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)));
}

View File

@ -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;