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
7 changes: 3 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# Windows dropped during Zig 0.16 port — src/sys.zig, sockaddr, and
# the libxev dep have @compileError branches for Windows pending a
# proper Winsock implementation.
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -36,6 +33,8 @@ jobs:
# real server over a socket, no browser needed. The browser legs stay
# local — see SPEC/webtransport_conformance.md.
- name: WebTransport conformance (Zig client)
# A bash script that has not been run on Windows yet.
if: runner.os != 'Windows'
run: ./tools/wt_conformance.sh --zig-only

# RFC 6455 conformance: the Autobahn fuzzing client against ws-echo-server,
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.claude
zig-out
.zig-cache
zig-pkg
.DS_Store
.vscode
interop/quiche/target
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ need a design decision. Thanks @notramo for suggesting it
@wallace0409 ([#45](https://github.com/endel/quic-zig/issues/45)).
- A peer that acknowledged nothing could run the server out of memory. Thanks
@wallace0409 ([#46](https://github.com/endel/quic-zig/issues/46)).
- Windows. The library, its tests and the apps build and run natively: sockets
on Winsock, the event loop on libxev's IOCP backend. ECN is not read or set
on Windows yet. See [SPEC/STATUS.md](SPEC/STATUS.md#platforms).

## 0.5.0

Expand Down
42 changes: 42 additions & 0 deletions SPEC/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,48 @@ Track at: https://datatracker.ietf.org/doc/draft-ietf-quic-multipath/

---

## Platforms

Every call into the OS goes through [`src/sys.zig`](../src/sys.zig); Windows
lives in [`src/sys/windows.zig`](../src/sys/windows.zig).

| Platform | Event loop | Tested |
|---|---|---|
| Linux | libxev epoll | CI |
| macOS | libxev kqueue | CI |
| Windows | libxev IOCP, readiness through AFD poll | CI |

### Windows caveats

- **libxev.** The event loop waits for readiness (`xev.File.poll`), which
upstream libxev's IOCP backend does not have. `build.zig.zon` pins a fork
that adds it by polling through the AFD driver, the mechanism beneath
`WSAPoll`, so the result completes on the loop's port.
- **ECN** is neither read nor set: receive is plain `recvfrom`, with no
`WSARecvMsg` control data, and marking is a no-op. ECN validation sees an
unmarked path.
- **No batching.** One `sendto` per datagram, and no UDP send/receive offload.
- **Timer resolution.** Timed waits round up to the system tick, about 15.6 ms.
The event loop's timers wait on the completion port in milliseconds and share
the tick. `sys.sleepNs` under a millisecond yields in a loop instead, so it
stays accurate and the manual-loop apps (`bench`, `*_manual`) measure the
stack, at the cost of a busy core while they wait.
- **ICMP resets are off.** Winsock fails a UDP socket's next receive after an
ICMP port-unreachable (`WSAECONNRESET`), so one departed client could break a
server's receive loop. `sys.udpSocket` turns that off.
- **`SO_REUSEADDR` is not set**: on Windows it lets a second socket take a port
that is still in use.
- **No Unix sockets or `SO_REUSEPORT`.** `Http1Config.unix_path` fails with
`error.UnixSocketsUnsupported`, and `reuse_port` with an error from `setReusePort`.
- **An HTTP/1.1 connection can't move between loops.** A socket stays on its
first completion port, so `Socket.rebind` onto another loop isn't supported.
- **A nonblocking `send` can take all of a large write.** Backpressure shows on
the next one, so `high_water` is only reached a write later than on POSIX.
- The interop servers find their preferred address with `getifaddrs`, which is
POSIX; on Windows they advertise none.

---

## Overall Progress

| RFC | Done | Partial | Missing | Completion |
Expand Down
6 changes: 3 additions & 3 deletions apps/bench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ fn runBench(alloc: std.mem.Allocator, config: BenchConfig) !void {
.skip_cert_verify = true,
};

const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET, .{});
defer sys.close(sockfd);
const local_addr = try net.Address.parseIp4("127.0.0.1", 0);
try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen());
Expand Down Expand Up @@ -282,7 +282,7 @@ fn runBench(alloc: std.mem.Allocator, config: BenchConfig) !void {

var conn_idx: u32 = 0;
while (conn_idx < config.num_connections) : (conn_idx += 1) {
const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET, .{});
defer sys.close(sockfd);
const local_addr = try net.Address.parseIp4("127.0.0.1", 0);
try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen());
Expand Down Expand Up @@ -367,7 +367,7 @@ pub fn main(init: std.process.Init.Minimal) !void {

var config = BenchConfig{};

var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand Down
4 changes: 2 additions & 2 deletions apps/bench_wt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fn percentile(sorted: []const i64, p: f64) i64 {
// ════════════════════════════════════════════════════════

fn createSocket() !struct { fd: posix.socket_t, addr: net.Address } {
const fd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const fd = try sys.udpSocket(posix.AF.INET, .{});
const addr = try net.Address.parseIp4("127.0.0.1", 0);
try sys.bind(fd, &addr.any, addr.getOsSockLen());
ecn_socket.enableEcnRecv(fd) catch {};
Expand Down Expand Up @@ -442,7 +442,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
const alloc = arena.allocator();

var config = Config{};
var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand Down
2 changes: 1 addition & 1 deletion apps/bench_wt_server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
var port: u16 = 4434;
var cert_path: []const u8 = "interop/certs/server.crt";
var key_path: []const u8 = "interop/certs/server.key";
var args = std.process.Args.Iterator.init(init.args);
var args = quic.sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand Down
4 changes: 2 additions & 2 deletions apps/client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn main(init: std.process.Init.Minimal) !void {

// Parse --port argument
var port: u16 = 4434;
var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next(); // skip program name
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand All @@ -56,7 +56,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
}

const server_addr = try net.Address.parseIp4("127.0.0.1", port);
const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET, .{});
defer sys.close(sockfd);

// Create a local socket with any available port
Expand Down
2 changes: 1 addition & 1 deletion apps/h3_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
var port: u16 = 4434;
var path: []const u8 = "/";
var insecure = false;
var args = std.process.Args.Iterator.init(init.args);
var args = quic.sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand Down
13 changes: 3 additions & 10 deletions apps/interop_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
defer if (keylog_file) |f| f.close();

// Parse request URLs from CLI args
var args_iter = std.process.Args.Iterator.init(init.args);
var args_iter = sys.argsIterator(init.args);
_ = args_iter.next(); // skip program name
var urls: std.ArrayList(ParsedUrl) = .empty;
while (args_iter.next()) |arg| {
Expand Down Expand Up @@ -275,12 +275,8 @@ fn downloadAll(

// Always create IPv6 dual-stack socket to support preferred_address migration across families.
// If the initial server address is IPv4, sendto converts it to IPv4-mapped IPv6 automatically.
const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET6, .{});
defer sys.close(sockfd);
// Disable IPV6_V6ONLY to allow dual-stack (IPv4 and IPv6 on same socket)
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {};

const local_addr = try net.Address.parseIp6("::", 0);
try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen());
Expand Down Expand Up @@ -589,11 +585,8 @@ fn downloadH0(
// Switch DCID before migrating (RFC 9000 §9.5)
if (conn.initiateClientMigration()) {
// Create a new socket bound to a different port
const new_sockfd = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch null;
const new_sockfd = sys.udpSocket(posix.AF.INET6, .{}) catch null;
if (new_sockfd) |nfd| {
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(nfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {};
const new_local = net.Address.parseIp6("::", 0) catch unreachable;
sys.bind(nfd, &new_local.any, new_local.getOsSockLen()) catch {
sys.close(nfd);
Expand Down
8 changes: 2 additions & 6 deletions apps/interop_client_manual.zig
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
defer if (keylog_file) |f| f.close();

// Parse request URLs from CLI args
var args_iter = std.process.Args.Iterator.init(init.args);
var args_iter = sys.argsIterator(init.args);
_ = args_iter.next(); // skip program name
var urls: std.ArrayList(ParsedUrl) = .empty;
while (args_iter.next()) |arg| {
Expand Down Expand Up @@ -226,12 +226,8 @@ fn downloadAll(

// Always create IPv6 dual-stack socket to support preferred_address migration across families.
// If the initial server address is IPv4, sendto converts it to IPv4-mapped IPv6 automatically.
const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET6, .{});
defer sys.close(sockfd);
// Disable IPV6_V6ONLY to allow dual-stack (IPv4 and IPv6 on same socket)
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {};

const local_addr = try net.Address.parseIp6("::", 0);
try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen());
Expand Down
4 changes: 4 additions & 0 deletions apps/interop_server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 {

/// Discover the server's non-loopback IPv4 and IPv6 addresses from network interfaces.
fn getServerAddresses() struct { ipv4: ?[4]u8, ipv6: ?[16]u8 } {
// getifaddrs is POSIX, and the interop runner only runs this in a Linux
// container. Elsewhere it finds nothing.
if (@import("builtin").os.tag == .windows) return .{ .ipv4 = null, .ipv6 = null };

const IfAddrs = extern struct {
ifa_next: ?*@This(),
ifa_name: [*:0]const u8,
Expand Down
44 changes: 16 additions & 28 deletions apps/interop_server_manual.zig
Original file line number Diff line number Diff line change
Expand Up @@ -140,34 +140,18 @@ pub fn main() !void {
// Create UDP socket (dual-stack: try IPv6 first, fall back to IPv4)
const listen_port: u16 = std.fmt.parseInt(u16, port_str, 10) catch 443;
const sockfd, const local_addr = blk: {
// Try IPv6 dual-stack socket first (handles both IPv4 and IPv6)
const addr6 = try net.Address.parseIp6("::", listen_port);
const fd6 = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch {
// Fall back to IPv4-only
const addr4 = try net.Address.parseIp4("0.0.0.0", listen_port);
const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch {
sys.close(fd4);
return error.BindFailed;
};
break :blk .{ fd4, addr4 };
};
// Allow dual-stack (disable IPV6_V6ONLY)
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(fd6, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {};
sys.bind(fd6, &addr6.any, addr6.getOsSockLen()) catch {
sys.close(fd6);
// Fall back to IPv4-only
const addr4 = try net.Address.parseIp4("0.0.0.0", listen_port);
const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch {
sys.close(fd4);
return error.BindFailed;
for ([_]net.Address{
try net.Address.parseIp6("::", listen_port),
try net.Address.parseIp4("0.0.0.0", listen_port),
}) |addr| {
const fd = sys.udpSocket(addr.any.family, .{}) catch continue;
sys.bind(fd, &addr.any, addr.getOsSockLen()) catch {
sys.close(fd);
continue;
};
break :blk .{ fd4, addr4 };
};
break :blk .{ fd6, addr6 };
break :blk .{ fd, addr };
}
return error.BindFailed;
};
defer sys.close(sockfd);
ecn_socket.enableEcnRecv(sockfd) catch {};
Expand Down Expand Up @@ -403,8 +387,12 @@ fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 {
}

/// Discover the server's non-loopback IPv4 and IPv6 addresses from network interfaces.
/// Uses C getifaddrs() which works on Linux (Docker containers).
/// Uses C getifaddrs() which works on Linux (Docker containers). Elsewhere it
/// finds nothing: getifaddrs is POSIX, and the interop runner never runs this
/// on Windows.
fn getServerAddresses() struct { ipv4: ?[4]u8, ipv6: ?[16]u8 } {
if (@import("builtin").os.tag == .windows) return .{ .ipv4 = null, .ipv6 = null };

const IfAddrs = extern struct {
ifa_next: ?*@This(),
ifa_name: [*:0]const u8,
Expand Down
7 changes: 2 additions & 5 deletions apps/interop_wt_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
}

// Parse request URLs from CLI args
var args_iter = std.process.Args.Iterator.init(init.args);
var args_iter = sys.argsIterator(init.args);
_ = args_iter.next(); // skip program name
var urls: std.ArrayList(ParsedUrl) = .empty;
while (args_iter.next()) |arg| {
Expand Down Expand Up @@ -239,11 +239,8 @@ pub fn main(init: std.process.Init.Minimal) !void {
};

// Create dual-stack UDP socket
const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
const sockfd = try sys.udpSocket(posix.AF.INET6, .{});
defer sys.close(sockfd);
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, mem.asBytes(&zero)) catch {};
const local_addr = try net.Address.parseIp6("::", 0);
try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen());
ecn_socket.enableEcnRecv(sockfd) catch {};
Expand Down
36 changes: 12 additions & 24 deletions apps/interop_wt_server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -200,32 +200,20 @@ pub fn main(_: std.process.Init.Minimal) !void {
.keylog_file = keylog_file,
};

// Create UDP socket (dual-stack)
// Create UDP socket: IPv6 dual-stack, or IPv4 where IPv6 is unavailable
const sockfd, const local_addr = blk: {
const addr6 = try net.Address.parseIp6("::", 443);
const fd6 = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch {
const addr4 = try net.Address.parseIp4("0.0.0.0", 443);
const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch {
sys.close(fd4);
return error.BindFailed;
};
break :blk .{ fd4, addr4 };
};
const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27;
const zero: c_int = 0;
posix.setsockopt(fd6, posix.IPPROTO.IPV6, IPV6_V6ONLY, mem.asBytes(&zero)) catch {};
sys.bind(fd6, &addr6.any, addr6.getOsSockLen()) catch {
sys.close(fd6);
const addr4 = try net.Address.parseIp4("0.0.0.0", 443);
const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0);
sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch {
sys.close(fd4);
return error.BindFailed;
for ([_]net.Address{
try net.Address.parseIp6("::", 443),
try net.Address.parseIp4("0.0.0.0", 443),
}) |addr| {
const fd = sys.udpSocket(addr.any.family, .{}) catch continue;
sys.bind(fd, &addr.any, addr.getOsSockLen()) catch {
sys.close(fd);
continue;
};
break :blk .{ fd4, addr4 };
};
break :blk .{ fd6, addr6 };
break :blk .{ fd, addr };
}
return error.BindFailed;
};
defer sys.close(sockfd);
ecn_socket.enableEcnRecv(sockfd) catch {};
Expand Down
2 changes: 1 addition & 1 deletion apps/moq_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
var track_names = std.ArrayList([]const u8).empty;
defer track_names.deinit(alloc);

var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--addr")) {
Expand Down
2 changes: 1 addition & 1 deletion apps/moq_lite.zig
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ pub fn main(init: std.process.Init.Minimal) !u8 {
var cert_path: []const u8 = "interop/certs/server.crt";
var key_path: []const u8 = "interop/certs/server.key";

var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "publish")) {
Expand Down
2 changes: 1 addition & 1 deletion apps/moq_lite_relay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
var cert_path: []const u8 = "interop/browser/certs/server.crt";
var key_path: []const u8 = "interop/browser/certs/server.key";

var args = std.process.Args.Iterator.init(init.args);
var args = sys.argsIterator(init.args);
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--port")) {
Expand Down
Loading
Loading