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:
parent
79e155ae94
commit
e6e6703aba
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue