feat(zingela): M6 UDP scan - source-port cookie, compile-time payload table, ICMP type3/code3 classification
This commit is contained in:
parent
16429c20c2
commit
29849fd258
|
|
@ -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-m5");
|
||||
opts.addOption([]const u8, "version", "0.0.0-m6");
|
||||
|
||||
const packet_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/packet.zig"),
|
||||
|
|
@ -63,6 +63,21 @@ pub fn build(b: *std.Build) void {
|
|||
template_mod.addImport("packet", packet_mod);
|
||||
template_mod.addImport("cookie", cookie_mod);
|
||||
|
||||
const payloads_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/payloads.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const udp_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/udp.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
udp_mod.addImport("packet", packet_mod);
|
||||
udp_mod.addImport("cookie", cookie_mod);
|
||||
udp_mod.addImport("payloads", payloads_mod);
|
||||
|
||||
const afpacket_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/afpacket.zig"),
|
||||
.target = target,
|
||||
|
|
@ -138,6 +153,7 @@ pub fn build(b: *std.Build) void {
|
|||
});
|
||||
scancmd_mod.addImport("targets", targets_mod);
|
||||
scancmd_mod.addImport("template", template_mod);
|
||||
scancmd_mod.addImport("udp", udp_mod);
|
||||
scancmd_mod.addImport("ratelimit", ratelimit_mod);
|
||||
scancmd_mod.addImport("afpacket", afpacket_mod);
|
||||
scancmd_mod.addImport("cookie", cookie_mod);
|
||||
|
|
@ -176,7 +192,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, cookie_mod, numtheory_mod, targets_mod, ratelimit_mod, template_mod, afpacket_mod, tx_mod, txcmd_mod, classify_mod, dedup_mod, rx_mod, netutil_mod, output_mod, scancmd_mod };
|
||||
const test_mods = [_]*std.Build.Module{ packet_mod, cli_mod, smoke_mod, cookie_mod, numtheory_mod, targets_mod, ratelimit_mod, template_mod, payloads_mod, udp_mod, afpacket_mod, tx_mod, txcmd_mod, classify_mod, dedup_mod, rx_mod, netutil_mod, output_mod, scancmd_mod };
|
||||
for (test_mods) |mod| {
|
||||
const t = b.addTest(.{ .root_module = mod });
|
||||
const rt = b.addRunArtifact(t);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const ETH_OFF_TYPE: usize = 12;
|
|||
const ETHERTYPE_IPV4: u16 = 0x0800;
|
||||
|
||||
const IPPROTO_TCP: u8 = 6;
|
||||
const IPPROTO_UDP: u8 = 17;
|
||||
const IPPROTO_ICMP: u8 = 1;
|
||||
const IP_MIN_IHL: usize = 20;
|
||||
const IP_IHL_MASK: u8 = 0x0f;
|
||||
|
|
@ -27,6 +28,13 @@ const IP_OFF_SRC: usize = 12;
|
|||
const IP_OFF_DST: usize = 16;
|
||||
|
||||
const INNER_TCP_MIN_LEN: usize = 8;
|
||||
const INNER_UDP_MIN_LEN: usize = 8;
|
||||
|
||||
const UDP_OFF_SPORT: usize = 0;
|
||||
const UDP_OFF_DPORT: usize = 2;
|
||||
const UDP_MIN_LEN: usize = 8;
|
||||
|
||||
const ICMP_CODE_PORT_UNREACH: u8 = 3;
|
||||
|
||||
const TCP_OFF_SPORT: usize = 0;
|
||||
const TCP_OFF_DPORT: usize = 2;
|
||||
|
|
@ -51,6 +59,13 @@ fn icmpCodeIsFilteredForSyn(code: u8) bool {
|
|||
};
|
||||
}
|
||||
|
||||
fn icmpCodeIsFilteredForUdp(code: u8) bool {
|
||||
return switch (code) {
|
||||
1, 2, 9, 10, 13 => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn ihlBytes(first_byte: u8) usize {
|
||||
return @as(usize, first_byte & IP_IHL_MASK) * IP_WORD_BYTES;
|
||||
}
|
||||
|
|
@ -116,10 +131,84 @@ pub fn classify(frame: []const u8, ck: cookie.Cookie) ?Result {
|
|||
return null;
|
||||
}
|
||||
|
||||
pub fn classifyUdp(frame: []const u8, ck: cookie.Cookie, base: u16, span: u16) ?Result {
|
||||
if (frame.len < ETH_HDR_LEN + IP_MIN_IHL) return null;
|
||||
if (std.mem.readInt(u16, frame[ETH_OFF_TYPE..][0..2], .big) != ETHERTYPE_IPV4) return null;
|
||||
|
||||
const ip = ETH_HDR_LEN;
|
||||
const ihl = ihlBytes(frame[ip]);
|
||||
if (ihl < IP_MIN_IHL or frame.len < ip + ihl) return null;
|
||||
|
||||
const proto = frame[ip + IP_OFF_PROTO];
|
||||
const ip_src = std.mem.readInt(u32, frame[ip + IP_OFF_SRC ..][0..4], .big);
|
||||
const ip_dst = std.mem.readInt(u32, frame[ip + IP_OFF_DST ..][0..4], .big);
|
||||
|
||||
if (proto == IPPROTO_UDP) {
|
||||
const udp = ip + ihl;
|
||||
if (frame.len < udp + UDP_MIN_LEN) return null;
|
||||
const sport = std.mem.readInt(u16, frame[udp + UDP_OFF_SPORT ..][0..2], .big);
|
||||
const dport = std.mem.readInt(u16, frame[udp + UDP_OFF_DPORT ..][0..2], .big);
|
||||
if (dport == ck.udpSrcPort(ip_src, sport, ip_dst, base, span))
|
||||
return .{ .ip = ip_src, .port = sport, .state = .open };
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proto == IPPROTO_ICMP) {
|
||||
const icmp = ip + ihl;
|
||||
if (frame.len < icmp + ICMP_HDR_LEN) return null;
|
||||
if (frame[icmp + ICMP_OFF_TYPE] != ICMP_TYPE_DEST_UNREACH) return null;
|
||||
const code = frame[icmp + ICMP_OFF_CODE];
|
||||
const state: State = if (code == ICMP_CODE_PORT_UNREACH)
|
||||
.closed
|
||||
else if (icmpCodeIsFilteredForUdp(code))
|
||||
.filtered
|
||||
else
|
||||
return null;
|
||||
|
||||
const inner = icmp + ICMP_HDR_LEN;
|
||||
if (frame.len < inner + IP_MIN_IHL) return null;
|
||||
const inner_ihl = ihlBytes(frame[inner]);
|
||||
if (inner_ihl < IP_MIN_IHL) return null;
|
||||
if (frame[inner + IP_OFF_PROTO] != IPPROTO_UDP) return null;
|
||||
|
||||
const inner_src = std.mem.readInt(u32, frame[inner + IP_OFF_SRC ..][0..4], .big);
|
||||
const inner_dst = std.mem.readInt(u32, frame[inner + IP_OFF_DST ..][0..4], .big);
|
||||
const inner_udp = inner + inner_ihl;
|
||||
if (frame.len < inner_udp + INNER_UDP_MIN_LEN) return null;
|
||||
const inner_sport = std.mem.readInt(u16, frame[inner_udp + UDP_OFF_SPORT ..][0..2], .big);
|
||||
const inner_dport = std.mem.readInt(u16, frame[inner_udp + UDP_OFF_DPORT ..][0..2], .big);
|
||||
|
||||
if (inner_sport == ck.udpSrcPort(inner_dst, inner_dport, inner_src, base, span))
|
||||
return .{ .ip = inner_dst, .port = inner_dport, .state = state };
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const TcpClassifier = struct {
|
||||
ck: cookie.Cookie,
|
||||
|
||||
pub fn match(self: TcpClassifier, frame: []const u8) ?Result {
|
||||
return classify(frame, self.ck);
|
||||
}
|
||||
};
|
||||
|
||||
pub const UdpClassifier = struct {
|
||||
ck: cookie.Cookie,
|
||||
base: u16,
|
||||
span: u16,
|
||||
|
||||
pub fn match(self: UdpClassifier, frame: []const u8) ?Result {
|
||||
return classifyUdp(frame, self.ck, self.base, self.span);
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(packet.EthHdr) == ETH_HDR_LEN);
|
||||
std.debug.assert(@sizeOf(packet.Ipv4Hdr) == IP_MIN_IHL);
|
||||
std.debug.assert(@sizeOf(packet.TcpHdr) == TCP_MIN_LEN);
|
||||
std.debug.assert(@sizeOf(packet.UdpHdr) == UDP_MIN_LEN);
|
||||
}
|
||||
|
||||
const test_key = [16]u8{
|
||||
|
|
@ -253,3 +342,133 @@ test "runt frames return null instead of reading out of bounds" {
|
|||
var empty = [_]u8{};
|
||||
try std.testing.expect(classify(&empty, ck) == null);
|
||||
}
|
||||
|
||||
const udp_base: u16 = 40000;
|
||||
const udp_span: u16 = 8192;
|
||||
const udp_port: u16 = 53;
|
||||
|
||||
fn buildUdpReply(buf: *[42]u8, ip_src: u32, ip_dst: u32, sport: u16, dport: u16) void {
|
||||
@memset(buf, 0);
|
||||
std.mem.writeInt(u16, buf[ETH_OFF_TYPE..][0..2], ETHERTYPE_IPV4, .big);
|
||||
buf[ETH_HDR_LEN] = 0x45;
|
||||
buf[ETH_HDR_LEN + IP_OFF_PROTO] = IPPROTO_UDP;
|
||||
std.mem.writeInt(u32, buf[ETH_HDR_LEN + IP_OFF_SRC ..][0..4], ip_src, .big);
|
||||
std.mem.writeInt(u32, buf[ETH_HDR_LEN + IP_OFF_DST ..][0..4], ip_dst, .big);
|
||||
const udp = ETH_HDR_LEN + IP_MIN_IHL;
|
||||
std.mem.writeInt(u16, buf[udp + UDP_OFF_SPORT ..][0..2], sport, .big);
|
||||
std.mem.writeInt(u16, buf[udp + UDP_OFF_DPORT ..][0..2], dport, .big);
|
||||
}
|
||||
|
||||
fn buildIcmpUdpUnreach(buf: *[128]u8, code: u8, inner_src: u32, inner_dst: u32, inner_sport: u16, inner_dport: u16) usize {
|
||||
@memset(buf, 0);
|
||||
std.mem.writeInt(u16, buf[ETH_OFF_TYPE..][0..2], ETHERTYPE_IPV4, .big);
|
||||
buf[ETH_HDR_LEN] = 0x45;
|
||||
buf[ETH_HDR_LEN + IP_OFF_PROTO] = IPPROTO_ICMP;
|
||||
std.mem.writeInt(u32, buf[ETH_HDR_LEN + IP_OFF_SRC ..][0..4], inner_dst, .big);
|
||||
std.mem.writeInt(u32, buf[ETH_HDR_LEN + IP_OFF_DST ..][0..4], inner_src, .big);
|
||||
const icmp = ETH_HDR_LEN + IP_MIN_IHL;
|
||||
buf[icmp + ICMP_OFF_TYPE] = ICMP_TYPE_DEST_UNREACH;
|
||||
buf[icmp + ICMP_OFF_CODE] = code;
|
||||
const inner = icmp + ICMP_HDR_LEN;
|
||||
buf[inner] = 0x45;
|
||||
buf[inner + IP_OFF_PROTO] = IPPROTO_UDP;
|
||||
std.mem.writeInt(u32, buf[inner + IP_OFF_SRC ..][0..4], inner_src, .big);
|
||||
std.mem.writeInt(u32, buf[inner + IP_OFF_DST ..][0..4], inner_dst, .big);
|
||||
const inner_udp = inner + IP_MIN_IHL;
|
||||
std.mem.writeInt(u16, buf[inner_udp + UDP_OFF_SPORT ..][0..2], inner_sport, .big);
|
||||
std.mem.writeInt(u16, buf[inner_udp + UDP_OFF_DPORT ..][0..2], inner_dport, .big);
|
||||
return inner_udp + INNER_UDP_MIN_LEN;
|
||||
}
|
||||
|
||||
test "validated UDP response classifies as open" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var f: [42]u8 = undefined;
|
||||
buildUdpReply(&f, their_ip, our_ip, udp_port, our_src);
|
||||
const r = classifyUdp(&f, ck, udp_base, udp_span).?;
|
||||
try std.testing.expectEqual(State.open, r.state);
|
||||
try std.testing.expectEqual(their_ip, r.ip);
|
||||
try std.testing.expectEqual(udp_port, r.port);
|
||||
}
|
||||
|
||||
test "UDP response to a non-cookie destination port is rejected (anti-spoof)" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
var f: [42]u8 = undefined;
|
||||
buildUdpReply(&f, their_ip, our_ip, udp_port, 12345);
|
||||
try std.testing.expect(classifyUdp(&f, ck, udp_base, udp_span) == null);
|
||||
}
|
||||
|
||||
test "validated ICMP port-unreachable (type 3 code 3) classifies as closed for UDP" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var f: [128]u8 = undefined;
|
||||
const len = buildIcmpUdpUnreach(&f, 3, our_ip, their_ip, our_src, udp_port);
|
||||
const r = classifyUdp(f[0..len], ck, udp_base, udp_span).?;
|
||||
try std.testing.expectEqual(State.closed, r.state);
|
||||
try std.testing.expectEqual(their_ip, r.ip);
|
||||
try std.testing.expectEqual(udp_port, r.port);
|
||||
}
|
||||
|
||||
test "validated ICMP type 3 code 1 classifies as filtered for UDP" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var f: [128]u8 = undefined;
|
||||
const len = buildIcmpUdpUnreach(&f, 1, our_ip, their_ip, our_src, udp_port);
|
||||
const r = classifyUdp(f[0..len], ck, udp_base, udp_span).?;
|
||||
try std.testing.expectEqual(State.filtered, r.state);
|
||||
try std.testing.expectEqual(their_ip, r.ip);
|
||||
try std.testing.expectEqual(udp_port, r.port);
|
||||
}
|
||||
|
||||
test "ICMP UDP-unreachable with a mismatched inner source port is rejected" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
var f: [128]u8 = undefined;
|
||||
const len = buildIcmpUdpUnreach(&f, 3, our_ip, their_ip, 9999, udp_port);
|
||||
try std.testing.expect(classifyUdp(f[0..len], ck, udp_base, udp_span) == null);
|
||||
}
|
||||
|
||||
test "ICMP UDP-unreachable with a non-unreachable type is ignored" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var f: [128]u8 = undefined;
|
||||
const len = buildIcmpUdpUnreach(&f, 3, our_ip, their_ip, our_src, udp_port);
|
||||
f[ETH_HDR_LEN + IP_MIN_IHL + ICMP_OFF_TYPE] = 8;
|
||||
try std.testing.expect(classifyUdp(f[0..len], ck, udp_base, udp_span) == null);
|
||||
}
|
||||
|
||||
test "classifyUdp ignores non-IPv4 and runt frames" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
var f: [42]u8 = undefined;
|
||||
buildUdpReply(&f, their_ip, our_ip, udp_port, ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span));
|
||||
std.mem.writeInt(u16, f[ETH_OFF_TYPE..][0..2], 0x0806, .big);
|
||||
try std.testing.expect(classifyUdp(&f, ck, udp_base, udp_span) == null);
|
||||
var tiny = [_]u8{0} ** 20;
|
||||
try std.testing.expect(classifyUdp(&tiny, ck, udp_base, udp_span) == null);
|
||||
}
|
||||
|
||||
test "the same ICMP code 3 is closed for UDP but filtered for a TCP SYN scan" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var f: [128]u8 = undefined;
|
||||
const len = buildIcmpUdpUnreach(&f, 3, our_ip, their_ip, our_src, udp_port);
|
||||
try std.testing.expectEqual(State.closed, classifyUdp(f[0..len], ck, udp_base, udp_span).?.state);
|
||||
try std.testing.expect(icmpCodeIsFilteredForSyn(3));
|
||||
try std.testing.expect(!icmpCodeIsFilteredForUdp(3));
|
||||
}
|
||||
|
||||
test "classifier adapters route each frame to the right protocol path" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
|
||||
const our_seq = ck.seq(their_ip, their_port, our_ip, our_port);
|
||||
var tcp_f: [54]u8 = undefined;
|
||||
buildTcpReply(&tcp_f, their_ip, our_ip, their_port, our_port, 0xCAFEBABE, our_seq +% 1, TCP_FLAG_SYN | TCP_FLAG_ACK);
|
||||
const tcp_clf = TcpClassifier{ .ck = ck };
|
||||
try std.testing.expectEqual(State.open, tcp_clf.match(&tcp_f).?.state);
|
||||
|
||||
const our_src = ck.udpSrcPort(their_ip, udp_port, our_ip, udp_base, udp_span);
|
||||
var udp_f: [42]u8 = undefined;
|
||||
buildUdpReply(&udp_f, their_ip, our_ip, udp_port, our_src);
|
||||
const udp_clf = UdpClassifier{ .ck = ck, .base = udp_base, .span = udp_span };
|
||||
try std.testing.expectEqual(State.open, udp_clf.match(&udp_f).?.state);
|
||||
try std.testing.expect(tcp_clf.match(&udp_f) == null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,16 +48,18 @@ pub fn printHelp(io: std.Io, env: *std.process.Environ.Map) !void {
|
|||
\\
|
||||
\\tx / scan options:
|
||||
\\ --target <cidr> target range, required (e.g. 10.0.0.0/24)
|
||||
\\ --ports <list> comma-separated dst ports (default 80)
|
||||
\\ --ports <list> comma-separated dst ports (default 80; --udp: 53,123,161)
|
||||
\\ --rate <pps> token-bucket rate, packets per second (default 10000)
|
||||
\\ --count <n> stop after n packets (default: every target once)
|
||||
\\ --iface <name> egress interface (default lo)
|
||||
\\ --src-ip <addr> source IPv4 (default: resolved from --iface)
|
||||
\\ --src-port <n> source TCP port (default 40000)
|
||||
\\ --src-port <n> source port; UDP uses it as the cookie-range base (default 40000)
|
||||
\\ --gw-mac <mac> gateway/dst MAC aa:bb:cc:dd:ee:ff (default 00:..:00)
|
||||
\\ --seed <n> permutation seed (default: per-scan CSPRNG)
|
||||
\\
|
||||
\\scan-only options:
|
||||
\\ --udp UDP scan: per-protocol payloads, ICMP type3/code3 = closed,
|
||||
\\ silent ports reported honestly as open|filtered
|
||||
\\ --wait <ms> receive drain window after transmit (default 2000)
|
||||
\\ --json emit NDJSON results to stdout (visuals go to stderr)
|
||||
\\ --color <when> auto | always | never (default auto)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ pub const Cookie = struct {
|
|||
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;
|
||||
}
|
||||
|
||||
pub fn udpSrcPort(self: Cookie, ip_them: u32, port_them: u16, ip_me: u32, base: u16, span: u16) u16 {
|
||||
const s: u32 = if (span == 0) 1 else span;
|
||||
const off: u32 = @intCast(self.generate(ip_them, port_them, ip_me, 0) % s);
|
||||
return @intCast((@as(u32, base) + off) & 0xffff);
|
||||
}
|
||||
};
|
||||
|
||||
const test_key = [16]u8{
|
||||
|
|
@ -72,3 +78,27 @@ test "validateSynAck wraps at the u32 boundary" {
|
|||
const seq_max: u32 = 0xFFFFFFFF;
|
||||
try std.testing.expectEqual(@as(u32, 0), seq_max +% 1);
|
||||
}
|
||||
|
||||
test "udpSrcPort is deterministic and stays inside [base, base+span)" {
|
||||
const c = Cookie.init(test_key);
|
||||
const base: u16 = 40000;
|
||||
const span: u16 = 8192;
|
||||
const a = c.udpSrcPort(0x08080808, 53, 0x0a000001, base, span);
|
||||
const b = c.udpSrcPort(0x08080808, 53, 0x0a000001, base, span);
|
||||
try std.testing.expectEqual(a, b);
|
||||
try std.testing.expect(a >= base and a < base + span);
|
||||
}
|
||||
|
||||
test "udpSrcPort excludes port_me from the tuple so RX can recompute it" {
|
||||
const c = Cookie.init(test_key);
|
||||
const full = c.generate(0x08080808, 53, 0x0a000001, 0);
|
||||
const want: u16 = @intCast((@as(u32, 40000) + @as(u32, @intCast(full % 8192))) & 0xffff);
|
||||
try std.testing.expectEqual(want, c.udpSrcPort(0x08080808, 53, 0x0a000001, 40000, 8192));
|
||||
}
|
||||
|
||||
test "udpSrcPort separates distinct targets" {
|
||||
const c = Cookie.init(test_key);
|
||||
const p53 = c.udpSrcPort(0x08080808, 53, 0x0a000001, 40000, 8192);
|
||||
const p123 = c.udpSrcPort(0x08080808, 123, 0x0a000001, 40000, 8192);
|
||||
try std.testing.expect(p53 != p123);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,10 +340,10 @@ pub const Dashboard = struct {
|
|||
}
|
||||
};
|
||||
|
||||
pub fn emitJson(out: *std.Io.Writer, r: Result) !void {
|
||||
pub fn emitJson(out: *std.Io.Writer, r: Result, proto: []const u8) !void {
|
||||
try out.print("{{\"ip\":\"", .{});
|
||||
try writeIp(out, r.ip);
|
||||
try out.print("\",\"port\":{d},\"proto\":\"tcp\",\"state\":\"{s}\"}}\n", .{ r.port, stateName(r.state) });
|
||||
try out.print("\",\"port\":{d},\"proto\":\"{s}\",\"state\":\"{s}\"}}\n", .{ r.port, proto, stateName(r.state) });
|
||||
}
|
||||
|
||||
const w_host: usize = 17;
|
||||
|
|
@ -440,6 +440,7 @@ pub fn renderSummary(
|
|||
out: *std.Io.Writer,
|
||||
level: ColorLevel,
|
||||
sent: u64,
|
||||
probe: []const u8,
|
||||
ifname: []const u8,
|
||||
elapsed_s: f64,
|
||||
open: u64,
|
||||
|
|
@ -452,7 +453,9 @@ pub fn renderSummary(
|
|||
try span(out, level, chrome_gray, "sent ");
|
||||
try setFg(out, level, bright_white);
|
||||
try writeThousands(out, sent);
|
||||
try span(out, level, chrome_gray, " SYN on ");
|
||||
try setFg(out, level, chrome_gray);
|
||||
try out.print(" {s} on ", .{probe});
|
||||
try resetFg(out, level);
|
||||
try setFg(out, level, bright_white);
|
||||
try out.writeAll(ifname);
|
||||
try span(out, level, chrome_gray, " in ");
|
||||
|
|
@ -472,6 +475,26 @@ pub fn renderSummary(
|
|||
try out.writeByte('\n');
|
||||
}
|
||||
|
||||
pub fn renderUnanswered(out: *std.Io.Writer, level: ColorLevel, count: u64) !void {
|
||||
try out.writeAll(" ");
|
||||
try span(out, level, violet_mid, gutter_bar);
|
||||
try out.writeByte(' ');
|
||||
try span(out, level, chrome_gray, "open|filtered (no response) ");
|
||||
try setFg(out, level, soft_amber);
|
||||
try writeThousands(out, count);
|
||||
try resetFg(out, level);
|
||||
try out.writeByte('\n');
|
||||
}
|
||||
|
||||
test "renderUnanswered reports the silent-port count in plain mode" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var w = std.Io.Writer.fixed(&buf);
|
||||
try renderUnanswered(&w, .none, 4094);
|
||||
const text = buf[0..w.end];
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "open|filtered (no response)") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "4,094") != null);
|
||||
}
|
||||
|
||||
test "resolveLevel honors choice, tty state, and truecolor" {
|
||||
try std.testing.expectEqual(ColorLevel.none, resolveLevel(.never, true, true));
|
||||
try std.testing.expectEqual(ColorLevel.truecolor, resolveLevel(.always, false, true));
|
||||
|
|
@ -518,14 +541,20 @@ test "Stats.record tallies per-state and total found without cross-talk" {
|
|||
try std.testing.expectEqual(@as(u64, 0), s.sent.v.load(.monotonic));
|
||||
}
|
||||
|
||||
test "emitJson writes one greppable NDJSON object per result" {
|
||||
test "emitJson writes one greppable NDJSON object per result with its proto" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var w = std.Io.Writer.fixed(&buf);
|
||||
try emitJson(&w, .{ .ip = 0x0a000005, .port = 80, .state = .open });
|
||||
try emitJson(&w, .{ .ip = 0x0a000005, .port = 80, .state = .open }, "tcp");
|
||||
try std.testing.expectEqualStrings(
|
||||
"{\"ip\":\"10.0.0.5\",\"port\":80,\"proto\":\"tcp\",\"state\":\"open\"}\n",
|
||||
buf[0..w.end],
|
||||
);
|
||||
w = std.Io.Writer.fixed(&buf);
|
||||
try emitJson(&w, .{ .ip = 0x08080808, .port = 53, .state = .closed }, "udp");
|
||||
try std.testing.expectEqualStrings(
|
||||
"{\"ip\":\"8.8.8.8\",\"port\":53,\"proto\":\"udp\",\"state\":\"closed\"}\n",
|
||||
buf[0..w.end],
|
||||
);
|
||||
}
|
||||
|
||||
test "writeThousands groups digits and leaves small numbers intact" {
|
||||
|
|
|
|||
|
|
@ -35,10 +35,18 @@ pub const TcpHdr = extern struct {
|
|||
urgent: u16,
|
||||
};
|
||||
|
||||
pub const UdpHdr = extern struct {
|
||||
src_port: u16,
|
||||
dst_port: u16,
|
||||
length: u16,
|
||||
checksum: u16,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn checksum(bytes: []const u8) u16 {
|
||||
|
|
@ -118,10 +126,38 @@ pub fn tcpChecksum(src_be: u32, dst_be: u32, segment: []const u8) u16 {
|
|||
return ~@as(u16, @truncate(sum));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)" {
|
||||
|
|
@ -193,3 +229,27 @@ test "tcpChecksum self-verifies: a segment with its correct checksum folds to 0"
|
|||
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 "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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
// ©AngelaMos | 2026
|
||||
// payloads.zig
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Entry = struct { port: u16, payload: []const u8 };
|
||||
|
||||
const ntp_client = [_]u8{0x1b} ++ [_]u8{0} ** 47;
|
||||
|
||||
const dns_version_bind = [_]u8{
|
||||
0x13, 0x37,
|
||||
0x01, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x07, 'v', 'e',
|
||||
'r', 's', 'i',
|
||||
'o', 'n', 0x04,
|
||||
'b', 'i', 'n',
|
||||
'd', 0x00, 0x00,
|
||||
0x10, 0x00, 0x03,
|
||||
};
|
||||
|
||||
const snmp_get_public = [_]u8{
|
||||
0x30, 0x26,
|
||||
0x02, 0x01, 0x00,
|
||||
0x04, 0x06, 'p', 'u', 'b', 'l', 'i', 'c',
|
||||
0xa0, 0x19,
|
||||
0x02, 0x01, 0x00,
|
||||
0x02, 0x01, 0x00,
|
||||
0x02, 0x01, 0x00,
|
||||
0x30, 0x0e,
|
||||
0x30, 0x0c,
|
||||
0x06, 0x08, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00,
|
||||
0x05, 0x00,
|
||||
};
|
||||
|
||||
const netbios_node_status = [_]u8{
|
||||
0x13, 0x37,
|
||||
0x00, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x20, 'C', 'K',
|
||||
} ++ ([_]u8{'A'} ** 30) ++ [_]u8{
|
||||
0x00,
|
||||
0x00, 0x21,
|
||||
0x00, 0x01,
|
||||
};
|
||||
|
||||
const ssdp_msearch =
|
||||
"M-SEARCH * HTTP/1.1\r\n" ++
|
||||
"HOST:239.255.255.250:1900\r\n" ++
|
||||
"MAN:\"ssdp:discover\"\r\n" ++
|
||||
"MX:1\r\n" ++
|
||||
"ST:ssdp:all\r\n" ++
|
||||
"\r\n";
|
||||
|
||||
const mdns_services = [_]u8{
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00,
|
||||
0x09, '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's',
|
||||
0x07, '_', 'd', 'n', 's', '-', 's', 'd',
|
||||
0x04, '_', 'u', 'd', 'p',
|
||||
0x05, 'l', 'o', 'c', 'a', 'l',
|
||||
0x00,
|
||||
0x00, 0x0c,
|
||||
0x00, 0x01,
|
||||
};
|
||||
|
||||
const table = [_]Entry{
|
||||
.{ .port = 53, .payload = &dns_version_bind },
|
||||
.{ .port = 123, .payload = &ntp_client },
|
||||
.{ .port = 137, .payload = &netbios_node_status },
|
||||
.{ .port = 161, .payload = &snmp_get_public },
|
||||
.{ .port = 1900, .payload = ssdp_msearch },
|
||||
.{ .port = 5353, .payload = &mdns_services },
|
||||
};
|
||||
|
||||
pub fn lookup(port: u16) []const u8 {
|
||||
inline for (table) |e| {
|
||||
if (e.port == port) return e.payload;
|
||||
}
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub const max_len: usize = blk: {
|
||||
var m: usize = 0;
|
||||
for (table) |e| {
|
||||
if (e.payload.len > m) m = e.payload.len;
|
||||
}
|
||||
break :blk m;
|
||||
};
|
||||
|
||||
test "lookup returns the protocol payload for a known port and empty for the rest" {
|
||||
try std.testing.expectEqual(@as(usize, 48), lookup(123).len);
|
||||
try std.testing.expectEqualSlices(u8, &ntp_client, lookup(123));
|
||||
try std.testing.expectEqual(@as(usize, 0), lookup(80).len);
|
||||
try std.testing.expectEqual(@as(usize, 0), lookup(0).len);
|
||||
}
|
||||
|
||||
test "NTP client probe is a 48-byte LI=0 VN=3 Mode=3 request" {
|
||||
const p = lookup(123);
|
||||
try std.testing.expectEqual(@as(usize, 48), p.len);
|
||||
try std.testing.expectEqual(@as(u8, 0x1b), p[0]);
|
||||
}
|
||||
|
||||
test "DNS probe is a well-formed single-question version.bind CH TXT query" {
|
||||
const p = lookup(53);
|
||||
try std.testing.expectEqual(@as(u16, 1), std.mem.readInt(u16, p[4..6], .big));
|
||||
var name: [16]u8 = undefined;
|
||||
var w: usize = 0;
|
||||
var i: usize = 12;
|
||||
while (p[i] != 0) {
|
||||
const len = p[i];
|
||||
i += 1;
|
||||
if (w != 0) {
|
||||
name[w] = '.';
|
||||
w += 1;
|
||||
}
|
||||
@memcpy(name[w .. w + len], p[i .. i + len]);
|
||||
w += len;
|
||||
i += len;
|
||||
}
|
||||
try std.testing.expectEqualStrings("version.bind", name[0..w]);
|
||||
try std.testing.expectEqual(@as(u16, 0x0010), std.mem.readInt(u16, p[i + 1 ..][0..2], .big));
|
||||
try std.testing.expectEqual(@as(u16, 0x0003), std.mem.readInt(u16, p[i + 3 ..][0..2], .big));
|
||||
}
|
||||
|
||||
test "SNMP probe is a v1 GetRequest for community public with a matching outer length" {
|
||||
const p = lookup(161);
|
||||
try std.testing.expectEqual(@as(u8, 0x30), p[0]);
|
||||
try std.testing.expectEqual(@as(usize, p[1]) + 2, p.len);
|
||||
try std.testing.expect(std.mem.indexOf(u8, p, "public") != null);
|
||||
try std.testing.expect(std.mem.indexOfScalar(u8, p, 0xa0) != null);
|
||||
}
|
||||
|
||||
test "NetBIOS probe carries the encoded wildcard name and NBSTAT type" {
|
||||
const p = lookup(137);
|
||||
try std.testing.expectEqual(@as(usize, 50), p.len);
|
||||
try std.testing.expect(std.mem.indexOf(u8, p, "CKAAAA") != null);
|
||||
try std.testing.expectEqual(@as(u16, 0x0021), std.mem.readInt(u16, p[p.len - 4 ..][0..2], .big));
|
||||
}
|
||||
|
||||
test "SSDP probe is an M-SEARCH discover" {
|
||||
const p = lookup(1900);
|
||||
try std.testing.expect(std.mem.startsWith(u8, p, "M-SEARCH * HTTP/1.1"));
|
||||
try std.testing.expect(std.mem.indexOf(u8, p, "ssdp:discover") != null);
|
||||
}
|
||||
|
||||
test "mDNS probe enumerates DNS-SD services over PTR" {
|
||||
const p = lookup(5353);
|
||||
try std.testing.expect(std.mem.indexOf(u8, p, "_services") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, p, "_dns-sd") != null);
|
||||
try std.testing.expectEqual(@as(u16, 0x000c), std.mem.readInt(u16, p[p.len - 4 ..][0..2], .big));
|
||||
}
|
||||
|
||||
test "max_len equals the longest table payload" {
|
||||
var m: usize = 0;
|
||||
for (table) |e| {
|
||||
if (e.payload.len > m) m = e.payload.len;
|
||||
}
|
||||
try std.testing.expectEqual(m, max_len);
|
||||
try std.testing.expect(max_len >= 48);
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ const cookie = @import("cookie");
|
|||
|
||||
pub const Result = classify.Result;
|
||||
pub const State = classify.State;
|
||||
pub const TcpClassifier = classify.TcpClassifier;
|
||||
pub const UdpClassifier = classify.UdpClassifier;
|
||||
|
||||
const RECV_BUF_LEN: usize = 2048;
|
||||
const PORT_BITS: u6 = 16;
|
||||
|
|
@ -18,10 +20,10 @@ pub fn resultKey(r: Result) u64 {
|
|||
return (@as(u64, r.ip) << PORT_BITS) | r.port;
|
||||
}
|
||||
|
||||
pub fn run(source: anytype, ck: cookie.Cookie, dd: *dedup.Dedup, sink: anytype) void {
|
||||
pub fn run(source: anytype, clf: anytype, dd: *dedup.Dedup, sink: anytype) void {
|
||||
var buf: [RECV_BUF_LEN]u8 = undefined;
|
||||
while (source.recv(&buf)) |n| {
|
||||
if (classify.classify(buf[0..n], ck)) |r| {
|
||||
if (clf.match(buf[0..n])) |r| {
|
||||
if (dd.insert(resultKey(r))) sink.emit(r);
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +215,7 @@ test "engine classifies, dedups, and emits each found host once" {
|
|||
|
||||
var src = FakeSource{ .frames = &frames };
|
||||
var sink = CollectSink{ .list = &list, .allocator = std.testing.allocator };
|
||||
run(&src, ck, &dd, &sink);
|
||||
run(&src, classify.TcpClassifier{ .ck = ck }, &dd, &sink);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), list.items.len);
|
||||
try std.testing.expectEqual(State.open, list.items[0].state);
|
||||
|
|
@ -258,7 +260,7 @@ test "Io.Queue hands the deduped set from a producer to a consumer fiber" {
|
|||
|
||||
var src = FakeSource{ .frames = &frames };
|
||||
var sink = QueueSink{ .queue = &queue, .io = io };
|
||||
run(&src, ck, &dd, &sink);
|
||||
run(&src, classify.TcpClassifier{ .ck = ck }, &dd, &sink);
|
||||
queue.close(io);
|
||||
consumer.await(io);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
const std = @import("std");
|
||||
const targets = @import("targets");
|
||||
const template = @import("template");
|
||||
const udp = @import("udp");
|
||||
const ratelimit = @import("ratelimit");
|
||||
const afpacket = @import("afpacket");
|
||||
const cookie = @import("cookie");
|
||||
|
|
@ -16,10 +17,12 @@ const output = @import("output");
|
|||
const default_iface = "lo";
|
||||
const default_rate: u64 = 10_000;
|
||||
const default_src_port: u16 = 40_000;
|
||||
const default_udp_src_span: u16 = 8_192;
|
||||
const default_wait_ms: i32 = 2_000;
|
||||
const ns_per_ms: u64 = 1_000_000;
|
||||
const ns_per_sec: u64 = 1_000_000_000;
|
||||
const default_ports = [_]u16{80};
|
||||
const default_tcp_ports = [_]u16{80};
|
||||
const default_udp_ports = [_]u16{ 53, 123, 161 };
|
||||
const dedup_capacity: usize = 1024;
|
||||
const queue_capacity: usize = 2048;
|
||||
const drain_batch: usize = 256;
|
||||
|
|
@ -53,9 +56,9 @@ const TxSink = struct {
|
|||
}
|
||||
};
|
||||
|
||||
fn txWorker(
|
||||
fn txWorkerImpl(
|
||||
engine: *targets.Engine,
|
||||
tmpl: *const template.SynTemplate,
|
||||
tmpl: anytype,
|
||||
bucket: *ratelimit.TokenBucket,
|
||||
sink: *TxSink,
|
||||
max_packets: u64,
|
||||
|
|
@ -69,14 +72,21 @@ fn txWorker(
|
|||
return sent;
|
||||
}
|
||||
|
||||
fn rxWorker(
|
||||
receiver: *rx.Receiver,
|
||||
ck: cookie.Cookie,
|
||||
dd: *dedup.Dedup,
|
||||
sink: *rx.QueueSink,
|
||||
rx_done: *std.atomic.Value(bool),
|
||||
) void {
|
||||
rx.run(receiver, ck, dd, sink);
|
||||
fn txWorkerTcp(engine: *targets.Engine, tmpl: *const template.SynTemplate, bucket: *ratelimit.TokenBucket, sink: *TxSink, max_packets: u64, budget_ns: u64, tx_done: *std.atomic.Value(bool)) u64 {
|
||||
return txWorkerImpl(engine, tmpl, bucket, sink, max_packets, budget_ns, tx_done);
|
||||
}
|
||||
|
||||
fn txWorkerUdp(engine: *targets.Engine, tmpl: *const udp.UdpTemplate, bucket: *ratelimit.TokenBucket, sink: *TxSink, max_packets: u64, budget_ns: u64, tx_done: *std.atomic.Value(bool)) u64 {
|
||||
return txWorkerImpl(engine, tmpl, bucket, sink, max_packets, budget_ns, tx_done);
|
||||
}
|
||||
|
||||
fn rxWorkerTcp(receiver: *rx.Receiver, ck: cookie.Cookie, dd: *dedup.Dedup, sink: *rx.QueueSink, rx_done: *std.atomic.Value(bool)) void {
|
||||
rx.run(receiver, rx.TcpClassifier{ .ck = ck }, dd, sink);
|
||||
rx_done.store(true, .release);
|
||||
}
|
||||
|
||||
fn rxWorkerUdp(receiver: *rx.Receiver, ck: cookie.Cookie, base: u16, span: u16, dd: *dedup.Dedup, sink: *rx.QueueSink, rx_done: *std.atomic.Value(bool)) void {
|
||||
rx.run(receiver, rx.UdpClassifier{ .ck = ck, .base = base, .span = span }, dd, sink);
|
||||
rx_done.store(true, .release);
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +96,12 @@ fn absorb(
|
|||
allocator: std.mem.Allocator,
|
||||
stats: *output.Stats,
|
||||
json_out: ?*std.Io.Writer,
|
||||
proto: []const u8,
|
||||
) void {
|
||||
for (batch) |r| {
|
||||
found.append(allocator, r) catch continue;
|
||||
stats.record(r.state);
|
||||
if (json_out) |w| output.emitJson(w, r) catch {};
|
||||
if (json_out) |w| output.emitJson(w, r, proto) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,11 +113,12 @@ fn drainQueue(
|
|||
allocator: std.mem.Allocator,
|
||||
stats: *output.Stats,
|
||||
json_out: ?*std.Io.Writer,
|
||||
proto: []const u8,
|
||||
) void {
|
||||
while (true) {
|
||||
const n = queue.get(io, buf, 0) catch return;
|
||||
if (n == 0) return;
|
||||
absorb(buf[0..n], found, allocator, stats, json_out);
|
||||
absorb(buf[0..n], found, allocator, stats, json_out, proto);
|
||||
if (n < buf.len) return;
|
||||
}
|
||||
}
|
||||
|
|
@ -138,8 +150,18 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
const src_port = if (netutil.getFlag(args, "--src-port")) |p| try std.fmt.parseInt(u16, p, 10) else default_src_port;
|
||||
const wait_ms = if (netutil.getFlag(args, "--wait")) |w| try std.fmt.parseInt(i32, w, 10) else default_wait_ms;
|
||||
const json = netutil.hasFlag(args, "--json");
|
||||
const is_udp = netutil.hasFlag(args, "--udp");
|
||||
const udp_base: u16 = src_port;
|
||||
const udp_span: u16 = @intCast(@min(@as(u32, default_udp_src_span), 65536 - @as(u32, udp_base)));
|
||||
const proto_json: []const u8 = if (is_udp) "udp" else "tcp";
|
||||
const probe_label: []const u8 = if (is_udp) "UDP" else "SYN";
|
||||
|
||||
const ports = if (netutil.getFlag(args, "--ports")) |p| try netutil.parsePorts(allocator, p) else try allocator.dupe(u16, &default_ports);
|
||||
const ports = if (netutil.getFlag(args, "--ports")) |p|
|
||||
try netutil.parsePorts(allocator, p)
|
||||
else if (is_udp)
|
||||
try allocator.dupe(u16, &default_udp_ports)
|
||||
else
|
||||
try allocator.dupe(u16, &default_tcp_ports);
|
||||
const gw_mac = if (netutil.getFlag(args, "--gw-mac")) |m| try netutil.parseMac(m) else [_]u8{0} ** 6;
|
||||
const src_ip = if (netutil.getFlag(args, "--src-ip")) |s| try netutil.parseIpv4(s) else try netutil.resolveSrcIp(ifname);
|
||||
const src_mac = try netutil.resolveSrcMac(ifname);
|
||||
|
|
@ -167,13 +189,21 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
const dash_total = @min(count, eng.total);
|
||||
|
||||
const ck = try cookie.Cookie.random(io);
|
||||
const tmpl = template.SynTemplate.init(.{
|
||||
const tcp_tmpl = template.SynTemplate.init(.{
|
||||
.src_mac = src_mac,
|
||||
.dst_mac = gw_mac,
|
||||
.src_ip = src_ip,
|
||||
.src_port = src_port,
|
||||
.cookie = ck,
|
||||
});
|
||||
const udp_tmpl = udp.UdpTemplate.init(.{
|
||||
.src_mac = src_mac,
|
||||
.dst_mac = gw_mac,
|
||||
.src_ip = src_ip,
|
||||
.src_port_base = udp_base,
|
||||
.src_port_span = udp_span,
|
||||
.cookie = ck,
|
||||
});
|
||||
var bucket = ratelimit.TokenBucket.init(rate, rate);
|
||||
|
||||
var backend = afpacket.Backend.open(ifname, .{}) catch |err| switch (err) {
|
||||
|
|
@ -218,7 +248,7 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
var stats: output.Stats = .{};
|
||||
const json_out: ?*std.Io.Writer = if (json) out else null;
|
||||
|
||||
try derr.print("zingela target {s} iface {s} rate {d} pps ports {d}\n", .{ target_text, ifname, rate, ports.len });
|
||||
try derr.print("zingela {s} scan target {s} iface {s} rate {d} pps ports {d}\n", .{ probe_label, target_text, ifname, rate, ports.len });
|
||||
try derr.flush();
|
||||
|
||||
var clock = netutil.RealClock{};
|
||||
|
|
@ -227,12 +257,20 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
var tx_sink = TxSink{ .backend = &backend, .sent = &stats.sent.v };
|
||||
var rx_sink = rx.QueueSink{ .queue = &queue, .io = io };
|
||||
|
||||
var tx_fut = io.concurrent(txWorker, .{ &eng, &tmpl, &bucket, &tx_sink, count, tx_budget_ns, &tx_done }) catch {
|
||||
const tx_res = if (is_udp)
|
||||
io.concurrent(txWorkerUdp, .{ &eng, &udp_tmpl, &bucket, &tx_sink, count, tx_budget_ns, &tx_done })
|
||||
else
|
||||
io.concurrent(txWorkerTcp, .{ &eng, &tcp_tmpl, &bucket, &tx_sink, count, tx_budget_ns, &tx_done });
|
||||
var tx_fut = tx_res catch {
|
||||
try derr.writeAll(concurrency_hint);
|
||||
try derr.flush();
|
||||
return;
|
||||
};
|
||||
var rx_fut = io.concurrent(rxWorker, .{ &receiver, ck, &dd, &rx_sink, &rx_done }) catch {
|
||||
const rx_res = if (is_udp)
|
||||
io.concurrent(rxWorkerUdp, .{ &receiver, ck, udp_base, udp_span, &dd, &rx_sink, &rx_done })
|
||||
else
|
||||
io.concurrent(rxWorkerTcp, .{ &receiver, ck, &dd, &rx_sink, &rx_done });
|
||||
var rx_fut = rx_res catch {
|
||||
_ = tx_fut.await(io);
|
||||
try derr.writeAll(concurrency_hint);
|
||||
try derr.flush();
|
||||
|
|
@ -245,7 +283,7 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
var last_render: u64 = 0;
|
||||
|
||||
while (!(tx_done.load(.acquire) and rx_done.load(.acquire))) {
|
||||
drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out);
|
||||
drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out, proto_json);
|
||||
if (json) out.flush() catch {};
|
||||
const now = clock.now();
|
||||
if (last_render == 0 or now -| last_render >= render_interval_ns) {
|
||||
|
|
@ -258,7 +296,7 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
const sent = tx_fut.await(io);
|
||||
rx_fut.await(io);
|
||||
queue.close(io);
|
||||
drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out);
|
||||
drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out, proto_json);
|
||||
if (json) out.flush() catch {};
|
||||
|
||||
dash.render(derr, &stats, clock.now() -| t0) catch {};
|
||||
|
|
@ -285,6 +323,10 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, e
|
|||
|
||||
const elapsed_s = @as(f64, @floatFromInt(clock.now() - t0)) / @as(f64, @floatFromInt(ns_per_sec));
|
||||
try derr.writeByte('\n');
|
||||
try output.renderSummary(derr, err_level, sent, ifname, elapsed_s, open_n, closed_n, filtered_n);
|
||||
try output.renderSummary(derr, err_level, sent, probe_label, ifname, elapsed_s, open_n, closed_n, filtered_n);
|
||||
if (is_udp) {
|
||||
const answered = open_n + closed_n + filtered_n;
|
||||
try output.renderUnanswered(derr, err_level, sent -| answered);
|
||||
}
|
||||
try derr.flush();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const default_window: u16 = 1024;
|
|||
|
||||
pub const SynTemplate = struct {
|
||||
pub const frame_len: usize = 54;
|
||||
pub const max_frame_len: usize = frame_len;
|
||||
|
||||
base: [frame_len]u8,
|
||||
src_ip_be: u32,
|
||||
|
|
@ -76,7 +77,7 @@ pub const SynTemplate = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn stamp(self: *const SynTemplate, out: *[frame_len]u8, dst_ip: u32, dst_port: u16) void {
|
||||
pub fn stamp(self: *const SynTemplate, out: *[frame_len]u8, dst_ip: u32, dst_port: u16) usize {
|
||||
@memcpy(out, &self.base);
|
||||
|
||||
std.mem.writeInt(u32, out[30..34], dst_ip, .big);
|
||||
|
|
@ -92,6 +93,7 @@ pub const SynTemplate = struct {
|
|||
const dst_be = std.mem.nativeToBig(u32, dst_ip);
|
||||
const tcp_ck = packet.tcpChecksum(self.src_ip_be, dst_be, out[34..54]);
|
||||
std.mem.writeInt(u16, out[50..52], tcp_ck, .big);
|
||||
return frame_len;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -110,7 +112,7 @@ test "stamped frame is 54 bytes with self-verifying IP and TCP checksums" {
|
|||
.cookie = ck,
|
||||
});
|
||||
var frame: [SynTemplate.frame_len]u8 = undefined;
|
||||
tmpl.stamp(&frame, 0x08080808, 443);
|
||||
_ = tmpl.stamp(&frame, 0x08080808, 443);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 54), frame.len);
|
||||
try std.testing.expectEqual(@as(u16, 0), packet.checksum(frame[14..34]));
|
||||
|
|
@ -129,7 +131,7 @@ test "stamp writes the destination and the SipHash seq" {
|
|||
.cookie = ck,
|
||||
});
|
||||
var frame: [SynTemplate.frame_len]u8 = undefined;
|
||||
tmpl.stamp(&frame, 0x08080808, 443);
|
||||
_ = tmpl.stamp(&frame, 0x08080808, 443);
|
||||
|
||||
try std.testing.expectEqual(@as(u32, 0x08080808), std.mem.readInt(u32, frame[30..34], .big));
|
||||
try std.testing.expectEqual(@as(u16, 443), std.mem.readInt(u16, frame[36..38], .big));
|
||||
|
|
@ -148,7 +150,7 @@ test "two different targets produce two different seqs" {
|
|||
});
|
||||
var a: [SynTemplate.frame_len]u8 = undefined;
|
||||
var b: [SynTemplate.frame_len]u8 = undefined;
|
||||
tmpl.stamp(&a, 0x08080808, 443);
|
||||
tmpl.stamp(&b, 0x08080808, 80);
|
||||
_ = tmpl.stamp(&a, 0x08080808, 443);
|
||||
_ = tmpl.stamp(&b, 0x08080808, 80);
|
||||
try std.testing.expect(!std.mem.eql(u8, a[38..42], b[38..42]));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const packet = @import("packet");
|
|||
|
||||
pub fn run(
|
||||
engine: *targets.Engine,
|
||||
tmpl: *const template.SynTemplate,
|
||||
tmpl: anytype,
|
||||
bucket: *ratelimit.TokenBucket,
|
||||
sink: anytype,
|
||||
clock: anytype,
|
||||
|
|
@ -19,7 +19,7 @@ pub fn run(
|
|||
) u64 {
|
||||
_ = bucket.takeBatch(clock.now(), 0);
|
||||
var sent: u64 = 0;
|
||||
var frame: [template.SynTemplate.frame_len]u8 = undefined;
|
||||
var frame: [@TypeOf(tmpl.*).max_frame_len]u8 = undefined;
|
||||
|
||||
var pending: ?targets.Target = engine.next();
|
||||
while (pending != null and sent < max_packets) {
|
||||
|
|
@ -33,11 +33,11 @@ pub fn run(
|
|||
var n: u64 = 0;
|
||||
while (n < granted) : (n += 1) {
|
||||
const t = pending orelse break;
|
||||
tmpl.stamp(&frame, t.ip, t.port);
|
||||
if (!sink.submit(&frame)) {
|
||||
const len = tmpl.stamp(&frame, t.ip, t.port);
|
||||
if (!sink.submit(frame[0..len])) {
|
||||
@branchHint(.unlikely);
|
||||
sink.kick();
|
||||
if (!sink.submit(&frame)) break;
|
||||
if (!sink.submit(frame[0..len])) break;
|
||||
}
|
||||
sent += 1;
|
||||
pending = engine.next();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
// ©AngelaMos | 2026
|
||||
// udp.zig
|
||||
|
||||
const std = @import("std");
|
||||
const packet = @import("packet");
|
||||
const cookie = @import("cookie");
|
||||
const payloads = @import("payloads");
|
||||
|
||||
const ethertype_ipv4: u16 = 0x0800;
|
||||
const ipv4_version_ihl: u8 = 0x45;
|
||||
const ip_proto_udp: u8 = 17;
|
||||
const ip_flag_dont_fragment: u16 = 0x4000;
|
||||
const default_ttl: u8 = 64;
|
||||
|
||||
const eth_len: usize = 14;
|
||||
const ip_len: usize = 20;
|
||||
const udp_len: usize = 8;
|
||||
const l4_off: usize = eth_len + ip_len;
|
||||
const payload_off: usize = l4_off + udp_len;
|
||||
|
||||
const ip_total_len_off: usize = eth_len + 2;
|
||||
const ip_checksum_off: usize = eth_len + 10;
|
||||
const ip_dst_off: usize = eth_len + 16;
|
||||
const udp_src_off: usize = l4_off;
|
||||
const udp_dst_off: usize = l4_off + 2;
|
||||
const udp_len_off: usize = l4_off + 4;
|
||||
const udp_checksum_off: usize = l4_off + 6;
|
||||
|
||||
pub const UdpTemplate = struct {
|
||||
pub const max_frame_len: usize = payload_off + payloads.max_len;
|
||||
|
||||
base: [payload_off]u8,
|
||||
src_ip_be: u32,
|
||||
src_ip: u32,
|
||||
src_port_base: u16,
|
||||
src_port_span: u16,
|
||||
cookie: cookie.Cookie,
|
||||
|
||||
pub const Config = struct {
|
||||
src_mac: [6]u8,
|
||||
dst_mac: [6]u8,
|
||||
src_ip: u32,
|
||||
src_port_base: u16,
|
||||
src_port_span: u16,
|
||||
cookie: cookie.Cookie,
|
||||
};
|
||||
|
||||
pub fn init(cfg: Config) UdpTemplate {
|
||||
var base: [payload_off]u8 = undefined;
|
||||
|
||||
const eth = packet.EthHdr{
|
||||
.dst = cfg.dst_mac,
|
||||
.src = cfg.src_mac,
|
||||
.ethertype = std.mem.nativeToBig(u16, ethertype_ipv4),
|
||||
};
|
||||
@memcpy(base[0..eth_len], std.mem.asBytes(ð));
|
||||
|
||||
const ip = packet.Ipv4Hdr{
|
||||
.version_ihl = ipv4_version_ihl,
|
||||
.tos = 0,
|
||||
.total_len = 0,
|
||||
.id = 0,
|
||||
.flags_frag = std.mem.nativeToBig(u16, ip_flag_dont_fragment),
|
||||
.ttl = default_ttl,
|
||||
.protocol = ip_proto_udp,
|
||||
.checksum = 0,
|
||||
.src = std.mem.nativeToBig(u32, cfg.src_ip),
|
||||
.dst = 0,
|
||||
};
|
||||
@memcpy(base[eth_len..l4_off], std.mem.asBytes(&ip));
|
||||
|
||||
const udp = packet.UdpHdr{
|
||||
.src_port = 0,
|
||||
.dst_port = 0,
|
||||
.length = 0,
|
||||
.checksum = 0,
|
||||
};
|
||||
@memcpy(base[l4_off..payload_off], std.mem.asBytes(&udp));
|
||||
|
||||
return .{
|
||||
.base = base,
|
||||
.src_ip_be = std.mem.nativeToBig(u32, cfg.src_ip),
|
||||
.src_ip = cfg.src_ip,
|
||||
.src_port_base = cfg.src_port_base,
|
||||
.src_port_span = cfg.src_port_span,
|
||||
.cookie = cfg.cookie,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn stamp(self: *const UdpTemplate, out: *[max_frame_len]u8, dst_ip: u32, dst_port: u16) usize {
|
||||
const payload = payloads.lookup(dst_port);
|
||||
const udp_total = udp_len + payload.len;
|
||||
const ip_total = ip_len + udp_total;
|
||||
const frame_len = eth_len + ip_total;
|
||||
|
||||
@memcpy(out[0..payload_off], &self.base);
|
||||
@memcpy(out[payload_off .. payload_off + payload.len], payload);
|
||||
|
||||
std.mem.writeInt(u16, out[ip_total_len_off..][0..2], @intCast(ip_total), .big);
|
||||
std.mem.writeInt(u32, out[ip_dst_off..][0..4], dst_ip, .big);
|
||||
std.mem.writeInt(u16, out[ip_checksum_off..][0..2], 0, .big);
|
||||
const ip_ck = packet.checksum(out[eth_len..l4_off]);
|
||||
std.mem.writeInt(u16, out[ip_checksum_off..][0..2], ip_ck, .big);
|
||||
|
||||
const src_port = self.cookie.udpSrcPort(dst_ip, dst_port, self.src_ip, self.src_port_base, self.src_port_span);
|
||||
std.mem.writeInt(u16, out[udp_src_off..][0..2], src_port, .big);
|
||||
std.mem.writeInt(u16, out[udp_dst_off..][0..2], dst_port, .big);
|
||||
std.mem.writeInt(u16, out[udp_len_off..][0..2], @intCast(udp_total), .big);
|
||||
std.mem.writeInt(u16, out[udp_checksum_off..][0..2], 0, .big);
|
||||
const dst_be = std.mem.nativeToBig(u32, dst_ip);
|
||||
const udp_ck = packet.udpChecksum(self.src_ip_be, dst_be, out[l4_off .. payload_off + payload.len]);
|
||||
std.mem.writeInt(u16, out[udp_checksum_off..][0..2], udp_ck, .big);
|
||||
|
||||
return frame_len;
|
||||
}
|
||||
};
|
||||
|
||||
const test_key = [16]u8{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
};
|
||||
|
||||
const our_ip: u32 = 0x0a000001;
|
||||
const their_ip: u32 = 0x08080808;
|
||||
const base_port: u16 = 40000;
|
||||
const span: u16 = 8192;
|
||||
|
||||
fn testTemplate(ck: cookie.Cookie) UdpTemplate {
|
||||
return UdpTemplate.init(.{
|
||||
.src_mac = .{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01 },
|
||||
.dst_mac = .{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 },
|
||||
.src_ip = our_ip,
|
||||
.src_port_base = base_port,
|
||||
.src_port_span = span,
|
||||
.cookie = ck,
|
||||
});
|
||||
}
|
||||
|
||||
test "stamped UDP frame carries self-verifying IP and UDP checksums" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const tmpl = testTemplate(ck);
|
||||
var frame: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
const len = tmpl.stamp(&frame, their_ip, 53);
|
||||
|
||||
try std.testing.expectEqual(@as(u16, 0), packet.checksum(frame[eth_len..l4_off]));
|
||||
const src_be = std.mem.nativeToBig(u32, our_ip);
|
||||
const dst_be = std.mem.nativeToBig(u32, their_ip);
|
||||
try std.testing.expectEqual(@as(u16, 0xffff), packet.udpChecksum(src_be, dst_be, frame[l4_off..len]));
|
||||
try std.testing.expect(std.mem.readInt(u16, frame[udp_checksum_off..][0..2], .big) != 0);
|
||||
}
|
||||
|
||||
test "stamp selects the per-protocol payload and computes the frame length" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const tmpl = testTemplate(ck);
|
||||
var frame: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
|
||||
const known = payloads.lookup(53);
|
||||
const len53 = tmpl.stamp(&frame, their_ip, 53);
|
||||
try std.testing.expectEqual(payload_off + known.len, len53);
|
||||
try std.testing.expectEqualSlices(u8, known, frame[payload_off..len53]);
|
||||
try std.testing.expectEqual(@as(u16, @intCast(udp_len + known.len)), std.mem.readInt(u16, frame[udp_len_off..][0..2], .big));
|
||||
|
||||
const len80 = tmpl.stamp(&frame, their_ip, 80);
|
||||
try std.testing.expectEqual(payload_off, len80);
|
||||
}
|
||||
|
||||
test "stamp writes the cookie-derived UDP source port" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const tmpl = testTemplate(ck);
|
||||
var frame: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
_ = tmpl.stamp(&frame, their_ip, 53);
|
||||
const want = ck.udpSrcPort(their_ip, 53, our_ip, base_port, span);
|
||||
try std.testing.expectEqual(want, std.mem.readInt(u16, frame[udp_src_off..][0..2], .big));
|
||||
try std.testing.expect(want >= base_port and want < base_port + span);
|
||||
}
|
||||
|
||||
test "stamp writes destination, port, and the UDP protocol number" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const tmpl = testTemplate(ck);
|
||||
var frame: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
_ = tmpl.stamp(&frame, their_ip, 161);
|
||||
try std.testing.expectEqual(their_ip, std.mem.readInt(u32, frame[ip_dst_off..][0..4], .big));
|
||||
try std.testing.expectEqual(@as(u16, 161), std.mem.readInt(u16, frame[udp_dst_off..][0..2], .big));
|
||||
try std.testing.expectEqual(ip_proto_udp, frame[eth_len + 9]);
|
||||
}
|
||||
|
||||
test "two different target ports produce two different source ports" {
|
||||
const ck = cookie.Cookie.init(test_key);
|
||||
const tmpl = testTemplate(ck);
|
||||
var a: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
var b: [UdpTemplate.max_frame_len]u8 = undefined;
|
||||
_ = tmpl.stamp(&a, their_ip, 53);
|
||||
_ = tmpl.stamp(&b, their_ip, 123);
|
||||
try std.testing.expect(!std.mem.eql(u8, a[udp_src_off..][0..2], b[udp_src_off..][0..2]));
|
||||
}
|
||||
Loading…
Reference in New Issue