// ©AngelaMos | 2026 // scancmd.zig const std = @import("std"); const targets = @import("targets"); const template = @import("template"); const udp = @import("udp"); const ratelimit = @import("ratelimit"); const packet_io = @import("packet_io"); const cookie = @import("cookie"); const tx = @import("tx"); const rx = @import("rx"); const dedup = @import("dedup"); const netutil = @import("netutil"); const output = @import("output"); const stealth = @import("stealth"); const service = @import("service"); const connect = @import("connect"); const rawprobe = @import("rawprobe"); const ndp = @import("ndp"); 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_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; const finding_queue_capacity: usize = 256; const finding_drain_batch: usize = 64; const drain_tick_ns: u64 = 50 * ns_per_ms; const render_tick_interactive_ns: u64 = 125 * ns_per_ms; const render_tick_plain_ns: u64 = 1_000 * ns_per_ms; const rx_hard_cap_floor_ns: u64 = 60 * ns_per_sec; const min_dashboard_cols: u16 = 64; const need_cap_hint = "scan: need CAP_NET_RAW + CAP_NET_ADMIN. Grant once, then re-run (no sudo):\n" ++ " sudo setcap cap_net_raw,cap_net_admin=eip ./zig-out/bin/zingela\nSkipping.\n"; const concurrency_hint = "scan: this system cannot launch concurrent TX/RX (needs >= 2 worker threads).\n"; const authorized_warning = "scan: stealth/evasion features require explicit authorization.\n" ++ "Re-run with --authorized-scan ONLY against systems you own or are\n" ++ "contractually authorized to test. Unauthorized scanning is a crime\n" ++ "under the CFAA and equivalent statutes worldwide.\n\n" ++ " stealth flags: --os-template --scan-type --jitter --source-port-rotation --decoys --suppress-rst\n\n" ++ stealth.omitted_help ++ "\n"; const TxSink = struct { backend: *packet_io.Backend, sent: *output.Counter, pub fn submit(self: *TxSink, frame: []const u8) bool { if (self.backend.submit(frame)) { _ = self.sent.fetchAdd(1, .monotonic); return true; } return false; } pub fn kick(self: *TxSink) void { self.backend.kick(); } }; const FindingSink = struct { queue: *std.Io.Queue(service.Finding), io: std.Io, fn emitImpl(ctx: *anyopaque, f: service.Finding) void { const self: *FindingSink = @ptrCast(@alignCast(ctx)); self.queue.putOne(self.io, f) catch {}; } const vtable = service.Sink.Vtable{ .emit = emitImpl }; pub fn sink(self: *FindingSink) service.Sink { return .{ .ctx = self, .vtable = &vtable }; } }; fn txWorkerImpl( engine: *targets.Engine, tmpl: anytype, bucket: *ratelimit.TokenBucket, sink: *TxSink, max_packets: u64, budget_ns: u64, tx_done: *std.atomic.Value(bool), ) u64 { var clock = netutil.RealClock{}; const deadline_ns = clock.now() +| budget_ns; const sent = tx.run(engine, tmpl, bucket, sink, &clock, max_packets, deadline_ns); tx_done.store(true, .release); return sent; } 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, clf: rx.TcpClassifier, dd: *dedup.Dedup, sink: *rx.QueueSink, rx_done: *std.atomic.Value(bool)) void { rx.run(receiver, clf, 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); } fn svcWorker( engine: *service.Engine, socket: *service.Socket, sink: service.Sink, tx_done: *std.atomic.Value(bool), drain_window_ns: u64, hard_cap_ns: u64, svc_done: *std.atomic.Value(bool), ) void { service.run(engine, socket, sink, tx_done, drain_window_ns, hard_cap_ns); svc_done.store(true, .release); } fn drainFindings( io: std.Io, queue: *std.Io.Queue(service.Finding), buf: []service.Finding, findings: *std.ArrayList(service.Finding), dd: *dedup.Dedup, allocator: std.mem.Allocator, json_out: ?*std.Io.Writer, ) void { while (true) { const n = queue.get(io, buf, 0) catch return; if (n == 0) return; for (buf[0..n]) |f| { if (!dd.insert(service.connKey(f.ip, f.port))) continue; findings.append(allocator, f) catch continue; if (json_out) |w| output.emitServiceJson(w, .{ .ip = f.ip, .port = f.port, .service = f.info.service, .info = f.info.infoSlice(), .tls = f.info.tls, }) catch {}; } if (n < buf.len) return; } } fn findingLess(_: void, a: service.Finding, b: service.Finding) bool { if (a.ip != b.ip) return a.ip < b.ip; return a.port < b.port; } fn renderServiceTable(out: *std.Io.Writer, level: output.ColorLevel, allocator: std.mem.Allocator, items: []service.Finding) !void { const rows = try allocator.alloc(output.ServiceRow, items.len); for (items, 0..) |*f, i| rows[i] = .{ .ip = f.ip, .port = f.port, .service = f.info.service, .info = f.info.infoSlice(), .tls = f.info.tls, }; try output.renderServices(out, level, rows); } fn absorb( batch: []const rx.Result, found: *std.ArrayList(rx.Result), 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, proto) catch {}; } } fn drainQueue( io: std.Io, queue: *std.Io.Queue(rx.Result), buf: []rx.Result, found: *std.ArrayList(rx.Result), 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, proto); if (n < buf.len) return; } } fn terminalCols(fd: i32) ?u16 { var ws: std.posix.winsize = undefined; const rc = std.os.linux.ioctl(fd, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws)); if (std.os.linux.errno(rc) != .SUCCESS) return null; if (ws.col == 0) return null; return ws.col; } fn txWorkerV6(engine: *targets.Engine6, tmpl: *const template.SynTemplate6, bucket: *ratelimit.TokenBucket, sink: *TxSink, max_packets: u64, budget_ns: u64, tx_done: *std.atomic.Value(bool)) u64 { var clock = netutil.RealClock{}; const deadline_ns = clock.now() +| budget_ns; const sent = tx.runV6(engine, tmpl, bucket, sink, &clock, max_packets, deadline_ns); tx_done.store(true, .release); return sent; } fn rxWorkerV6(receiver: *rx.Receiver, clf: rx.TcpClassifier6, dd: *dedup.Dedup, sink: *rx.QueueSink, rx_done: *std.atomic.Value(bool)) void { rx.run(receiver, clf, dd, sink); rx_done.store(true, .release); } fn resolveV6Gateway(io: std.Io, args: []const []const u8, ifname: []const u8, src_ip6: [16]u8, src_mac: [6]u8, derr: *std.Io.Writer) !?[6]u8 { _ = io; if (netutil.getFlag(args, "--gw-mac")) |m| return try netutil.parseMac(m); const next_hop = if (netutil.getFlag(args, "--gw-ip6")) |g| try netutil.parseIpv6(g) else netutil.defaultGateway6(ifname) orelse { try derr.writeAll("scan: IPv6 raw scan needs a next hop: pass --gw-mac , or --gw-ip6 to resolve it via NDP\n"); try derr.flush(); return null; }; return ndp.resolve(ifname, src_ip6, src_mac, next_hop, 1500) catch |e| { try derr.print("scan: NDP neighbor resolution failed ({s}); pass --gw-mac explicitly\n", .{@errorName(e)}); try derr.flush(); return null; }; } fn runV6Scan(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, env: *std.process.Environ.Map, target_text: []const u8, ifname: []const u8, rate: u64, backend_choice: packet_io.Choice, derr: *std.Io.Writer) !void { var obuf: [4096]u8 = undefined; var ow = std.Io.File.stdout().writer(io, &obuf); const out = &ow.interface; if (netutil.hasFlag(args, "--udp")) { try derr.writeAll("scan: IPv6 UDP scanning is not supported yet; run a TCP SYN scan or --connect\n"); try derr.flush(); return; } if (netutil.getFlag(args, "--scan-type") != null or netutil.getFlag(args, "--os-template") != null or netutil.getFlag(args, "--decoys") != null or netutil.getFlag(args, "--jitter") != null or netutil.hasFlag(args, "--source-port-rotation")) { try derr.writeAll(" note: stealth/scan-type options are not yet wired for IPv6; running a plain SYN scan\n"); } const ports = if (netutil.getFlag(args, "--ports")) |p| try netutil.parsePorts(allocator, p) else try allocator.dupe(u16, &default_tcp_ports); 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 max_hosts = if (netutil.getFlag(args, "--max-hosts")) |m| try std.fmt.parseInt(u64, m, 10) else targets.default_max_hosts6; var seed: u64 = undefined; if (netutil.getFlag(args, "--seed")) |s| { seed = try std.fmt.parseInt(u64, s, 10); } else { var seed_bytes: [8]u8 = undefined; try io.randomSecure(&seed_bytes); seed = std.mem.readInt(u64, &seed_bytes, .little); } const src_ip6 = if (netutil.getFlag(args, "--src-ip6")) |s| try netutil.parseIpv6(s) else netutil.resolveSrcIp6(ifname) catch { try derr.print("scan: no IPv6 source address on '{s}' (assign one, or pass --src-ip6 )\n", .{ifname}); try derr.flush(); return; }; const src_mac = try netutil.resolveSrcMac(ifname); const gw_mac = (try resolveV6Gateway(io, args, ifname, src_ip6, src_mac, derr)) orelse return; const cidr = targets.parseCidr6(target_text) catch |e| { try derr.print("scan: invalid IPv6 target '{s}' ({s})\n", .{ target_text, @errorName(e) }); try derr.flush(); return; }; var eng = targets.Engine6.init(allocator, cidr, ports, seed, max_hosts) catch |e| switch (e) { error.PrefixTooLarge => { try derr.print("scan: IPv6 prefix '{s}' is too large to enumerate (over --max-hosts {d}); narrow the prefix or raise --max-hosts\n", .{ target_text, max_hosts }); try derr.flush(); return; }, else => return e, }; defer eng.deinit(); const count = if (netutil.getFlag(args, "--count")) |c| try std.fmt.parseInt(u64, c, 10) else eng.total; const ck = try cookie.Cookie.random(io); const tmpl = template.SynTemplate6.init(.{ .src_mac = src_mac, .dst_mac = gw_mac, .src_ip = src_ip6, .src_port = src_port, .cookie = ck, }); var bucket = ratelimit.TokenBucket.init(rate, rate); const choice = output.parseColorChoice(netutil.getFlag(args, "--color")); const out_level = output.envLevel(io, std.Io.File.stdout(), env, choice); const err_level = output.envLevel(io, std.Io.File.stderr(), env, choice); const stderr_tty = std.Io.File.stderr().isTty(io) catch false; const wide_enough = if (terminalCols(2)) |c| c >= min_dashboard_cols else true; const interactive = stderr_tty and wide_enough; const dash_total = @min(count, eng.total); const drain_window_ns: u64 = @as(u64, @intCast(@max(wait_ms, 0))) * ns_per_ms; const est_tx_ns: u64 = if (rate > 0) (dash_total / rate) *| ns_per_sec else rx_hard_cap_floor_ns; const tx_budget_ns: u64 = (est_tx_ns *| 4) +| rx_hard_cap_floor_ns; const hard_cap_ns: u64 = tx_budget_ns +| drain_window_ns; var backend = packet_io.select(allocator, ifname, backend_choice, .{}, .{}, derr) catch |err| switch (err) { error.NeedCapNetRaw => { try derr.writeAll(need_cap_hint); try derr.flush(); return; }, error.XdpNotCompiledIn => { try derr.writeAll("scan: --backend xdp needs a build with -Dxdp\n"); try derr.flush(); return; }, else => return err, }; defer backend.close(); try derr.print(" using {s}\n", .{packet_io.kindLabel(backend.kind())}); var tx_done = std.atomic.Value(bool).init(false); var rx_done = std.atomic.Value(bool).init(false); var receiver = rx.Receiver.open(ifname, rx.ETH_P_IPV6, &tx_done, drain_window_ns, hard_cap_ns) catch |err| switch (err) { error.NeedCapNetRaw => { try derr.writeAll(need_cap_hint); try derr.flush(); return; }, else => return err, }; defer receiver.close(); var dd = try dedup.Dedup.init(allocator, dedup_capacity); defer dd.deinit(); var qbuf: [queue_capacity]rx.Result = undefined; var queue = std.Io.Queue(rx.Result).init(&qbuf); var found_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer found_arena.deinit(); const found_alloc = found_arena.allocator(); var found: std.ArrayList(rx.Result) = .empty; var stats: output.Stats = .{}; const json_out: ?*std.Io.Writer = if (json) out else null; try derr.print("zingela SYN scan (IPv6) target {s} iface {s} rate {d} pps ports {d}\n", .{ target_text, ifname, rate, ports.len }); try derr.flush(); var clock = netutil.RealClock{}; const t0 = clock.now(); var tx_sink = TxSink{ .backend = &backend, .sent = &stats.sent.v }; var rx_sink = rx.QueueSink{ .queue = &queue, .io = io }; var tx_fut = (io.concurrent(txWorkerV6, .{ &eng, &tmpl, &bucket, &tx_sink, count, tx_budget_ns, &tx_done })) catch { try derr.writeAll(concurrency_hint); try derr.flush(); return; }; var rx_fut = (io.concurrent(rxWorkerV6, .{ &receiver, rx.TcpClassifier6{ .ck = ck }, &dd, &rx_sink, &rx_done })) catch { _ = tx_fut.await(io); try derr.writeAll(concurrency_hint); try derr.flush(); return; }; var dash = output.Dashboard.init(err_level, interactive, dash_total); const render_interval_ns: u64 = if (interactive) render_tick_interactive_ns else render_tick_plain_ns; var drain_buf: [drain_batch]rx.Result = undefined; 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, "tcp"); if (json) out.flush() catch {}; const now = clock.now(); if (last_render == 0 or now -| last_render >= render_interval_ns) { dash.render(derr, &stats, now -| t0) catch {}; last_render = now; } clock.sleepNs(drain_tick_ns); } 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, "tcp"); if (json) out.flush() catch {}; dash.render(derr, &stats, clock.now() -| t0) catch {}; var open_n: u64 = 0; var closed_n: u64 = 0; var filtered_n: u64 = 0; var unfiltered_n: u64 = 0; for (found.items) |r| switch (r.state) { .open => open_n += 1, .closed => closed_n += 1, .filtered => filtered_n += 1, .unfiltered => unfiltered_n += 1, }; if (!json) { if (found.items.len > 0) { std.mem.sort(rx.Result, found.items, {}, output.ipPortLess); try out.writeByte('\n'); try output.renderTable(out, out_level, found.items); try out.flush(); } else { try derr.writeAll(" no open, closed, or filtered responses observed\n"); if (sent > 0) try derr.writeAll(" (if a cloud/VM hypervisor is filtering raw sends upstream, retry with --connect)\n"); } } 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, "SYN", ifname, elapsed_s, open_n, closed_n, filtered_n, unfiltered_n, 0); try derr.flush(); } fn runConnect( io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, env: *std.process.Environ.Map, target_text: []const u8, ifname: []const u8, rate: u64, is_udp: bool, json: bool, derr: *std.Io.Writer, ) !void { if (is_udp) { try derr.writeAll("scan: connect mode is TCP-only; --udp is not supported with --backend connect\n"); try derr.flush(); return; } if (netutil.hasFlag(args, "--banners")) { try derr.writeAll(" note: --banners is unavailable in connect mode; reporting open/closed/filtered only\n"); } const ports = if (netutil.getFlag(args, "--ports")) |p| try netutil.parsePorts(allocator, p) else try allocator.dupe(u16, &default_tcp_ports); var seed: u64 = undefined; if (netutil.getFlag(args, "--seed")) |s| { seed = try std.fmt.parseInt(u64, s, 10); } else { var seed_bytes: [8]u8 = undefined; try io.randomSecure(&seed_bytes); seed = std.mem.readInt(u64, &seed_bytes, .little); } const concurrency = if (netutil.getFlag(args, "--concurrency")) |c| try std.fmt.parseInt(usize, c, 10) else connect.default_concurrency; const timeout_ms = if (netutil.getFlag(args, "--connect-timeout")) |t| try std.fmt.parseInt(u64, t, 10) else connect.default_timeout_ms; const max_hosts = if (netutil.getFlag(args, "--max-hosts")) |m| try std.fmt.parseInt(u64, m, 10) else targets.default_max_hosts6; const choice = output.parseColorChoice(netutil.getFlag(args, "--color")); const out_level = output.envLevel(io, std.Io.File.stdout(), env, choice); const err_level = output.envLevel(io, std.Io.File.stderr(), env, choice); const stderr_tty = std.Io.File.stderr().isTty(io) catch false; const wide_enough = if (terminalCols(2)) |c| c >= min_dashboard_cols else true; const interactive = stderr_tty and wide_enough; try derr.flush(); if (std.mem.indexOfScalar(u8, target_text, ':') != null) { const cidr = targets.parseCidr6(target_text) catch |e| { try derr.print("scan: invalid IPv6 target '{s}' ({s})\n", .{ target_text, @errorName(e) }); try derr.flush(); return; }; var eng = targets.Engine6.init(allocator, cidr, ports, seed, max_hosts) catch |e| switch (e) { error.PrefixTooLarge => { try derr.print("scan: IPv6 prefix '{s}' is too large to enumerate (over --max-hosts {d}); narrow the prefix or raise --max-hosts\n", .{ target_text, max_hosts }); try derr.flush(); return; }, else => return e, }; defer eng.deinit(); const count = if (netutil.getFlag(args, "--count")) |c| try std.fmt.parseInt(u64, c, 10) else eng.total; try connect.run(allocator, .{ .source = .{ .v6 = &eng }, .count = count, .total = @min(count, eng.total), .rate = rate, .concurrency = concurrency, .timeout_ns = timeout_ms *| ns_per_ms, .out_level = out_level, .err_level = err_level, .interactive = interactive, .json = json, .target_text = target_text, .iface = ifname, }); return; } const cidr = try targets.parseCidr(target_text); var eng = try targets.Engine.init(allocator, &.{cidr}, ports, seed); defer eng.deinit(); const count = if (netutil.getFlag(args, "--count")) |c| try std.fmt.parseInt(u64, c, 10) else eng.total; try connect.run(allocator, .{ .source = .{ .v4 = &eng }, .count = count, .total = @min(count, eng.total), .rate = rate, .concurrency = concurrency, .timeout_ns = timeout_ms *| ns_per_ms, .out_level = out_level, .err_level = err_level, .interactive = interactive, .json = json, .target_text = target_text, .iface = ifname, }); } pub fn run(io: std.Io, allocator: std.mem.Allocator, args: []const []const u8, env: *std.process.Environ.Map) !void { var obuf: [4096]u8 = undefined; var ow = std.Io.File.stdout().writer(io, &obuf); const out = &ow.interface; var ebuf: [4096]u8 = undefined; var ew = std.Io.File.stderr().writer(io, &ebuf); const derr = &ew.interface; const target_text = netutil.getFlag(args, "--target") orelse { try derr.writeAll("scan: --target is required (e.g. --target 10.0.0.0/24)\n"); try derr.flush(); return; }; const ifname = netutil.getFlag(args, "--iface") orelse default_iface; const rate = if (netutil.getFlag(args, "--rate")) |r| try std.fmt.parseInt(u64, r, 10) else default_rate; 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 banners_flag = netutil.hasFlag(args, "--banners"); const backend_arg = netutil.getFlag(args, "--backend"); const connect_mode = netutil.hasFlag(args, "--connect") or (backend_arg != null and std.mem.eql(u8, backend_arg.?, "connect")); if (connect_mode) { return runConnect(io, allocator, args, env, target_text, ifname, rate, is_udp, json, derr); } const backend_choice = packet_io.parseChoice(backend_arg) orelse { try derr.writeAll("scan: --backend must be one of auto, xdp, afpacket, connect\n"); try derr.flush(); return; }; if (backend_choice == .auto) { const raw_status = rawprobe.probe(ifname); if (raw_status != .ok) { try derr.print(" raw sends unavailable on '{s}' ({s}); falling back to unprivileged connect scan\n", .{ ifname, raw_status.reason() }); try derr.writeAll(" (force the raw engine with --backend afpacket, or select it directly with --connect)\n"); try derr.flush(); return runConnect(io, allocator, args, env, target_text, ifname, rate, is_udp, json, derr); } } if (std.mem.indexOfScalar(u8, target_text, ':') != null) { return runV6Scan(io, allocator, args, env, target_text, ifname, rate, backend_choice, derr); } var scfg = stealth.parse(allocator, io, args) catch |e| switch (e) { error.AuthorizationRequired => { try derr.writeAll(authorized_warning); try derr.flush(); return; }, error.BadOsTemplate => { try derr.writeAll("scan: --os-template must be none, masscan, linux, windows, or macos\n"); try derr.flush(); return; }, error.BadScanType => { try derr.writeAll("scan: --scan-type must be syn, fin, null, xmas, maimon, ack, or window\n"); try derr.flush(); return; }, error.BadJitterMode => { try derr.writeAll("scan: --jitter must be poisson or none\n"); try derr.flush(); return; }, error.BadDecoySpec => { try derr.writeAll("scan: --decoys must be comma-separated IPv4 addresses and/or RND:N\n"); try derr.flush(); return; }, error.TooManyDecoys => { try derr.print("scan: at most {d} decoys allowed\n", .{stealth.max_decoys}); try derr.flush(); return; }, error.OutOfMemory => return e, }; defer scfg.deinit(allocator); if (is_udp and (scfg.profile != .none or scfg.scan != .syn or scfg.rotate or scfg.decoys.len > 0 or scfg.suppress_rst)) { try derr.writeAll(" note: --os-template/--scan-type/--source-port-rotation/--decoys/--suppress-rst apply to TCP scans; ignored for --udp\n"); } if (is_udp and banners_flag) { try derr.writeAll(" note: --banners is a TCP feature; ignored for --udp\n"); } const banners = banners_flag and !is_udp and scfg.scan == .syn; if (banners_flag and !is_udp and scfg.scan != .syn) { try derr.writeAll(" note: --banners needs a full handshake; ignored for non-SYN scan types\n"); } 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 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); const choice = output.parseColorChoice(netutil.getFlag(args, "--color")); const out_level = output.envLevel(io, std.Io.File.stdout(), env, choice); const err_level = output.envLevel(io, std.Io.File.stderr(), env, choice); const stderr_tty = std.Io.File.stderr().isTty(io) catch false; const wide_enough = if (terminalCols(2)) |c| c >= min_dashboard_cols else true; const interactive = stderr_tty and wide_enough; var seed: u64 = undefined; if (netutil.getFlag(args, "--seed")) |s| { seed = try std.fmt.parseInt(u64, s, 10); } else { var seed_bytes: [8]u8 = undefined; try io.randomSecure(&seed_bytes); seed = std.mem.readInt(u64, &seed_bytes, .little); } const cidr = try targets.parseCidr(target_text); var eng = try targets.Engine.init(allocator, &.{cidr}, ports, seed); defer eng.deinit(); const count = if (netutil.getFlag(args, "--count")) |c| try std.fmt.parseInt(u64, c, 10) else eng.total; const frames_per_probe: u64 = if (is_udp) 1 else 1 + @as(u64, @intCast(scfg.decoys.len)); const dash_total = @min(count, eng.total) *| frames_per_probe; const ck = try cookie.Cookie.random(io); const rot_span: u16 = if (scfg.rotate) @intCast(@min(@as(u32, scfg.rotate_span), 65536 - @as(u32, src_port))) else 0; const tcp_tmpl = template.SynTemplate.init(.{ .src_mac = src_mac, .dst_mac = gw_mac, .src_ip = src_ip, .src_port = src_port, .cookie = ck, .profile = scfg.profile, .scan = scfg.scan, .rotate = scfg.rotate, .rotate_base = src_port, .rotate_span = rot_span, .decoys = scfg.decoys, }); 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); if (scfg.jitter) bucket = bucket.withJitter(seed); var backend = packet_io.select(allocator, ifname, backend_choice, .{}, .{}, derr) catch |err| switch (err) { error.NeedCapNetRaw => { try derr.writeAll(need_cap_hint); try derr.flush(); return; }, error.XdpNotCompiledIn => { try derr.writeAll("scan: --backend xdp needs a build with -Dxdp\n"); try derr.flush(); return; }, else => return err, }; defer backend.close(); try derr.print(" using {s}\n", .{packet_io.kindLabel(backend.kind())}); if (scfg.profile != .none or scfg.scan != .syn or scfg.jitter or scfg.rotate or scfg.decoys.len > 0) { try derr.print(" stealth: template={s} scan={s} jitter={s} rotate={s} decoys={d}\n", .{ @tagName(scfg.profile), @tagName(scfg.scan), if (scfg.jitter) "on" else "off", if (scfg.rotate) "on" else "off", scfg.decoys.len, }); } var supp: ?stealth.RstSuppressor = null; if ((scfg.suppress_rst or banners) and !is_udp) { const lo = src_port; const hi = if (scfg.rotate) src_port +| (rot_span -| 1) else src_port; supp = stealth.RstSuppressor.install(allocator, io, src_ip, lo, hi) catch |e| blk: { try derr.print(" note: RST-suppression unavailable ({s})", .{@errorName(e)}); if (banners) try derr.writeAll("; banner grabs may be unreliable (the kernel RSTs the SYN-ACK)"); try derr.writeByte('\n'); break :blk null; }; } defer if (supp) |*s| s.teardown(); if (supp) |*s| { var hbuf: [160]u8 = undefined; try derr.print(" RST-suppression active (cleanup if hard-killed: {s})\n", .{s.cleanupHint(&hbuf)}); } var tx_done = std.atomic.Value(bool).init(false); var rx_done = std.atomic.Value(bool).init(false); const drain_window_ns: u64 = @as(u64, @intCast(@max(wait_ms, 0))) * ns_per_ms; const est_tx_ns: u64 = if (rate > 0) (dash_total / rate) *| ns_per_sec else rx_hard_cap_floor_ns; const tx_budget_ns: u64 = (est_tx_ns *| 4) +| rx_hard_cap_floor_ns; const hard_cap_ns: u64 = tx_budget_ns +| drain_window_ns; const banner_wait_ns: u64 = service.default_banner_wait_ns; const svc_drain_window_ns: u64 = @max(drain_window_ns, banner_wait_ns +| ns_per_sec); const svc_hard_cap_ns: u64 = tx_budget_ns +| svc_drain_window_ns; var banners_active = banners; var svc_socket: service.Socket = undefined; var svc_socket_open = false; var svc_engine: ?*service.Engine = null; if (banners_active) { if (service.Socket.open(ifname)) |s| { svc_socket = s; svc_socket_open = true; errdefer svc_socket.close(); const svc_eng = try allocator.create(service.Engine); svc_eng.* = service.Engine.init(.{ .cookie = ck, .our_ip = src_ip, .src_mac = src_mac, .gw_mac = gw_mac, .banner_wait_ns = banner_wait_ns, }); svc_engine = svc_eng; } else |e| { try derr.print(" note: banner engine socket unavailable ({s}); continuing port-scan only\n", .{@errorName(e)}); banners_active = false; } } defer if (svc_socket_open) svc_socket.close(); var fqbuf: [finding_queue_capacity]service.Finding = undefined; var finding_queue = std.Io.Queue(service.Finding).init(&fqbuf); var finding_sink = FindingSink{ .queue = &finding_queue, .io = io }; var svc_done = std.atomic.Value(bool).init(true); var findings: std.ArrayList(service.Finding) = .empty; var fdedup = try dedup.Dedup.init(allocator, dedup_capacity); defer fdedup.deinit(); var receiver = rx.Receiver.open(ifname, rx.ETH_P_IP, &tx_done, drain_window_ns, hard_cap_ns) catch |err| switch (err) { error.NeedCapNetRaw => { try derr.writeAll(need_cap_hint); try derr.flush(); return; }, else => return err, }; defer receiver.close(); var dd = try dedup.Dedup.init(allocator, dedup_capacity); defer dd.deinit(); var qbuf: [queue_capacity]rx.Result = undefined; var queue = std.Io.Queue(rx.Result).init(&qbuf); var found_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer found_arena.deinit(); const found_alloc = found_arena.allocator(); var found: std.ArrayList(rx.Result) = .empty; var stats: output.Stats = .{}; const json_out: ?*std.Io.Writer = if (json) out else null; 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{}; const t0 = clock.now(); var tx_sink = TxSink{ .backend = &backend, .sent = &stats.sent.v }; var rx_sink = rx.QueueSink{ .queue = &queue, .io = io }; 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; }; 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, rx.TcpClassifier{ .ck = ck, .scan = scfg.scan }, &dd, &rx_sink, &rx_done }); var rx_fut = rx_res catch { _ = tx_fut.await(io); try derr.writeAll(concurrency_hint); try derr.flush(); return; }; const SvcFuture = @typeInfo(@TypeOf(io.concurrent(svcWorker, .{ svc_engine orelse undefined, &svc_socket, finding_sink.sink(), &tx_done, svc_drain_window_ns, svc_hard_cap_ns, &svc_done, }))).error_union.payload; var svc_fut: ?SvcFuture = null; if (banners_active) { svc_done.store(false, .release); if (io.concurrent(svcWorker, .{ svc_engine.?, &svc_socket, finding_sink.sink(), &tx_done, svc_drain_window_ns, svc_hard_cap_ns, &svc_done, })) |f| { svc_fut = f; } else |_| { svc_done.store(true, .release); banners_active = false; try derr.writeAll(" note: banner engine could not launch a worker thread; continuing port-scan only\n"); } } var dash = output.Dashboard.init(err_level, interactive, dash_total); dash.banners_mode = banners_active; const render_interval_ns: u64 = if (interactive) render_tick_interactive_ns else render_tick_plain_ns; var drain_buf: [drain_batch]rx.Result = undefined; var finding_buf: [finding_drain_batch]service.Finding = undefined; var last_render: u64 = 0; while (!(tx_done.load(.acquire) and rx_done.load(.acquire) and svc_done.load(.acquire))) { drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out, proto_json); if (banners_active) { drainFindings(io, &finding_queue, finding_buf[0..], &findings, &fdedup, found_alloc, json_out); stats.banners.v.store(svc_engine.?.banners.load(.monotonic), .monotonic); } if (json) out.flush() catch {}; const now = clock.now(); if (last_render == 0 or now -| last_render >= render_interval_ns) { dash.render(derr, &stats, now -| t0) catch {}; last_render = now; } clock.sleepNs(drain_tick_ns); } const sent = tx_fut.await(io); rx_fut.await(io); if (svc_fut) |*f| f.await(io); queue.close(io); drainQueue(io, &queue, drain_buf[0..], &found, found_alloc, &stats, json_out, proto_json); if (banners_active) { finding_queue.close(io); drainFindings(io, &finding_queue, finding_buf[0..], &findings, &fdedup, found_alloc, json_out); stats.banners.v.store(svc_engine.?.banners.load(.monotonic), .monotonic); } if (json) out.flush() catch {}; dash.render(derr, &stats, clock.now() -| t0) catch {}; var open_n: u64 = 0; var closed_n: u64 = 0; var filtered_n: u64 = 0; var unfiltered_n: u64 = 0; for (found.items) |r| switch (r.state) { .open => open_n += 1, .closed => closed_n += 1, .filtered => filtered_n += 1, .unfiltered => unfiltered_n += 1, }; if (!json) { if (found.items.len > 0) { std.mem.sort(rx.Result, found.items, {}, output.ipPortLess); try out.writeByte('\n'); try output.renderTable(out, out_level, found.items); try out.flush(); } else { try derr.writeAll(" no open, closed, or filtered responses observed\n"); if (sent > 0) try derr.writeAll(" (if a cloud/VM hypervisor is filtering raw sends upstream, retry with --connect)\n"); } if (findings.items.len > 0) { std.mem.sort(service.Finding, findings.items, {}, findingLess); try out.writeByte('\n'); try renderServiceTable(out, out_level, found_alloc, findings.items); try out.flush(); } } 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, probe_label, ifname, elapsed_s, open_n, closed_n, filtered_n, unfiltered_n, stats.banners.v.load(.monotonic)); if (banners_active) { const drops = svc_engine.?.drops.load(.monotonic); if (drops > 0) try derr.print(" note: {d} banner connection(s) dropped under backpressure (conn-table full at {d})\n", .{ drops, service.default_capacity }); } if (is_udp) { const answered = open_n + closed_n + filtered_n + unfiltered_n; try output.renderUnanswered(derr, err_level, sent -| answered); } try derr.flush(); }