222 lines
7.4 KiB
Zig
222 lines
7.4 KiB
Zig
// ©AngelaMos | 2026
|
|
// afpacket.zig
|
|
|
|
const std = @import("std");
|
|
const packet = @import("packet");
|
|
|
|
const TPACKET_ALIGNMENT: usize = 16;
|
|
|
|
fn tpacketAlign(x: usize) usize {
|
|
return (x + TPACKET_ALIGNMENT - 1) & ~(TPACKET_ALIGNMENT - 1);
|
|
}
|
|
|
|
pub const tpacket2_hdr = extern struct {
|
|
tp_status: u32,
|
|
tp_len: u32,
|
|
tp_snaplen: u32,
|
|
tp_mac: u16,
|
|
tp_net: u16,
|
|
tp_sec: u32,
|
|
tp_nsec: u32,
|
|
tp_vlan_tci: u16,
|
|
tp_vlan_tpid: u16,
|
|
tp_padding: [4]u8,
|
|
};
|
|
|
|
const TP_STATUS_AVAILABLE: u32 = 0;
|
|
const TP_STATUS_SEND_REQUEST: u32 = 1;
|
|
const DATA_OFFSET: usize = 32;
|
|
|
|
comptime {
|
|
std.debug.assert(@sizeOf(tpacket2_hdr) == 32);
|
|
std.debug.assert(DATA_OFFSET == tpacketAlign(@sizeOf(tpacket2_hdr)));
|
|
}
|
|
|
|
pub const Ring = struct {
|
|
buf: []u8,
|
|
frame_size: usize,
|
|
frame_nr: usize,
|
|
next: usize,
|
|
|
|
pub fn init(buf: []u8, frame_size: usize, frame_nr: usize) Ring {
|
|
return .{ .buf = buf, .frame_size = frame_size, .frame_nr = frame_nr, .next = 0 };
|
|
}
|
|
|
|
fn header(self: Ring, idx: usize) *tpacket2_hdr {
|
|
return @ptrCast(@alignCast(self.buf.ptr + idx * self.frame_size));
|
|
}
|
|
|
|
pub fn reserve(self: *Ring) ?usize {
|
|
const idx = self.next;
|
|
const hdr = self.header(idx);
|
|
if (@atomicLoad(u32, &hdr.tp_status, .monotonic) != TP_STATUS_AVAILABLE) return null;
|
|
self.next = (self.next + 1) % self.frame_nr;
|
|
return idx;
|
|
}
|
|
|
|
pub fn fill(self: *Ring, idx: usize, frame: []const u8) void {
|
|
std.debug.assert(frame.len + DATA_OFFSET <= self.frame_size);
|
|
const hdr = self.header(idx);
|
|
const data = self.buf[idx * self.frame_size + DATA_OFFSET ..][0..frame.len];
|
|
@memcpy(data, frame);
|
|
hdr.tp_len = @intCast(frame.len);
|
|
@atomicStore(u32, &hdr.tp_status, TP_STATUS_SEND_REQUEST, .monotonic);
|
|
}
|
|
};
|
|
|
|
const linux = std.os.linux;
|
|
|
|
pub const OpenError = error{
|
|
NeedCapNetRaw,
|
|
SocketFailed,
|
|
IfIndexFailed,
|
|
SetSockOptFailed,
|
|
MmapFailed,
|
|
BindFailed,
|
|
};
|
|
|
|
pub const RingConfig = struct {
|
|
frame_size: u32 = 2048,
|
|
block_size: u32 = 1 << 22,
|
|
block_nr: u32 = 4,
|
|
};
|
|
|
|
pub const Backend = struct {
|
|
fd: i32,
|
|
ring: Ring,
|
|
map: []align(std.heap.page_size_min) u8,
|
|
sll: linux.sockaddr.ll,
|
|
|
|
pub fn open(ifname: []const u8, cfg: RingConfig) OpenError!Backend {
|
|
const rc_sock = linux.socket(
|
|
linux.AF.PACKET,
|
|
linux.SOCK.RAW,
|
|
std.mem.nativeToBig(u16, @as(u16, linux.ETH.P.IP)),
|
|
);
|
|
switch (linux.errno(rc_sock)) {
|
|
.SUCCESS => {},
|
|
.PERM, .ACCES => return error.NeedCapNetRaw,
|
|
else => return error.SocketFailed,
|
|
}
|
|
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.IfIndexFailed;
|
|
@memcpy(ifr.ifrn.name[0..ifname.len], ifname);
|
|
if (linux.errno(linux.ioctl(fd, linux.SIOCGIFINDEX, @intFromPtr(&ifr))) != .SUCCESS)
|
|
return error.IfIndexFailed;
|
|
const ifindex: i32 = ifr.ifru.ivalue;
|
|
|
|
var ver: u32 = @intFromEnum(linux.tpacket_versions.V2);
|
|
if (linux.errno(linux.setsockopt(fd, linux.SOL.PACKET, linux.PACKET.VERSION, std.mem.asBytes(&ver), @sizeOf(u32))) != .SUCCESS)
|
|
return error.SetSockOptFailed;
|
|
|
|
const frame_nr = (cfg.block_size / cfg.frame_size) * cfg.block_nr;
|
|
var req = linux.tpacket_req3{
|
|
.block_size = cfg.block_size,
|
|
.block_nr = cfg.block_nr,
|
|
.frame_size = cfg.frame_size,
|
|
.frame_nr = frame_nr,
|
|
.retire_blk_tov = 0,
|
|
.sizeof_priv = 0,
|
|
.feature_req_word = 0,
|
|
};
|
|
if (linux.errno(linux.setsockopt(fd, linux.SOL.PACKET, linux.PACKET.TX_RING, std.mem.asBytes(&req), @sizeOf(@TypeOf(req)))) != .SUCCESS)
|
|
return error.SetSockOptFailed;
|
|
|
|
var bypass: u32 = 1;
|
|
_ = linux.setsockopt(fd, linux.SOL.PACKET, linux.PACKET.QDISC_BYPASS, std.mem.asBytes(&bypass), @sizeOf(u32));
|
|
|
|
const ring_sz: usize = @as(usize, cfg.block_size) * cfg.block_nr;
|
|
const rc_map = linux.mmap(null, ring_sz, .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED }, fd, 0);
|
|
if (linux.errno(rc_map) != .SUCCESS) return error.MmapFailed;
|
|
const map_ptr: [*]align(std.heap.page_size_min) u8 = @ptrFromInt(rc_map);
|
|
const map = map_ptr[0..ring_sz];
|
|
errdefer _ = linux.munmap(map.ptr, ring_sz);
|
|
|
|
var sll = std.mem.zeroes(linux.sockaddr.ll);
|
|
sll.family = linux.AF.PACKET;
|
|
sll.protocol = std.mem.nativeToBig(u16, @as(u16, linux.ETH.P.IP));
|
|
sll.ifindex = ifindex;
|
|
if (linux.errno(linux.bind(fd, @ptrCast(&sll), @sizeOf(linux.sockaddr.ll))) != .SUCCESS)
|
|
return error.BindFailed;
|
|
|
|
return .{
|
|
.fd = fd,
|
|
.ring = Ring.init(map, cfg.frame_size, frame_nr),
|
|
.map = map,
|
|
.sll = sll,
|
|
};
|
|
}
|
|
|
|
pub fn submit(self: *Backend, frame: []const u8) bool {
|
|
const idx = self.ring.reserve() orelse return false;
|
|
self.ring.fill(idx, frame);
|
|
return true;
|
|
}
|
|
|
|
pub fn kick(self: *Backend) void {
|
|
var dummy: u8 = 0;
|
|
_ = linux.sendto(self.fd, @ptrCast(&dummy), 0, 0, @ptrCast(&self.sll), @sizeOf(linux.sockaddr.ll));
|
|
}
|
|
|
|
pub fn close(self: *Backend) void {
|
|
_ = linux.munmap(self.map.ptr, self.map.len);
|
|
_ = linux.close(self.fd);
|
|
}
|
|
};
|
|
|
|
test "tpacket2_hdr is the wire-exact 32 bytes" {
|
|
try std.testing.expectEqual(@as(usize, 32), @sizeOf(tpacket2_hdr));
|
|
}
|
|
|
|
test "Ring reserve cycles all frames then returns null when full" {
|
|
const frame_size: usize = 2048;
|
|
const frame_nr: usize = 4;
|
|
const buf = try std.testing.allocator.alloc(u8, frame_size * frame_nr);
|
|
defer std.testing.allocator.free(buf);
|
|
@memset(buf, 0);
|
|
var ring = Ring.init(buf, frame_size, frame_nr);
|
|
|
|
var i: usize = 0;
|
|
while (i < frame_nr) : (i += 1) {
|
|
const idx = ring.reserve() orelse return error.UnexpectedFull;
|
|
ring.fill(idx, &[_]u8{ 0xAA, 0xBB });
|
|
}
|
|
try std.testing.expect(ring.reserve() == null);
|
|
}
|
|
|
|
test "fill writes len, SEND_REQUEST status, and the frame bytes at the data offset" {
|
|
const frame_size: usize = 2048;
|
|
const buf = try std.testing.allocator.alloc(u8, frame_size);
|
|
defer std.testing.allocator.free(buf);
|
|
@memset(buf, 0);
|
|
var ring = Ring.init(buf, frame_size, 1);
|
|
|
|
const idx = ring.reserve().?;
|
|
const payload = [_]u8{ 1, 2, 3, 4, 5 };
|
|
ring.fill(idx, &payload);
|
|
|
|
const hdr: *const tpacket2_hdr = @ptrCast(@alignCast(buf.ptr));
|
|
try std.testing.expectEqual(@as(u32, payload.len), hdr.tp_len);
|
|
try std.testing.expectEqual(TP_STATUS_SEND_REQUEST, @atomicLoad(u32, &hdr.tp_status, .monotonic));
|
|
try std.testing.expectEqualSlices(u8, &payload, buf[DATA_OFFSET .. DATA_OFFSET + payload.len]);
|
|
}
|
|
|
|
test "a kernel-drained slot (status reset to AVAILABLE) is reusable" {
|
|
const frame_size: usize = 2048;
|
|
const buf = try std.testing.allocator.alloc(u8, frame_size);
|
|
defer std.testing.allocator.free(buf);
|
|
@memset(buf, 0);
|
|
var ring = Ring.init(buf, frame_size, 1);
|
|
|
|
const idx0 = ring.reserve().?;
|
|
ring.fill(idx0, &[_]u8{0x01});
|
|
try std.testing.expect(ring.reserve() == null);
|
|
|
|
const hdr: *tpacket2_hdr = @ptrCast(@alignCast(buf.ptr));
|
|
@atomicStore(u32, &hdr.tp_status, TP_STATUS_AVAILABLE, .monotonic);
|
|
try std.testing.expect(ring.reserve() != null);
|
|
}
|