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

173 lines
5.9 KiB
Zig

// ©AngelaMos | 2026
// rawprobe.zig
const std = @import("std");
const linux = std.os.linux;
pub const Status = enum {
ok,
no_cap,
no_egress,
pub fn reason(self: Status) []const u8 {
return switch (self) {
.ok => "raw sends work",
.no_cap => "no CAP_NET_RAW",
.no_egress => "raw sends are silently dropped",
};
}
};
const ETH_P_ALL: u16 = 0x0003;
const probe_ethertype: u16 = 0x88b5;
const eth_hdr_len: usize = 14;
const nonce_off: usize = eth_hdr_len;
const nonce_len: usize = 8;
const frame_len: usize = 60;
const poll_budget_ms: i64 = 250;
const poll_tick_ms: i32 = 25;
const probe_mac = [6]u8{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 };
fn monoMs() i64 {
var ts: linux.timespec = undefined;
_ = linux.clock_gettime(.MONOTONIC, &ts);
return @as(i64, @intCast(ts.sec)) * 1000 + @divTrunc(@as(i64, @intCast(ts.nsec)), 1_000_000);
}
fn freshNonce() [nonce_len]u8 {
var ts: linux.timespec = undefined;
_ = linux.clock_gettime(.MONOTONIC, &ts);
const mixed: u64 = (@as(u64, @intCast(ts.nsec)) ^ (@as(u64, @intCast(ts.sec)) << 20)) *% 0x9e3779b97f4a7c15;
var out: [nonce_len]u8 = undefined;
std.mem.writeInt(u64, &out, mixed, .little);
return out;
}
fn buildFrame(nonce: [nonce_len]u8) [frame_len]u8 {
var frame = [_]u8{0} ** frame_len;
@memcpy(frame[0..6], &probe_mac);
@memcpy(frame[6..12], &probe_mac);
std.mem.writeInt(u16, frame[12..14], probe_ethertype, .big);
@memcpy(frame[nonce_off .. nonce_off + nonce_len], &nonce);
return frame;
}
fn carriesNonce(frame: []const u8, nonce: [nonce_len]u8) bool {
if (frame.len < nonce_off + nonce_len) return false;
if (std.mem.readInt(u16, frame[12..14], .big) != probe_ethertype) return false;
return std.mem.eql(u8, frame[nonce_off .. nonce_off + nonce_len], &nonce);
}
const send_bursts: usize = 3;
const BoundSock = struct { fd: i32, sll: linux.sockaddr.ll };
const OpenError = error{ NoCap, Failed };
fn openBound(ifname: []const u8) OpenError!BoundSock {
const rc_sock = linux.socket(linux.AF.PACKET, linux.SOCK.RAW, std.mem.nativeToBig(u16, ETH_P_ALL));
switch (linux.errno(rc_sock)) {
.SUCCESS => {},
.PERM, .ACCES => return error.NoCap,
else => return error.Failed,
}
const fd: i32 = @intCast(rc_sock);
errdefer _ = linux.close(fd);
var ifr = std.mem.zeroes(linux.ifreq);
if (ifname.len >= ifr.ifrn.name.len) return error.Failed;
@memcpy(ifr.ifrn.name[0..ifname.len], ifname);
if (linux.errno(linux.ioctl(fd, linux.SIOCGIFINDEX, @intFromPtr(&ifr))) != .SUCCESS) return error.Failed;
var sll = std.mem.zeroes(linux.sockaddr.ll);
sll.family = linux.AF.PACKET;
sll.protocol = std.mem.nativeToBig(u16, ETH_P_ALL);
sll.ifindex = ifr.ifru.ivalue;
if (linux.errno(linux.bind(fd, @ptrCast(&sll), @sizeOf(linux.sockaddr.ll))) != .SUCCESS) return error.Failed;
return .{ .fd = fd, .sll = sll };
}
fn openStatus(e: OpenError) Status {
return switch (e) {
error.NoCap => .no_cap,
error.Failed => .no_egress,
};
}
pub fn probe(ifname: []const u8) Status {
const observer = openBound(ifname) catch |e| return openStatus(e);
defer _ = linux.close(observer.fd);
var sender = openBound(ifname) catch |e| return openStatus(e);
defer _ = linux.close(sender.fd);
const nonce = freshNonce();
const frame = buildFrame(nonce);
var burst: usize = 0;
while (burst < send_bursts) : (burst += 1) {
_ = linux.sendto(sender.fd, &frame, frame.len, 0, @ptrCast(&sender.sll), @sizeOf(linux.sockaddr.ll));
}
var buf: [2048]u8 = undefined;
const deadline = monoMs() + poll_budget_ms;
while (monoMs() < deadline) {
var pfd = [_]linux.pollfd{.{ .fd = observer.fd, .events = linux.POLL.IN, .revents = 0 }};
const pr = linux.poll(&pfd, 1, poll_tick_ms);
switch (linux.errno(pr)) {
.SUCCESS => {},
.INTR => continue,
else => return .no_egress,
}
if (pr == 0) continue;
const rc = linux.recvfrom(observer.fd, &buf, buf.len, 0, null, null);
switch (linux.errno(rc)) {
.SUCCESS => {},
.INTR, .AGAIN => continue,
else => return .no_egress,
}
const n: usize = @intCast(rc);
if (carriesNonce(buf[0..n], nonce)) return .ok;
}
return .no_egress;
}
test "buildFrame stamps the experimental ethertype, self-addressed MACs, and the nonce" {
const nonce = [8]u8{ 1, 2, 3, 4, 5, 6, 7, 8 };
const f = buildFrame(nonce);
try std.testing.expectEqual(@as(usize, 60), f.len);
try std.testing.expectEqualSlices(u8, &probe_mac, f[0..6]);
try std.testing.expectEqualSlices(u8, &probe_mac, f[6..12]);
try std.testing.expectEqual(@as(u16, probe_ethertype), std.mem.readInt(u16, f[12..14], .big));
try std.testing.expectEqualSlices(u8, &nonce, f[14..22]);
}
test "carriesNonce matches only our tagged frame, rejects other traffic and wrong nonce" {
const nonce = freshNonce();
const other = freshNonce();
const f = buildFrame(nonce);
try std.testing.expect(carriesNonce(&f, nonce));
try std.testing.expect(!carriesNonce(&f, other));
var ip_frame = [_]u8{0} ** 60;
std.mem.writeInt(u16, ip_frame[12..14], 0x0800, .big);
@memcpy(ip_frame[14..22], &nonce);
try std.testing.expect(!carriesNonce(&ip_frame, nonce));
try std.testing.expect(!carriesNonce(&[_]u8{ 0, 1, 2 }, nonce));
}
test "freshNonce is not trivially constant across calls" {
const a = freshNonce();
var differs = false;
var i: usize = 0;
while (i < 8) : (i += 1) {
const b = freshNonce();
if (!std.mem.eql(u8, &a, &b)) differs = true;
}
try std.testing.expect(differs);
}
test "probe returns no_cap without CAP_NET_RAW, otherwise a defined status" {
const st = probe("lo");
try std.testing.expect(st == .ok or st == .no_cap or st == .no_egress);
}