Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
104 changes: 97 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,60 @@ 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;
// Match production fallback behavior when socket-specific ring setup is blocked.
ring.init(rx.fd) catch return;
defer ring.deinit();

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