Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ Trust authorization order:
| **meshguard** (Zig, userspace) | **3.93 Gbps** | **3.94 Gbps** | libsodium AVX2, UDP GRO, zero-copy GSO, NAPI busy-poll |
| boringtun (Rust, userspace) | 1.82 Gbps | 1.81 Gbps | Cloudflare userspace WG |

Linux UDP sockets can use an `io_uring` recvmsg/sendmsg ring when runtime
probing and initialization succeed; startup logs print the active UDP path.
The poll+recvmsg path remains the fallback, and the separate TUN `io_uring`
reader stays disabled pending bare-metal TUN read validation.

### Optimization History

| Optimization | Download | Δ |
Expand Down Expand Up @@ -408,6 +413,7 @@ Core functionality is implemented and under active benchmarking:
- [x] WireGuard tunnel FFI API (open/send/recv/close for encrypted audio/data channels)
- [x] Compile-time AEAD backend selection (libsodium on Linux, std.crypto on Android/macOS/FreeBSD/Windows)
- [x] Multi-queue TUN (`IFF_MULTI_QUEUE` — Linux, parallel I/O via flow-hash distribution)
- [x] `io_uring` UDP ring (Linux runtime-gated, fallback to poll+recvmsg)
- [x] `io_uring` TUN reader (implemented, runtime-disabled pending bare-metal TUN validation)
- [x] IPv6 dual-stack (ULA `fd99:6d67::/64`, deterministic from pubkey via Blake3)
- [x] DNS / mDNS seed discovery
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ meshguard is organized into six top-level modules, each with a focused responsib
- **`utun.zig`** / **`fbsdtun.zig`** / **`wintun.zig`** — macOS, FreeBSD, and Windows userspace tunnel devices
- **`darwincfg.zig`** / **`freebsdcfg.zig`** / **`wincfg.zig`** — platform interface configuration helpers
- **`io.zig`** — Event loop abstraction layer
- **`io_uring.zig`** — Linux io_uring integration for async I/O
- **`io_uring.zig`** — Linux io_uring UDP ring and TUN reader gates for async I/O
- **`pipeline.zig`** — Packet processing pipeline with batched encrypt/decrypt

## Packet Flow
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Reference map of all source modules and their responsibilities.
| `offload.zig` | GSO/GRO offload: `IFF_VNET_HDR`, segmentation offload for high-throughput paths |
| `pipeline.zig` | Packet processing pipeline: batched encrypt/decrypt with multi-queue TUN support |
| `io.zig` | Event loop abstraction layer |
| `io_uring.zig` | Linux io_uring integration for async I/O |
| `io_uring.zig` | Linux io_uring UDP ring and TUN reader gates for async I/O |

## `docker/`

Expand Down
43 changes: 30 additions & 13 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3293,8 +3293,8 @@ fn userspaceEventLoop(

// Set all TUN fds to non-blocking (only for legacy poll+read path).
// io_uring manages its own waiting and needs blocking fds.
const use_io_uring = lib.net.IoUring.isAvailable();
if (!use_io_uring) {
const use_tun_io_uring = lib.net.IoUring.isTunReaderAvailable();
if (!use_tun_io_uring) {
for (0..opened_workers) |w| {
const flags = posix.system.fcntl(tun_fds[w], posix.F.GETFL, @as(usize, 0));
switch (posix.errno(flags)) {
Expand Down Expand Up @@ -3353,10 +3353,10 @@ fn userspaceEventLoop(
};

// Log io_uring TUN reader choice (detection happened above)
if (use_io_uring) {
writeFormatted(stdout, " io_uring: available, using ring-based TUN reader\n", .{}) catch {};
if (use_tun_io_uring) {
writeFormatted(stdout, " io_uring TUN: using ring-based TUN reader\n", .{}) catch {};
} else {
writeFormatted(stdout, " io_uring: unavailable, using poll+read TUN reader\n", .{}) catch {};
writeFormatted(stdout, " io_uring TUN: {s}, using poll+read TUN reader\n", .{lib.net.IoUring.tunReaderUnavailableReason()}) catch {};
}

// Parallel pipeline: TUN readers + encrypt workers
Expand All @@ -3369,7 +3369,7 @@ fn userspaceEventLoop(
data_pool,
crypto_queue,
};
const thread = if (use_io_uring)
const thread = if (use_tun_io_uring)
std.Thread.spawn(.{}, tunReaderIoUring, args)
else
std.Thread.spawn(.{}, tunReaderPipeline, args);
Expand Down Expand Up @@ -3425,17 +3425,34 @@ fn userspaceEventLoop(

var gro_rx = BatchUdp.GROReceiver{};
var udp_ring: lib.net.IoUring.UdpRing = undefined;
var udp_ring_init_error: ?anyerror = null;
const udp_ring_available = lib.net.IoUring.isUdpRingAvailable();
const use_udp_ring = blk: {
udp_ring.init(udp_sock.fd) catch break :blk false;
if (!udp_ring_available) break :blk false;
udp_ring.init(udp_sock.fd) catch |err| {
udp_ring_init_error = err;
break :blk false;
};
break :blk true;
};
defer if (use_udp_ring) udp_ring.deinit();
if (use_udp_ring) {
const sqpoll_msg = if (udp_ring.sqpoll) "SQPOLL" else "submit";
const buffer_msg = if (udp_ring.registered_buffers) "registered buffers" else "unregistered buffers";
writeFormatted(stdout, " io_uring UDP: recvmsg/sendmsg ring active ({s}, {s})\n", .{ sqpoll_msg, buffer_msg }) catch {};
} else {
writeFormatted(stdout, " io_uring UDP: unavailable, using poll+recvmsg path\n", .{}) catch {};
const udp_path = lib.net.IoUring.selectUdpPath(udp_ring_available, use_udp_ring);
switch (udp_path) {
.io_uring => {
const sqpoll_msg = if (udp_ring.sqpoll) "SQPOLL" else "submit";
const buffer_msg = if (udp_ring.registered_buffers) "registered buffers" else "unregistered buffers";
writeFormatted(stdout, " UDP path: io_uring recvmsg/sendmsg ring active ({s}, {s})\n", .{ sqpoll_msg, buffer_msg }) catch {};
},
.poll_recvmsg => |reason| switch (reason) {
.runtime_unavailable => writeFormatted(stdout, " UDP path: poll+recvmsg (io_uring runtime unavailable)\n", .{}) catch {},
.init_failed => {
if (udp_ring_init_error) |err| {
writeFormatted(stdout, " UDP path: poll+recvmsg (io_uring init failed: {s})\n", .{@errorName(err)}) catch {};
} else {
writeFormatted(stdout, " UDP path: poll+recvmsg (io_uring init failed)\n", .{}) catch {};
}
},
},
}

const MAX_DECRYPTED = 64;
Expand Down
103 changes: 96 additions & 7 deletions src/net/io_uring.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const posix = std.posix;
const linux = std.os.linux;
const IoUring = linux.IoUring;
const Offload = @import("offload.zig");
const Udp = @import("udp.zig");

const UDP_RECV_USER_DATA: u64 = 0x1000_0000_0000_0000;
const UDP_SEND_USER_DATA: u64 = 0x2000_0000_0000_0000;
Expand All @@ -30,6 +31,22 @@ fn initRing(entries: u16, use_sqpoll: bool) !IoUring {
};
}

pub const UdpFallbackReason = enum {
runtime_unavailable,
init_failed,
};

pub const UdpPath = union(enum) {
io_uring,
poll_recvmsg: UdpFallbackReason,
};

pub fn selectUdpPath(runtime_available: bool, init_succeeded: bool) UdpPath {
if (!runtime_available) return .{ .poll_recvmsg = .runtime_unavailable };
if (!init_succeeded) return .{ .poll_recvmsg = .init_failed };
return .io_uring;
}

/// TUN reader backed by io_uring.
/// Pre-submits N read SQEs and resubmits on each completion.
/// The caller processes completed reads identically to the poll()+read() path.
Expand Down Expand Up @@ -365,17 +382,33 @@ pub const UdpRing = struct {
}
};

/// Check whether the UDP io_uring path should be attempted. This is intentionally
/// independent from the TUN-reader gate: UDP sockets can use recvmsg/sendmsg rings
/// even while TUN character-device reads remain disabled pending validation.
pub fn isUdpRingAvailable() bool {
if (builtin.os.tag != .linux) return false;
var ring = initRing(8, true) catch return false;
ring.deinit();
return true;
}

/// Check if io_uring is available AND compatible with TUN devices.
/// Currently disabled: TUN character devices (tun_chr_read_iter) do not
/// reliably support io_uring IORING_OP_READ — reads fail silently or
/// return errors in LXC containers. The io_uring TUN reader code is
/// preserved for future bare-metal testing.
pub fn isAvailable() bool {
// TODO: Enable after validating io_uring reads on TUN devices work
// on bare metal (outside LXC containers).
pub fn isTunReaderAvailable() bool {
// TUN character devices (tun_chr_read_iter) still need bare-metal validation:
// reads have failed silently or returned errors in LXC containers. Keep the
// reader code preserved, but do not let that disable the UDP ring path.
return false;
}

pub fn tunReaderUnavailableReason() []const u8 {
return "disabled pending bare-metal TUN read validation";
}

/// Back-compat alias for older call sites; prefer the split gates above.
pub fn isAvailable() bool {
return isTunReaderAvailable();
}

test "UdpRing parses UDP_GRO cmsg segment size" {
var slot = UdpRing.RecvSlot{};
slot.setup();
Expand All @@ -393,3 +426,59 @@ test "UdpRing parses UDP_GRO cmsg segment size" {

try std.testing.expectEqual(@as(u16, 1440), slot.segmentSize());
}

test "io_uring UDP and TUN availability gates are independent" {
switch (selectUdpPath(false, true)) {
.poll_recvmsg => |reason| try std.testing.expectEqual(UdpFallbackReason.runtime_unavailable, reason),
.io_uring => return error.TestUnexpectedResult,
}
switch (selectUdpPath(true, false)) {
.poll_recvmsg => |reason| try std.testing.expectEqual(UdpFallbackReason.init_failed, reason),
.io_uring => return error.TestUnexpectedResult,
}
switch (selectUdpPath(true, true)) {
.io_uring => {},
.poll_recvmsg => return error.TestUnexpectedResult,
}
try std.testing.expect(!isTunReaderAvailable());
_ = isUdpRingAvailable();
}

test "UdpRing receives loopback datagram when runtime available" {
if (!isUdpRingAvailable()) return;

var rx = try Udp.UdpSocket.bindAddr(.{ 127, 0, 0, 1 }, 0);
defer rx.close();
var tx = try Udp.UdpSocket.bindAddr(.{ 127, 0, 0, 1 }, 0);
defer tx.close();

var ring: UdpRing = undefined;
try ring.init(rx.fd);
defer ring.deinit();
Comment on lines +455 to +458

const payload = "meshguard-udp-ring-probe";
try std.testing.expectEqual(payload.len, try tx.sendTo(payload, .{ 127, 0, 0, 1 }, rx.port));

var cqes: [UdpRing.RECV_DEPTH + UdpRing.SEND_DEPTH]linux.io_uring_cqe = undefined;
var attempts: usize = 0;
while (attempts < 100) : (attempts += 1) {
const n = ring.copyCompletions(&cqes, 0) catch 0;
for (cqes[0..n]) |cqe| {
ring.noteSendCompletion(cqe);
const recv = ring.recvCompletion(cqe) orelse {
if (UdpRing.recvSlotFromUserData(cqe.user_data)) |slot| {
ring.resubmitRecv(slot, rx.fd) catch {};
}
continue;
};
try std.testing.expectEqualSlices(u8, payload, recv.data);
try std.testing.expectEqualSlices(u8, &[_]u8{ 127, 0, 0, 1 }, &recv.sender_addr);
try std.testing.expect(recv.sender_port != 0);
try ring.resubmitRecv(recv.slot, rx.fd);
return;
}
std.Thread.sleep(1 * std.time.ns_per_ms);
}

return error.UdpRingProbeTimedOut;
}
Loading