Cybersecurity-Projects/PROJECTS/advanced/zig-stateless-scanner/src/tx.zig

471 lines
16 KiB
Zig

// ©AngelaMos | 2026
// tx.zig
const std = @import("std");
const targets = @import("targets");
const template = @import("template");
const ratelimit = @import("ratelimit");
const cookie = @import("cookie");
const packet = @import("packet");
fn submitFrame(sink: anytype, frame: []const u8) bool {
if (!sink.submit(frame)) {
@branchHint(.unlikely);
sink.kick();
return sink.submit(frame);
}
return true;
}
pub fn run(
engine: *targets.Engine,
tmpl: anytype,
bucket: *ratelimit.TokenBucket,
sink: anytype,
clock: anytype,
max_packets: u64,
deadline_ns: u64,
) u64 {
if (bucket.jitter != null) return runJittered(engine, tmpl, bucket, sink, clock, max_packets, deadline_ns);
bucket.prime(clock.now());
var sent: u64 = 0;
var probes: u64 = 0;
var cursor: usize = 0;
var frame: [@TypeOf(tmpl.*).max_frame_len]u8 = undefined;
const vc = tmpl.variantCount();
var pending: ?targets.Target = engine.next();
while (pending != null and probes < max_packets) {
const now_ns = clock.now();
if (now_ns >= deadline_ns) break;
const granted = bucket.takeBatch(now_ns, (max_packets - probes) *| vc);
if (granted == 0) {
clock.sleepNs(bucket.step_ns);
continue;
}
var used: u64 = 0;
while (used < granted and probes < max_packets) {
const t = pending orelse break;
const len = tmpl.stampVariant(&frame, t.ip, t.port, cursor);
if (!submitFrame(sink, frame[0..len])) break;
used += 1;
sent += 1;
cursor += 1;
if (cursor == vc) {
cursor = 0;
probes += 1;
pending = engine.next();
}
}
if (used < granted) bucket.refund(granted - used);
sink.kick();
}
sink.kick();
return sent;
}
fn runJittered(
engine: *targets.Engine,
tmpl: anytype,
bucket: *ratelimit.TokenBucket,
sink: anytype,
clock: anytype,
max_packets: u64,
deadline_ns: u64,
) u64 {
var sent: u64 = 0;
var probes: u64 = 0;
var cursor: usize = 0;
var frame: [@TypeOf(tmpl.*).max_frame_len]u8 = undefined;
const vc = tmpl.variantCount();
var pending: ?targets.Target = engine.next();
while (pending != null and probes < max_packets) {
if (clock.now() >= deadline_ns) break;
const t = pending.?;
const len = tmpl.stampVariant(&frame, t.ip, t.port, cursor);
if (!submitFrame(sink, frame[0..len])) {
clock.sleepNs(bucket.step_ns);
continue;
}
sent += 1;
cursor += 1;
sink.kick();
if (cursor < vc) continue;
cursor = 0;
probes += 1;
pending = engine.next();
if (pending == null) break;
clock.sleepNs(bucket.jitter.?.nextGapNs() *| vc);
}
sink.kick();
return sent;
}
pub fn runV6(
engine: *targets.Engine6,
tmpl: anytype,
bucket: *ratelimit.TokenBucket,
sink: anytype,
clock: anytype,
max_packets: u64,
deadline_ns: u64,
) u64 {
bucket.prime(clock.now());
var sent: u64 = 0;
var frame: [@TypeOf(tmpl.*).max_frame_len]u8 = undefined;
var pending: ?targets.Target6 = engine.next();
while (pending != null and sent < max_packets) {
const now_ns = clock.now();
if (now_ns >= deadline_ns) break;
const granted = bucket.takeBatch(now_ns, max_packets - sent);
if (granted == 0) {
clock.sleepNs(bucket.step_ns);
continue;
}
var used: u64 = 0;
while (used < granted and sent < max_packets) {
const t = pending orelse break;
const len = tmpl.stamp(&frame, t.addr, t.port);
if (!submitFrame(sink, frame[0..len])) break;
used += 1;
sent += 1;
pending = engine.next();
}
if (used < granted) bucket.refund(granted - used);
sink.kick();
}
sink.kick();
return sent;
}
const FakeClock = struct {
t: u64 = 0,
fn now(self: *FakeClock) u64 {
self.t += 1_000_000_000;
return self.t;
}
fn sleepNs(self: *FakeClock, ns: u64) void {
self.t += ns;
}
};
const FakeSink = struct {
frames: std.ArrayList([54]u8) = .empty,
kicks: usize = 0,
allocator: std.mem.Allocator,
fn submit(self: *FakeSink, frame: []const u8) bool {
self.frames.append(self.allocator, frame[0..54].*) catch return false;
return true;
}
fn kick(self: *FakeSink) void {
self.kicks += 1;
}
};
test "the TX engine drives the M2 bijection through stamp + ratelimit + submit" {
const test_key = [16]u8{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
};
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/30")};
const ports = [_]u16{ 80, 443 };
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 0xDEADBEEF);
defer eng.deinit();
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
});
var tb = ratelimit.TokenBucket.init(1000, 64);
var clock = FakeClock{};
var sink = FakeSink{ .allocator = std.testing.allocator };
defer sink.frames.deinit(std.testing.allocator);
const sent = run(&eng, &tmpl, &tb, &sink, &clock, 1_000_000, std.math.maxInt(u64));
try std.testing.expectEqual(@as(u64, 8), sent);
try std.testing.expectEqual(@as(usize, 8), sink.frames.items.len);
try std.testing.expect(sink.kicks >= 1);
var seen = std.AutoHashMap(u64, void).init(std.testing.allocator);
defer seen.deinit();
for (sink.frames.items) |*f| {
try std.testing.expectEqual(@as(u16, 0), packet.checksum(f[14..34]));
const ip = std.mem.readInt(u32, f[30..34], .big);
const port = std.mem.readInt(u16, f[36..38], .big);
try std.testing.expect(!targets.isReserved(ip));
const key = (@as(u64, ip) << 16) | port;
try std.testing.expect(!seen.contains(key));
try seen.put(key, {});
}
try std.testing.expectEqual(@as(usize, 8), seen.count());
}
test "max_packets caps the send count below the target total" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/28")};
const ports = [_]u16{ 80, 443, 22 };
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 0x1234);
defer eng.deinit();
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
});
var tb = ratelimit.TokenBucket.init(1000, 64);
var clock = FakeClock{};
var sink = FakeSink{ .allocator = std.testing.allocator };
defer sink.frames.deinit(std.testing.allocator);
const sent = run(&eng, &tmpl, &tb, &sink, &clock, 5, std.math.maxInt(u64));
try std.testing.expectEqual(@as(u64, 5), sent);
try std.testing.expectEqual(@as(usize, 5), sink.frames.items.len);
}
const StuckSink = struct {
kicks: usize = 0,
fn submit(_: *StuckSink, _: []const u8) bool {
return false;
}
fn kick(self: *StuckSink) void {
self.kicks += 1;
}
};
test "run bails at the deadline when the sink never drains (stall watchdog)" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/28")};
const ports = [_]u16{80};
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 0x1234);
defer eng.deinit();
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
});
var tb = ratelimit.TokenBucket.init(1000, 64);
var clock = FakeClock{};
var sink = StuckSink{};
const sent = run(&eng, &tmpl, &tb, &sink, &clock, 1_000_000, 5_000_000_000);
try std.testing.expectEqual(@as(u64, 0), sent);
try std.testing.expect(sink.kicks >= 1);
}
test "decoys emit the real probe plus every spoofed source for each target" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/30")};
const ports = [_]u16{80};
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 9);
defer eng.deinit();
const total = eng.total;
const decoys = [_]u32{ 0x01010101, 0x02020202 };
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
.decoys = &decoys,
});
var tb = ratelimit.TokenBucket.init(1000, 1000);
var clock = FakeClock{};
var sink = FakeSink{ .allocator = std.testing.allocator };
defer sink.frames.deinit(std.testing.allocator);
const sent = run(&eng, &tmpl, &tb, &sink, &clock, total, std.math.maxInt(u64));
try std.testing.expectEqual(total * 3, sent);
try std.testing.expectEqual(@as(usize, @intCast(total * 3)), sink.frames.items.len);
var real_count: usize = 0;
for (sink.frames.items) |*f| {
if (std.mem.readInt(u32, f[26..30], .big) == 0x0a000001) real_count += 1;
}
try std.testing.expectEqual(@as(usize, @intCast(total)), real_count);
}
test "jittered pacing covers every target and advances the clock by the sleeps" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/29")};
const ports = [_]u16{80};
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 4);
defer eng.deinit();
const total = eng.total;
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
});
var tb = ratelimit.TokenBucket.init(1000, 1000).withJitter(0xC0FFEE);
var clock = FakeClock{};
var sink = FakeSink{ .allocator = std.testing.allocator };
defer sink.frames.deinit(std.testing.allocator);
const before = clock.t;
const sent = run(&eng, &tmpl, &tb, &sink, &clock, total, std.math.maxInt(u64));
try std.testing.expectEqual(total, sent);
try std.testing.expect(clock.t > before);
}
const SlowClock = struct {
t: u64 = 0,
fn now(self: *SlowClock) u64 {
self.t += 1_000;
return self.t;
}
fn sleepNs(self: *SlowClock, ns: u64) void {
self.t += ns;
}
};
const CountSink = struct {
count: u64 = 0,
fn submit(self: *CountSink, _: []const u8) bool {
self.count += 1;
return true;
}
fn kick(_: *CountSink) void {}
};
test "decoy scans keep making progress past the initial token burst (no livelock)" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.0.0/20")};
const ports = [_]u16{80};
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 11);
defer eng.deinit();
const total = eng.total;
const decoys = [_]u32{ 0x01010101, 0x02020202 };
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
.decoys = &decoys,
});
var tb = ratelimit.TokenBucket.init(1000, 1000);
var clock = SlowClock{};
var sink = CountSink{};
const sent = run(&eng, &tmpl, &tb, &sink, &clock, total, 6_000_000_000);
try std.testing.expectEqual(sink.count, sent);
try std.testing.expect(sent > 3000);
}
const BoundedSink = struct {
frames: std.ArrayList([54]u8) = .empty,
allocator: std.mem.Allocator,
held: usize = 0,
cap: usize,
fn submit(self: *BoundedSink, frame: []const u8) bool {
if (self.held >= self.cap) return false;
self.frames.append(self.allocator, frame[0..54].*) catch return false;
self.held += 1;
return true;
}
fn kick(self: *BoundedSink) void {
self.held = 0;
}
};
const V6Sink = struct {
frames: std.ArrayList([74]u8) = .empty,
allocator: std.mem.Allocator,
fn submit(self: *V6Sink, frame: []const u8) bool {
self.frames.append(self.allocator, frame[0..74].*) catch return false;
return true;
}
fn kick(_: *V6Sink) void {}
};
test "the IPv6 TX engine drives Engine6 through stamp6 with self-verifying checksums and a bijection" {
const test_key = [_]u8{0} ** 16;
const cidr = try targets.parseCidr6("2001:470:1:2::/122");
const ports = [_]u16{ 80, 443 };
var eng = try targets.Engine6.init(std.testing.allocator, cidr, &ports, 0xBEEF, targets.default_max_hosts6);
defer eng.deinit();
const total = eng.total;
const src_ip = try @import("netutil").parseIpv6("2001:470:1:2::1");
const tmpl = template.SynTemplate6.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = src_ip,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
});
var tb = ratelimit.TokenBucket.init(1000, 1000);
var clock = FakeClock{};
var sink = V6Sink{ .allocator = std.testing.allocator };
defer sink.frames.deinit(std.testing.allocator);
const sent = runV6(&eng, &tmpl, &tb, &sink, &clock, total, std.math.maxInt(u64));
try std.testing.expectEqual(total, sent);
try std.testing.expectEqual(@as(usize, @intCast(total)), sink.frames.items.len);
var seen = std.AutoHashMap([18]u8, void).init(std.testing.allocator);
defer seen.deinit();
for (sink.frames.items) |*f| {
try std.testing.expectEqual(@as(u16, 0x86dd), std.mem.readInt(u16, f[12..14], .big));
var dst: [16]u8 = undefined;
@memcpy(&dst, f[38..54]);
try std.testing.expect(!targets.isReserved6(dst));
try std.testing.expectEqual(@as(u16, 0), packet.tcpChecksum6(src_ip, dst, f[54..74]));
var key: [18]u8 = undefined;
@memcpy(key[0..16], &dst);
@memcpy(key[16..18], f[56..58]);
try std.testing.expect(!seen.contains(key));
try seen.put(key, {});
}
try std.testing.expectEqual(@as(usize, @intCast(total)), seen.count());
}
test "decoy groups resume across backpressure without re-sending the real probe" {
const test_key = [_]u8{0} ** 16;
const cidrs = [_]targets.Range{try targets.parseCidr("8.8.8.0/29")};
const ports = [_]u16{80};
var eng = try targets.Engine.init(std.testing.allocator, &cidrs, &ports, 13);
defer eng.deinit();
const total = eng.total;
const decoys = [_]u32{ 0x01010101, 0x02020202 };
const tmpl = template.SynTemplate.init(.{
.src_mac = .{0} ** 6,
.dst_mac = .{0} ** 6,
.src_ip = 0x0a000001,
.src_port = 40000,
.cookie = cookie.Cookie.init(test_key),
.decoys = &decoys,
});
var tb = ratelimit.TokenBucket.init(1000, 1000);
var clock = FakeClock{};
var sink = BoundedSink{ .allocator = std.testing.allocator, .cap = 2 };
defer sink.frames.deinit(std.testing.allocator);
const sent = run(&eng, &tmpl, &tb, &sink, &clock, total, std.math.maxInt(u64));
try std.testing.expectEqual(total * 3, sent);
try std.testing.expectEqual(@as(usize, @intCast(total * 3)), sink.frames.items.len);
var real: usize = 0;
for (sink.frames.items) |*f| {
if (std.mem.readInt(u32, f[26..30], .big) == 0x0a000001) real += 1;
}
try std.testing.expectEqual(@as(usize, @intCast(total)), real);
}