Implementing an interface for Raw Sockets

I think I need to implement an interface, but I’m not sure what type of interface to implement. These are my requirements:

  1. Allow my library code to interact with planned supported network interfaces: linux raw socket, windows raw socket (via npcap), or linux xdp.
  2. Allow user to implement their own raw socket interface (for embedded applications)
  3. Not use anytype because I literally cannot stomach how it degrades readability at the callsite.

My interface will likely have two functions:

writeAll: write all the bytes of a slice to the network
read: read bytes from the network

My questions:

  1. From these requirements, should I implement an interface like std.mem.allocator? Do I need a vtable?
  2. Where can I find a detailed and comprehensive overview of my options for interfaces?

this is what I’ve got so far:

/// Interface for networking hardware
pub const NetworkAdapter = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        write: *const fn (data: []const u8) anyerror!usize,
        read: *const fn (out: []u8) anyerror!usize,
    };
};

/// Raw socket implementation for NetworkAdapter
pub const RawSocket = struct {
    socket: std.posix.socket_t,

    pub fn init(
        ifname: []const u8,
    ) !RawSocket {
        assert(ifname.len <= std.posix.IFNAMESIZE - 1); // ifname too long
        const socket: std.posix.socket_t = try std.posix.socket(
            std.posix.AF.PACKET,
            std.posix.SOCK.RAW,
            std.mem.nativeToBig(u32, ETH_P_ETHERCAT),
        );
        var timeout_rcv = std.posix.timeval{
            .sec = 0,
            .usec = 1,
        };
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.RCVTIMEO,
            std.mem.asBytes(&timeout_rcv),
        );

        var timeout_snd = std.posix.timeval{
            .sec = 0,
            .usec = 1,
        };
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.SNDTIMEO,
            std.mem.asBytes(&timeout_snd),
        );
        const dontroute_enable: c_int = 1;
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.DONTROUTE,
            std.mem.asBytes(&dontroute_enable),
        );
        var ifr: std.posix.ifreq = std.mem.zeroInit(std.posix.ifreq, .{});
        @memcpy(ifr.ifrn.name[0..ifname.len], ifname);
        ifr.ifrn.name[ifname.len] = 0;
        try std.posix.ioctl_SIOCGIFINDEX(socket, &ifr);
        const ifindex: i32 = ifr.ifru.ivalue;

        const IFF_PROMISC = 256;
        const IFF_BROADCAST = 2;
        const SIOCGIFFLAGS = 0x8913;
        const SIOCSIFFLAGS = 0x8914;

        var rval = std.posix.errno(std.os.linux.ioctl(socket, SIOCGIFFLAGS, @intFromPtr(&ifr)));
        switch (rval) {
            .SUCCESS => {},
            else => {
                return error.nicError;
            },
        }
        ifr.ifru.flags = ifr.ifru.flags | IFF_BROADCAST | IFF_PROMISC;
        rval = std.posix.errno(std.os.linux.ioctl(socket, SIOCSIFFLAGS, @intFromPtr(&ifr)));
        switch (rval) {
            .SUCCESS => {},
            else => {
                return error.nicError;
            },
        }
        const sockaddr_ll = std.posix.sockaddr.ll{
            .family = std.posix.AF.PACKET,
            .ifindex = ifindex,
            .protocol = std.mem.nativeToBig(u16, @as(u16, ETH_P_ETHERCAT)),
            .halen = 0, //not used
            .addr = .{ 0, 0, 0, 0, 0, 0, 0, 0 }, //not used
            .pkttype = 0, //not used
            .hatype = 0, //not used
        };
        try std.posix.bind(socket, @ptrCast(&sockaddr_ll), @sizeOf(@TypeOf(sockaddr_ll)));
        return RawSocket{
            .socket = socket,
        };
    }

    pub fn deinit(self: *RawSocket) void {
        _ = self;
        // TODO: de init socket
    }

    pub fn write(self: *RawSocket, bytes: []const u8) std.posix.WriteError!usize {
        return try std.posix.write(self.socket, bytes);
    }

    pub fn read(self: *RawSocket, out: []u8) std.posix.ReadError!usize {
        return try std.posix.read(self.socket, out);
    }

    pub fn networkAdapter(self: *RawSocket) NetworkAdapter {
        return NetworkAdapter{
            .ptr = self,
            .vtable = &.{ .write = write, .read = read },
        };
    }
};

ugh Idk what I’m doing, really wish this was easier for a pythonista like me

/// Interface for networking hardware
pub const NetworkAdapter = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        write: *const fn (ctx: *anyopaque, data: []const u8) anyerror!usize,
        read: *const fn (ctx: *anyopaque, out: []u8) anyerror!usize,
    };

    pub fn write(ctx: *anyopaque, data: []const u8) anyerror!usize {
        const self: NetworkAdapter = @ptrCast(@alignCast(ctx));
        return try self.vtable.write(self.ptr, data);
    }

    pub fn read(ctx: *anyopaque, out: []u8) anyerror!usize {
        const self: NetworkAdapter = @ptrCast(@alignCast(ctx));
        return try self.vtable.read(self.ptr, out);
    }
};

jeff@jeff-debian:~/repos/gatorcat$ zig build
install
└─ install gatorcat
   └─ zig build-exe gatorcat Debug native 2 errors
src/nic.zig:103:37: error: no field or member function named 'write' in 'nic.NetworkAdapter'
            _ = self.network_adapter.write(out) catch return error.LinkError;
                ~~~~~~~~~~~~~~~~~~~~^~~~~~
src/nic.zig:252:28: note: struct declared here
pub const NetworkAdapter = struct {
                           ^~~~~~
src/nic.zig:261:9: note: 'write' is not a member function
    pub fn write(ctx: *anyopaque, data: []const u8) anyerror!usize {
    ~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/nic.zig:370:28: error: expected type '*const fn (*anyopaque, []const u8) anyerror!usize', found '*const fn (*nic.RawSocket, []const u8) error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,ProcessNotFound,Unexpected}!usize'
            .vtable = &.{ .write = write, .read = read },
                          ~^~~~~~~~~~~~~
src/nic.zig:370:28: note: pointer type child 'fn (*nic.RawSocket, []const u8) error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,ProcessNotFound,Unexpected}!usize' cannot cast into pointer type child 'fn (*anyopaque, []const u8) anyerror!usize'
src/nic.zig:370:28: note: parameter 0 '*nic.RawSocket' cannot cast into '*anyopaque'
src/nic.zig:370:28: note: pointer type child 'anyopaque' cannot cast into pointer type child 'nic.RawSocket'

well this built, we shall see if it works


/// Interface for networking hardware
pub const NetworkAdapter = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        write: *const fn (ctx: *anyopaque, data: []const u8) anyerror!usize,
        read: *const fn (ctx: *anyopaque, out: []u8) anyerror!usize,
    };

    pub fn write(self: NetworkAdapter, data: []const u8) anyerror!usize {
        return try self.vtable.write(self.ptr, data);
    }

    pub fn read(self: NetworkAdapter, out: []u8) anyerror!usize {
        return try self.vtable.read(self.ptr, out);
    }
};

/// Raw socket implementation for NetworkAdapter
pub const RawSocket = struct {
    socket: std.posix.socket_t,

    pub fn init(
        ifname: []const u8,
    ) !RawSocket {
        assert(ifname.len <= std.posix.IFNAMESIZE - 1); // ifname too long
        const socket: std.posix.socket_t = try std.posix.socket(
            std.posix.AF.PACKET,
            std.posix.SOCK.RAW,
            std.mem.nativeToBig(u32, ETH_P_ETHERCAT),
        );
        var timeout_rcv = std.posix.timeval{
            .sec = 0,
            .usec = 1,
        };
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.RCVTIMEO,
            std.mem.asBytes(&timeout_rcv),
        );

        var timeout_snd = std.posix.timeval{
            .sec = 0,
            .usec = 1,
        };
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.SNDTIMEO,
            std.mem.asBytes(&timeout_snd),
        );
        const dontroute_enable: c_int = 1;
        try std.posix.setsockopt(
            socket,
            std.posix.SOL.SOCKET,
            std.posix.SO.DONTROUTE,
            std.mem.asBytes(&dontroute_enable),
        );
        var ifr: std.posix.ifreq = std.mem.zeroInit(std.posix.ifreq, .{});
        @memcpy(ifr.ifrn.name[0..ifname.len], ifname);
        ifr.ifrn.name[ifname.len] = 0;
        try std.posix.ioctl_SIOCGIFINDEX(socket, &ifr);
        const ifindex: i32 = ifr.ifru.ivalue;

        const IFF_PROMISC = 256;
        const IFF_BROADCAST = 2;
        const SIOCGIFFLAGS = 0x8913;
        const SIOCSIFFLAGS = 0x8914;

        var rval = std.posix.errno(std.os.linux.ioctl(socket, SIOCGIFFLAGS, @intFromPtr(&ifr)));
        switch (rval) {
            .SUCCESS => {},
            else => {
                return error.nicError;
            },
        }
        ifr.ifru.flags = ifr.ifru.flags | IFF_BROADCAST | IFF_PROMISC;
        rval = std.posix.errno(std.os.linux.ioctl(socket, SIOCSIFFLAGS, @intFromPtr(&ifr)));
        switch (rval) {
            .SUCCESS => {},
            else => {
                return error.nicError;
            },
        }
        const sockaddr_ll = std.posix.sockaddr.ll{
            .family = std.posix.AF.PACKET,
            .ifindex = ifindex,
            .protocol = std.mem.nativeToBig(u16, @as(u16, ETH_P_ETHERCAT)),
            .halen = 0, //not used
            .addr = .{ 0, 0, 0, 0, 0, 0, 0, 0 }, //not used
            .pkttype = 0, //not used
            .hatype = 0, //not used
        };
        try std.posix.bind(socket, @ptrCast(&sockaddr_ll), @sizeOf(@TypeOf(sockaddr_ll)));
        return RawSocket{
            .socket = socket,
        };
    }

    pub fn deinit(self: *RawSocket) void {
        _ = self;
        // TODO: de init socket
    }

    pub fn write(ctx: *anyopaque, bytes: []const u8) std.posix.WriteError!usize {
        const self: *RawSocket = @ptrCast(@alignCast(ctx));
        return try std.posix.write(self.socket, bytes);
    }

    pub fn read(ctx: *anyopaque, out: []u8) std.posix.ReadError!usize {
        const self: *RawSocket = @ptrCast(@alignCast(ctx));
        return try std.posix.read(self.socket, out);
    }

    pub fn networkAdapter(self: *RawSocket) NetworkAdapter {
        return NetworkAdapter{
            .ptr = self,
            .vtable = &.{ .write = write, .read = read },
        };
    }
};

edit: It works!

I’m not sure if I should be using anyerror?

Does this ruin my ability to do exhaustive switching on errors?

Yeah, it would be best if you can avoid anyerror altogether. My mental model of anyerror is “it’s impossible for me to know which errors may arise” or “I’m using recursion and the compiler cannot infer the error union”.

Adding an error set in NetworkAdapter is a good idea.
You can enumerate error values or use || to construct unions of error sets.

Years into Zig, some advice: use inferred error sets for rapid development but try to convert them to explicit error sets as soon as you can. Likewise, avoid else => in error switches. Its hugely valuable to discover you introduced new error case from the compiler erroring.
– Mitchell Hashimoto

3 Likes

This Zig News article outlines several options. I haven’t written enough Zig yet to say whether it is outdated (written in Jan 2023) or still totally up to date.