Raw socket with the new std.Io.net interfaces

I am trying to implement an API that uses raw sockets to send data. I want to be able to specify all of the content in a packet, from the ethernet headers to the packet payload.

This document outlines the binary protocol that I am trying to implement: docs/saprus_proto_design.md · main · C2 Games / Red Team / Saprus · GitLab

This function is a basic example of how they create a raw socket and send data in the existing implementation of the protocol: utils/relay_message.go · dev · C2 Games / Red Team / Saprus · GitLab

Here is what I have tried doing using std.Io.net:

const ip: std.Io.net.IpAddress = .{ .ip4 = .unspecified(0) };
const socket = try ip.bind(init.io, .{ .mode = .raw, .protocol = .ethernet }); // I have also tried .protocol = .raw and gotten the same behavior.
defer socket.close(init.io);

try socket.send(init.io, &.{ .ip4 = try .parse("255.255.255.255", 8888) }, "foo");

Where I have foo I want to write data starting with the ethernet headers, then the IP headers, then the UDP headers, then my payload data. If I run my program as a non-root user, the program fails at the try ip.bind(...), which is what I would expect and what I want. If I run as root, I fail with the following stack trace:

unexpected errno: 13
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/posix.zig:2216:40: 0x10645e0 in unexpectedErrno (std.zig)
        std.debug.dumpCurrentStackTrace(.{});
                                       ^
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/Io/Threaded.zig:11305:63: 0x11bfb46 in netSendMany (std.zig)
                    else => |err| return posix.unexpectedErrno(err),
                                                              ^
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/Io/Threaded.zig:11099:29: 0x11b3c7f in netSendPosix (std.zig)
            i += netSendMany(handle, messages[i..], posix_flags) catch |err| return .{ err, i };
                            ^
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/Io/net.zig:1101:47: 0x11a7e72 in send (std.zig)
        const err, const n = io.vtable.netSend(io.userdata, s.handle, (&message)[0..1], .{});
                                              ^
/home/robby/src/zaprus/src/main.zig:100:20: 0x11a35ce in main (main.zig)
    try socket.send(init.io, &.{ .ip4 = try .parse("255.255.255.255", 8888) }, "foo");
                   ^
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/start.zig:718:30: 0x11a43eb in callMain (std.zig)
    return wrapMain(root.main(.{
                             ^
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/start.zig:190:5: 0x11a2a81 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: Unexpected
/home/robby/downloads/zig-x86_64-linux-0.16.0-dev.2223+4f16e80ce/lib/std/Io/net.zig:1102:21: 0x11a7f0e in send (std.zig)
        if (n != 1) return err.?;
                    ^
/home/robby/src/zaprus/src/main.zig:100:5: 0x11a365f in main (main.zig)
    try socket.send(init.io, &.{ .ip4 = try .parse("255.255.255.255", 8888) }, "foo");
    ^

(errno 13 is std.os.linux.E.ACCES (Permission denied), which should probably be an expected error)

Am I trying to do something that is simply unsupported by the current Io interface? Or am I misunderstanding how to do what I am trying to do? It seems like, instead of specifying an IP address and port that is not actually used, I should be able to get a raw socket by specifying the std.Io.net.Interface, but there does not seem to be any way to do that right now.

Have you look via strace which syscall fails?

Man page says:

       EACCES (For UNIX domain sockets, which are identified by pathname) Write permission is denied on the des‐
              tination  socket  file, or search permission is denied for one of the directories the path prefix.
              (See path_resolution(7).)

              (For UDP sockets) An attempt was made to send to a network/broadcast address as though  it  was  a
              unicast address.

Thanks for reporting the unexpected errno, I put up #30874 - std.Io.Threaded: sendmmsg can return EACCES - ziglang/zig - Codeberg.org for the missing error code.

IPPROTO_RAW, which I think is what your code is currently using, is different from an AF_PACKET socket.

IPPROTO_RAW:

AF_PACKET:

IPPROTO_RAW is for implementing protocols on top of IPv4. You will not be able to control the MAC address etc at this layer.

AF_PACKET is for implementing protocols on top of ethernet (layer 2). At this layer you can control the MAC header etc.

I haven’t looks too closely yet into how to implement AF_PACKET (“raw sockets”) on the io interface yet. It may not be possible, considering that it is impossible to accomplish on windows without a kernel level driver like npcap. (Windows doesn’t think I should have control over my own machine).

The IO interface would need to do the equivalent of the following:

create a socket of type AF_PACKET and SOCK_RAW, with protocol of your choice. The choice of protocol is a filter applied to your packet socket for ingested packets. Only packets with the selected protocol (a 2 byte value in the ethernet header) will pass into your socket. Man page for packet says:

By default, all packets of the specified protocol type are passed
       to a packet socket. 

In this example we filter for EtherCAT packets, but you can put anything you want there.

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),
        );

You will then need to “bind to an interface”. This means bind to an ethernet port like eth0, or eno1 etc. It depends on your distribution on how they name the interfaces. You can list the available ethernet interfaces on your system with ip addr command.

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;

        var rval = std.posix.errno(std.os.linux.ioctl(socket, std.os.linux.SIOCGIFFLAGS, @intFromPtr(&ifr)));
        switch (rval) {
            .SUCCESS => {},
            else => {
                return error.NicError;
            },
        }
        ifr.ifru.flags.BROADCAST = true;
        ifr.ifru.flags.PROMISC = true;
        rval = std.posix.errno(std.os.linux.ioctl(socket, std.os.linux.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)));

These code examples came from my library gatorcat which uses raw sockets to send EtherCAT frames on an ethernet interface: Could at least make sure that all gods with.

I wrote this code a long long time ago. I should clean it up and not use std.posix anymore, instead using the linux syscalls directly. But the concepts should be the same.