648 lines
22 KiB
Zig
648 lines
22 KiB
Zig
// ©AngelaMos | 2026
|
|
// packet.zig
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
pub const EthHdr = extern struct {
|
|
dst: [6]u8,
|
|
src: [6]u8,
|
|
ethertype: u16,
|
|
};
|
|
|
|
pub const Ipv4Hdr = extern struct {
|
|
version_ihl: u8,
|
|
tos: u8,
|
|
total_len: u16,
|
|
id: u16,
|
|
flags_frag: u16,
|
|
ttl: u8,
|
|
protocol: u8,
|
|
checksum: u16,
|
|
src: u32,
|
|
dst: u32,
|
|
};
|
|
|
|
pub const TcpHdr = extern struct {
|
|
src_port: u16,
|
|
dst_port: u16,
|
|
seq: u32,
|
|
ack: u32,
|
|
data_off_ns: u8,
|
|
flags: u8,
|
|
window: u16,
|
|
checksum: u16,
|
|
urgent: u16,
|
|
};
|
|
|
|
pub const UdpHdr = extern struct {
|
|
src_port: u16,
|
|
dst_port: u16,
|
|
length: u16,
|
|
checksum: u16,
|
|
};
|
|
|
|
pub const Ipv6Hdr = extern struct {
|
|
version_tc_flow: u32,
|
|
payload_len: u16,
|
|
next_header: u8,
|
|
hop_limit: u8,
|
|
src: [16]u8,
|
|
dst: [16]u8,
|
|
};
|
|
|
|
comptime {
|
|
std.debug.assert(@sizeOf(EthHdr) == 14);
|
|
std.debug.assert(@sizeOf(Ipv4Hdr) == 20);
|
|
std.debug.assert(@sizeOf(TcpHdr) == 20);
|
|
std.debug.assert(@sizeOf(UdpHdr) == 8);
|
|
std.debug.assert(@sizeOf(Ipv6Hdr) == 40);
|
|
}
|
|
|
|
pub const Addr = union(enum) {
|
|
v4: u32,
|
|
v6: [16]u8,
|
|
|
|
pub fn eql(a: Addr, b: Addr) bool {
|
|
if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false;
|
|
return switch (a) {
|
|
.v4 => |x| x == b.v4,
|
|
.v6 => |x| std.mem.eql(u8, &x, &b.v6),
|
|
};
|
|
}
|
|
|
|
pub fn order(a: Addr, b: Addr) std.math.Order {
|
|
const fam_a: u2 = if (a == .v4) 0 else 1;
|
|
const fam_b: u2 = if (b == .v4) 0 else 1;
|
|
if (fam_a != fam_b) return std.math.order(fam_a, fam_b);
|
|
return switch (a) {
|
|
.v4 => |x| std.math.order(x, b.v4),
|
|
.v6 => |x| std.mem.order(u8, &x, &b.v6),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn checksum(bytes: []const u8) u16 {
|
|
var sum: u32 = 0;
|
|
var i: usize = 0;
|
|
while (i + 1 < bytes.len) : (i += 2) {
|
|
const word = (@as(u16, bytes[i]) << 8) | @as(u16, bytes[i + 1]);
|
|
sum += word;
|
|
}
|
|
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 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));
|
|
}
|
|
|
|
pub fn pseudoChecksum6(src: [16]u8, dst: [16]u8, next_header: u8, payload: []const u8) u16 {
|
|
var sum: u32 = 0;
|
|
var i: usize = 0;
|
|
while (i + 1 < src.len) : (i += 2) sum += (@as(u32, src[i]) << 8) | src[i + 1];
|
|
i = 0;
|
|
while (i + 1 < dst.len) : (i += 2) sum += (@as(u32, dst[i]) << 8) | dst[i + 1];
|
|
const len: u32 = @intCast(payload.len);
|
|
sum += (len >> 16) & 0xffff;
|
|
sum += len & 0xffff;
|
|
sum += next_header;
|
|
i = 0;
|
|
while (i + 1 < payload.len) : (i += 2) sum += (@as(u32, payload[i]) << 8) | payload[i + 1];
|
|
if (i < payload.len) sum += @as(u32, payload[i]) << 8;
|
|
while (sum >> 16 != 0) sum = (sum & 0xffff) + (sum >> 16);
|
|
return ~@as(u16, @truncate(sum));
|
|
}
|
|
|
|
pub fn tcpChecksum6(src: [16]u8, dst: [16]u8, segment: []const u8) u16 {
|
|
return pseudoChecksum6(src, dst, 6, segment);
|
|
}
|
|
|
|
pub fn udpChecksum(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] = 17;
|
|
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);
|
|
}
|
|
const folded: u16 = ~@as(u16, @truncate(sum));
|
|
return if (folded == 0) 0xffff else folded;
|
|
}
|
|
|
|
pub const TcpFlag = struct {
|
|
pub const fin: u8 = 0x01;
|
|
pub const syn: u8 = 0x02;
|
|
pub const rst: u8 = 0x04;
|
|
pub const psh: u8 = 0x08;
|
|
pub const ack: u8 = 0x10;
|
|
pub const urg: u8 = 0x20;
|
|
};
|
|
|
|
pub const ScanType = enum {
|
|
syn,
|
|
fin,
|
|
null_scan,
|
|
xmas,
|
|
maimon,
|
|
ack,
|
|
window,
|
|
|
|
pub fn probeFlags(self: ScanType) u8 {
|
|
return switch (self) {
|
|
.syn => TcpFlag.syn,
|
|
.fin => TcpFlag.fin,
|
|
.null_scan => 0,
|
|
.xmas => TcpFlag.fin | TcpFlag.psh | TcpFlag.urg,
|
|
.maimon => TcpFlag.fin | TcpFlag.ack,
|
|
.ack, .window => TcpFlag.ack,
|
|
};
|
|
}
|
|
|
|
pub fn cookieInAck(self: ScanType) bool {
|
|
return switch (self) {
|
|
.maimon, .ack, .window => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
pub fn seqConsumed(self: ScanType) u32 {
|
|
return switch (self) {
|
|
.syn, .fin, .xmas => 1,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
pub fn parse(text: []const u8) ?ScanType {
|
|
const map = .{
|
|
.{ "syn", ScanType.syn },
|
|
.{ "fin", ScanType.fin },
|
|
.{ "null", ScanType.null_scan },
|
|
.{ "xmas", ScanType.xmas },
|
|
.{ "maimon", ScanType.maimon },
|
|
.{ "ack", ScanType.ack },
|
|
.{ "window", ScanType.window },
|
|
};
|
|
inline for (map) |entry| {
|
|
if (std.mem.eql(u8, text, entry[0])) return entry[1];
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const opt_eol: u8 = 0;
|
|
const opt_nop: u8 = 1;
|
|
const opt_mss: u8 = 2;
|
|
const opt_mss_len: u8 = 4;
|
|
const opt_wscale: u8 = 3;
|
|
const opt_wscale_len: u8 = 3;
|
|
const opt_sack_perm: u8 = 4;
|
|
const opt_sack_perm_len: u8 = 2;
|
|
const opt_ts: u8 = 8;
|
|
const opt_ts_len: u8 = 10;
|
|
|
|
const mss_ethernet_hi: u8 = 0x05;
|
|
const mss_ethernet_lo: u8 = 0xb4;
|
|
|
|
const wscale_linux: u8 = 7;
|
|
const wscale_windows: u8 = 8;
|
|
const wscale_macos: u8 = 6;
|
|
|
|
const window_minimal: u16 = 1024;
|
|
const window_linux: u16 = 64240;
|
|
const window_windows: u16 = 64240;
|
|
const window_macos: u16 = 65535;
|
|
|
|
const syn_opts_masscan = [_]u8{ opt_mss, opt_mss_len, mss_ethernet_hi, mss_ethernet_lo };
|
|
|
|
const syn_opts_linux = [_]u8{
|
|
opt_mss, opt_mss_len, mss_ethernet_hi, mss_ethernet_lo,
|
|
opt_sack_perm, opt_sack_perm_len, opt_ts, opt_ts_len,
|
|
0, 0, 0, 0,
|
|
0, 0, 0, 0,
|
|
opt_nop, opt_wscale, opt_wscale_len, wscale_linux,
|
|
};
|
|
|
|
const syn_opts_windows = [_]u8{
|
|
opt_mss, opt_mss_len, mss_ethernet_hi, mss_ethernet_lo,
|
|
opt_nop, opt_wscale, opt_wscale_len, wscale_windows,
|
|
opt_nop, opt_nop, opt_sack_perm, opt_sack_perm_len,
|
|
};
|
|
|
|
const syn_opts_macos = [_]u8{
|
|
opt_mss, opt_mss_len, mss_ethernet_hi, mss_ethernet_lo,
|
|
opt_nop, opt_wscale, opt_wscale_len, wscale_macos,
|
|
opt_nop, opt_nop, opt_ts, opt_ts_len,
|
|
0, 0, 0, 0,
|
|
0, 0, 0, 0,
|
|
opt_sack_perm, opt_sack_perm_len, opt_eol, opt_eol,
|
|
};
|
|
|
|
pub const max_syn_options_len: usize = syn_opts_macos.len;
|
|
|
|
pub const OsProfile = enum {
|
|
none,
|
|
masscan,
|
|
linux,
|
|
windows,
|
|
macos,
|
|
|
|
pub fn options(self: OsProfile) []const u8 {
|
|
return switch (self) {
|
|
.none => &.{},
|
|
.masscan => &syn_opts_masscan,
|
|
.linux => &syn_opts_linux,
|
|
.windows => &syn_opts_windows,
|
|
.macos => &syn_opts_macos,
|
|
};
|
|
}
|
|
|
|
pub fn window(self: OsProfile) u16 {
|
|
return switch (self) {
|
|
.none, .masscan => window_minimal,
|
|
.linux => window_linux,
|
|
.windows => window_windows,
|
|
.macos => window_macos,
|
|
};
|
|
}
|
|
|
|
pub fn tsValOffset(self: OsProfile) ?usize {
|
|
return switch (self) {
|
|
.linux => 8,
|
|
.macos => 12,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
pub fn variesIpId(self: OsProfile) bool {
|
|
return switch (self) {
|
|
.windows, .macos => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
pub fn parse(text: []const u8) ?OsProfile {
|
|
const map = .{
|
|
.{ "none", OsProfile.none },
|
|
.{ "masscan", OsProfile.masscan },
|
|
.{ "linux", OsProfile.linux },
|
|
.{ "windows", OsProfile.windows },
|
|
.{ "macos", OsProfile.macos },
|
|
};
|
|
inline for (map) |entry| {
|
|
if (std.mem.eql(u8, text, entry[0])) return entry[1];
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
pub fn optionKinds(opts: []const u8, out: []u8) usize {
|
|
var i: usize = 0;
|
|
var n: usize = 0;
|
|
while (i < opts.len) {
|
|
const kind = opts[i];
|
|
if (kind == opt_eol) break;
|
|
if (n < out.len) {
|
|
out[n] = kind;
|
|
n += 1;
|
|
}
|
|
if (kind == opt_nop) {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (i + 1 >= opts.len) break;
|
|
const len = opts[i + 1];
|
|
if (len < 2) break;
|
|
i += len;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
comptime {
|
|
std.debug.assert((@sizeOf(TcpHdr) + syn_opts_masscan.len) % 4 == 0);
|
|
std.debug.assert((@sizeOf(TcpHdr) + syn_opts_linux.len) % 4 == 0);
|
|
std.debug.assert((@sizeOf(TcpHdr) + syn_opts_windows.len) % 4 == 0);
|
|
std.debug.assert((@sizeOf(TcpHdr) + syn_opts_macos.len) % 4 == 0);
|
|
std.debug.assert(syn_opts_linux.len == 20);
|
|
std.debug.assert(syn_opts_windows.len == 12);
|
|
std.debug.assert(syn_opts_macos.len == 24);
|
|
}
|
|
|
|
test "header sizes are wire-exact" {
|
|
try std.testing.expectEqual(@as(usize, 14), @sizeOf(EthHdr));
|
|
try std.testing.expectEqual(@as(usize, 20), @sizeOf(Ipv4Hdr));
|
|
try std.testing.expectEqual(@as(usize, 20), @sizeOf(TcpHdr));
|
|
try std.testing.expectEqual(@as(usize, 8), @sizeOf(UdpHdr));
|
|
}
|
|
|
|
test "RFC 1071 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), 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)));
|
|
}
|
|
|
|
test "Ipv6 header is wire-exact 40 bytes" {
|
|
try std.testing.expectEqual(@as(usize, 40), @sizeOf(Ipv6Hdr));
|
|
}
|
|
|
|
test "tcpChecksum6 equals a full RFC 1071 sum over the assembled IPv6 pseudo-header (independent path)" {
|
|
const src = [16]u8{ 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
|
|
const dst = [16]u8{ 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
|
|
const seg = [_]u8{ 0xde, 0xad, 0x00, 0x50, 0x11, 0x22, 0x33, 0x44, 0, 0, 0, 0, 0x50, 0x02, 0x04, 0x00, 0, 0, 0, 0 };
|
|
var buf: [40 + seg.len]u8 = undefined;
|
|
@memcpy(buf[0..16], &src);
|
|
@memcpy(buf[16..32], &dst);
|
|
std.mem.writeInt(u32, buf[32..36], @intCast(seg.len), .big);
|
|
buf[36] = 0;
|
|
buf[37] = 0;
|
|
buf[38] = 0;
|
|
buf[39] = 6;
|
|
@memcpy(buf[40..], &seg);
|
|
try std.testing.expectEqual(checksum(&buf), tcpChecksum6(src, dst, &seg));
|
|
}
|
|
|
|
test "tcpChecksum6 self-verifies: a correctly-summed segment folds back to 0" {
|
|
const src = [16]u8{ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
|
|
const dst = [16]u8{ 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
|
|
var tcp = TcpHdr{
|
|
.src_port = std.mem.nativeToBig(u16, 54321),
|
|
.dst_port = std.mem.nativeToBig(u16, 443),
|
|
.seq = std.mem.nativeToBig(u32, 0x1234_5678),
|
|
.ack = 0,
|
|
.data_off_ns = 0x50,
|
|
.flags = 0x02,
|
|
.window = std.mem.nativeToBig(u16, 65535),
|
|
.checksum = 0,
|
|
.urgent = 0,
|
|
};
|
|
tcp.checksum = std.mem.nativeToBig(u16, tcpChecksum6(src, dst, std.mem.asBytes(&tcp)));
|
|
try std.testing.expectEqual(@as(u16, 0), tcpChecksum6(src, dst, std.mem.asBytes(&tcp)));
|
|
}
|
|
|
|
test "udpChecksum self-verifies: a correct datagram re-sums to the 0xFFFF all-ones marker" {
|
|
const src = std.mem.nativeToBig(u32, 0x7f000001);
|
|
const dst = std.mem.nativeToBig(u32, 0x08080808);
|
|
var seg: [12]u8 = undefined;
|
|
var hdr = UdpHdr{
|
|
.src_port = std.mem.nativeToBig(u16, 40000),
|
|
.dst_port = std.mem.nativeToBig(u16, 53),
|
|
.length = std.mem.nativeToBig(u16, 12),
|
|
.checksum = 0,
|
|
};
|
|
@memcpy(seg[0..8], std.mem.asBytes(&hdr));
|
|
@memcpy(seg[8..12], "abcd");
|
|
const ck = udpChecksum(src, dst, &seg);
|
|
try std.testing.expect(ck != 0);
|
|
hdr.checksum = std.mem.nativeToBig(u16, ck);
|
|
@memcpy(seg[0..8], std.mem.asBytes(&hdr));
|
|
try std.testing.expectEqual(@as(u16, 0xffff), udpChecksum(src, dst, &seg));
|
|
}
|
|
|
|
test "udpChecksum maps a computed 0x0000 to 0xFFFF (IPv4 UDP quirk)" {
|
|
try std.testing.expectEqual(@as(u16, 0xffff), udpChecksum(0, 0, &[_]u8{ 0xff, 0xec }));
|
|
try std.testing.expect(udpChecksum(0, 0, &[_]u8{ 0xff, 0xec }) != 0);
|
|
}
|
|
|
|
test "the Linux SYN option chain decodes to the authoritative JA4T kind list 2-4-8-1-3" {
|
|
var kinds: [16]u8 = undefined;
|
|
const n = optionKinds(OsProfile.linux.options(), &kinds);
|
|
try std.testing.expectEqualSlices(u8, &.{ 2, 4, 8, 1, 3 }, kinds[0..n]);
|
|
}
|
|
|
|
test "the Windows SYN option chain omits the timestamp (kinds 2-1-3-1-1-4)" {
|
|
var kinds: [16]u8 = undefined;
|
|
const n = optionKinds(OsProfile.windows.options(), &kinds);
|
|
try std.testing.expectEqualSlices(u8, &.{ 2, 1, 3, 1, 1, 4 }, kinds[0..n]);
|
|
for (kinds[0..n]) |k| try std.testing.expect(k != 8);
|
|
}
|
|
|
|
test "the macOS SYN option chain carries the timestamp before SACK (kinds 2-1-3-1-1-8-4)" {
|
|
var kinds: [16]u8 = undefined;
|
|
const n = optionKinds(OsProfile.macos.options(), &kinds);
|
|
try std.testing.expectEqualSlices(u8, &.{ 2, 1, 3, 1, 1, 8, 4 }, kinds[0..n]);
|
|
}
|
|
|
|
test "the masscan profile sends exactly one MSS option and the fingerprintable 1024 window" {
|
|
var kinds: [16]u8 = undefined;
|
|
const n = optionKinds(OsProfile.masscan.options(), &kinds);
|
|
try std.testing.expectEqualSlices(u8, &.{2}, kinds[0..n]);
|
|
try std.testing.expectEqual(@as(u16, 1024), OsProfile.masscan.window());
|
|
}
|
|
|
|
test "the bare profile sends no options" {
|
|
try std.testing.expectEqual(@as(usize, 0), OsProfile.none.options().len);
|
|
}
|
|
|
|
test "every OS profile advertises the ethernet MSS 1460" {
|
|
for ([_]OsProfile{ .masscan, .linux, .windows, .macos }) |p| {
|
|
const opts = p.options();
|
|
try std.testing.expectEqual(opt_mss, opts[0]);
|
|
try std.testing.expectEqual(@as(u16, 1460), std.mem.readInt(u16, opts[2..4], .big));
|
|
}
|
|
}
|
|
|
|
test "the timestamp offset points at four zero bytes inside the option chain" {
|
|
inline for ([_]OsProfile{ .linux, .macos }) |p| {
|
|
const off = p.tsValOffset().?;
|
|
const opts = p.options();
|
|
try std.testing.expectEqual(opt_ts, opts[off - 2]);
|
|
try std.testing.expectEqual(opt_ts_len, opts[off - 1]);
|
|
try std.testing.expectEqual(@as(u32, 0), std.mem.readInt(u32, opts[off..][0..4], .big));
|
|
}
|
|
try std.testing.expect(OsProfile.windows.tsValOffset() == null);
|
|
}
|
|
|
|
test "scan-type probe flags match the RFC 793 flag combinations" {
|
|
try std.testing.expectEqual(TcpFlag.syn, ScanType.syn.probeFlags());
|
|
try std.testing.expectEqual(TcpFlag.fin, ScanType.fin.probeFlags());
|
|
try std.testing.expectEqual(@as(u8, 0), ScanType.null_scan.probeFlags());
|
|
try std.testing.expectEqual(TcpFlag.fin | TcpFlag.psh | TcpFlag.urg, ScanType.xmas.probeFlags());
|
|
try std.testing.expectEqual(TcpFlag.fin | TcpFlag.ack, ScanType.maimon.probeFlags());
|
|
try std.testing.expectEqual(TcpFlag.ack, ScanType.ack.probeFlags());
|
|
try std.testing.expectEqual(TcpFlag.ack, ScanType.window.probeFlags());
|
|
}
|
|
|
|
test "ack-flag scans carry the cookie in the ack field, seq-scans in the seq field" {
|
|
for ([_]ScanType{ .maimon, .ack, .window }) |st| try std.testing.expect(st.cookieInAck());
|
|
for ([_]ScanType{ .syn, .fin, .null_scan, .xmas }) |st| try std.testing.expect(!st.cookieInAck());
|
|
}
|
|
|
|
test "seqConsumed reflects whether the probe advances the sequence space" {
|
|
for ([_]ScanType{ .syn, .fin, .xmas }) |st| try std.testing.expectEqual(@as(u32, 1), st.seqConsumed());
|
|
for ([_]ScanType{ .null_scan, .maimon, .ack, .window }) |st| try std.testing.expectEqual(@as(u32, 0), st.seqConsumed());
|
|
}
|
|
|
|
test "scan-type and OS-profile parsers round-trip the CLI spellings" {
|
|
try std.testing.expectEqual(ScanType.null_scan, ScanType.parse("null").?);
|
|
try std.testing.expectEqual(ScanType.window, ScanType.parse("window").?);
|
|
try std.testing.expect(ScanType.parse("bogus") == null);
|
|
try std.testing.expectEqual(OsProfile.macos, OsProfile.parse("macos").?);
|
|
try std.testing.expect(OsProfile.parse("bogus") == null);
|
|
}
|
|
|
|
test "Addr.eql distinguishes families and values" {
|
|
const a = Addr{ .v4 = 0x0a000001 };
|
|
const b = Addr{ .v4 = 0x0a000001 };
|
|
const c = Addr{ .v4 = 0x0a000002 };
|
|
const v6a = Addr{ .v6 = [_]u8{0} ** 15 ++ [_]u8{1} };
|
|
const v6b = Addr{ .v6 = [_]u8{0} ** 15 ++ [_]u8{1} };
|
|
const v6c = Addr{ .v6 = [_]u8{0} ** 15 ++ [_]u8{2} };
|
|
try std.testing.expect(a.eql(b));
|
|
try std.testing.expect(!a.eql(c));
|
|
try std.testing.expect(v6a.eql(v6b));
|
|
try std.testing.expect(!v6a.eql(v6c));
|
|
try std.testing.expect(!a.eql(v6a));
|
|
}
|
|
|
|
test "Addr.order sorts v4 before v6, then by value" {
|
|
const v4lo = Addr{ .v4 = 1 };
|
|
const v4hi = Addr{ .v4 = 2 };
|
|
const v6lo = Addr{ .v6 = [_]u8{0} ** 16 };
|
|
const v6hi = Addr{ .v6 = [_]u8{0} ** 15 ++ [_]u8{1} };
|
|
try std.testing.expectEqual(std.math.Order.lt, v4lo.order(v4hi));
|
|
try std.testing.expectEqual(std.math.Order.lt, v4hi.order(v6lo));
|
|
try std.testing.expectEqual(std.math.Order.lt, v6lo.order(v6hi));
|
|
try std.testing.expectEqual(std.math.Order.eq, v6hi.order(v6hi));
|
|
}
|