a73x

b34e7a1a

refactor: src/quic.zig owns the shared QUIC vocabulary; quic_server is just the listener

a73x   2026-08-12 18:04

Commit message
refactor: src/quic.zig owns the shared QUIC vocabulary; quic_server is just the listener

build.zig named the module `quic` but pointed it at quic_server.zig, so the
listener file doubled as the vocabulary both binaries import. Split them: the
@cImport, Key, the wire constants, Egress/WriteAction/accountWrite,
timestampNs/keepAliveNs and the two shared ngtcp2 callbacks move verbatim into
src/quic.zig, along with the eight tests that pin them. quic_server.zig keeps
the Listener and its scaffolding and imports the rest.

The cImport stays in exactly ONE file, which is the whole constraint: two
blocks over the same headers are two distinct type universes and `ngtcp2_vec`s
cross between listener and client.

Also spells the ngtcp2_path literal once. Six call sites built it by hand —
four in the listener file, two in the client — and pathFrom replaces all six.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -96,14 +96,27 @@ pub fn build(b: *std.Build) void {
96 .link_libc = true, 96 .link_libc = true,
97 }); 97 });
98 98
99 // The QUIC listener: std + the vendored C stack, and deliberately no 99 // The QUIC vocabulary both ends share: the one @cImport of the vendored
100 // protocol import — it carries opaque bytes, exactly as proxy.zig does. 100 // stack, the key, the wire constants, the egress ring. It has to be ONE
101 // module — two @cImport blocks over the same headers are two distinct
102 // type universes, and `ngtcp2_vec`s cross between listener and client.
101 const quic_mod = b.createModule(.{ 103 const quic_mod = b.createModule(.{
104 .root_source_file = b.path("src/quic.zig"),
105 .target = target,
106 .optimize = optimize,
107 .link_libc = true,
108 });
109
110 // The QUIC listener: the vocabulary plus a UDP socket and a connection
111 // table, and deliberately no protocol import — it carries opaque bytes,
112 // exactly as proxy.zig does.
113 const quic_server_mod = b.createModule(.{
102 .root_source_file = b.path("src/quic_server.zig"), 114 .root_source_file = b.path("src/quic_server.zig"),
103 .target = target, 115 .target = target,
104 .optimize = optimize, 116 .optimize = optimize,
105 .link_libc = true, 117 .link_libc = true,
106 }); 118 });
119 quic_server_mod.addImport("quic", quic_mod);
107 120
108 // Speculative local echo: the overlay and its policy, and deliberately 121 // Speculative local echo: the overlay and its policy, and deliberately
109 // nothing else. No engine import, which is what lets the whole state 122 // nothing else. No engine import, which is what lets the whole state
@@ -169,7 +182,10 @@ pub fn build(b: *std.Build) void {
169 server_mod.addImport("engine", engine_mod); 182 server_mod.addImport("engine", engine_mod);
170 server_mod.addImport("pty", pty_mod); 183 server_mod.addImport("pty", pty_mod);
171 server_mod.addImport("protocol", protocol_mod); 184 server_mod.addImport("protocol", protocol_mod);
185 // Both: the listener it owns, and the vocabulary it names directly
186 // (the key it loads, the idle default it falls back to).
172 server_mod.addImport("quic", quic_mod); 187 server_mod.addImport("quic", quic_mod);
188 server_mod.addImport("quic_server", quic_server_mod);
173 server_mod.addImport("testtmp", testtmp_mod); 189 server_mod.addImport("testtmp", testtmp_mod);
174 // For endpoint_req's lazy bind: the default key path, resolved by the 190 // For endpoint_req's lazy bind: the default key path, resolved by the
175 // daemon itself when nobody handed it a --key. 191 // daemon itself when nobody handed it a --key.
@@ -289,8 +305,9 @@ pub fn build(b: *std.Build) void {
289 exe_mod.addImport("protocol", protocol_mod); 305 exe_mod.addImport("protocol", protocol_mod);
290 exe_mod.addImport("proxy", proxy_mod); 306 exe_mod.addImport("proxy", proxy_mod);
291 // The daemon entrypoint loads the key and constructs the listener, so it 307 // The daemon entrypoint loads the key and constructs the listener, so it
292 // needs the module directly rather than through the server. 308 // needs the modules directly rather than through the server.
293 exe_mod.addImport("quic", quic_mod); 309 exe_mod.addImport("quic", quic_mod);
310 exe_mod.addImport("quic_server", quic_server_mod);
294 exe_mod.addImport("build_options", version_opts.createModule()); 311 exe_mod.addImport("build_options", version_opts.createModule());
295 exe_mod.addImport("xdg", xdg_mod); 312 exe_mod.addImport("xdg", xdg_mod);
296 // The keygen round-trip test needs a directory to generate into; the 313 // The keygen round-trip test needs a directory to generate into; the
@@ -343,7 +360,7 @@ pub fn build(b: *std.Build) void {
343 // absence here was a live hazard recorded in decisions.md — muxd's 360 // absence here was a live hazard recorded in decisions.md — muxd's
344 // entrypoint could grow tests that silently never ran, exactly as 361 // entrypoint could grow tests that silently never ran, exactly as
345 // mux_main.zig's five did before it was added. 362 // mux_main.zig's five did before it was added.
346 for ([_]*std.Build.Module{ protocol_mod, engine_mod, pty_mod, server_mod, client_mod, proxy_mod, mux_mod, quic_mod, exe_mod, testtmp_mod, quic_client_mod, predict_mod, rawmode_mod, delaypipe_mod, xdg_mod, spawn_mod, handoff_mod, paint_mod, render_mod, ptyclient_mod }) |mod| { 363 for ([_]*std.Build.Module{ protocol_mod, engine_mod, pty_mod, server_mod, client_mod, proxy_mod, mux_mod, quic_mod, quic_server_mod, exe_mod, testtmp_mod, quic_client_mod, predict_mod, rawmode_mod, delaypipe_mod, xdg_mod, spawn_mod, handoff_mod, paint_mod, render_mod, ptyclient_mod }) |mod| {
347 const t = b.addTest(.{ .root_module = mod }); 364 const t = b.addTest(.{ .root_module = mod });
348 t.use_llvm = true; 365 t.use_llvm = true;
349 t.use_lld = true; 366 t.use_lld = true;
@@ -353,8 +370,9 @@ pub fn build(b: *std.Build) void {
353 // deps when they are absent. Without it the dependency reached only 370 // deps when they are absent. Without it the dependency reached only
354 // `muxd`, and a clean checkout running `make test` first would have 371 // `muxd`, and a clean checkout running `make test` first would have
355 // found no libraries and no explanation. 372 // found no libraries and no explanation.
356 if (mod == server_mod or mod == quic_mod or mod == exe_mod or 373 if (mod == server_mod or mod == quic_mod or mod == quic_server_mod or
357 mod == client_mod or mod == mux_mod or mod == quic_client_mod) linkQuic(b, t, quic); 374 mod == exe_mod or mod == client_mod or mod == mux_mod or
375 mod == quic_client_mod) linkQuic(b, t, quic);
358 test_step.dependOn(&b.addRunArtifact(t).step); 376 test_step.dependOn(&b.addRunArtifact(t).step);
359 } 377 }
360 378
src/main.zig
Old New
@@ -6,6 +6,7 @@ const Server = @import("server").Server;
6 const proto = @import("protocol"); 6 const proto = @import("protocol");
7 const proxy = @import("proxy"); 7 const proxy = @import("proxy");
8 const quic = @import("quic"); 8 const quic = @import("quic");
9 const quic_server = @import("quic_server");
9 const build_options = @import("build_options"); 10 const build_options = @import("build_options");
10 const xdg = @import("xdg"); 11 const xdg = @import("xdg");
11 const spawn = @import("spawn"); 12 const spawn = @import("spawn");
@@ -341,9 +342,9 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
341 // already taken costs nothing: no shell has been started and no socket 342 // already taken costs nothing: no shell has been started and no socket
342 // left on disk. It is the same discipline as loading the key first, one 343 // left on disk. It is the same discipline as loading the key first, one
343 // syscall further along. 344 // syscall further along.
344 var listener: ?*quic.Listener = null; 345 var listener: ?*quic_server.Listener = null;
345 if (quic_bind) |addr| { 346 if (quic_bind) |addr| {
346 listener = quic.Listener.bind(alloc, addr, quic_key, o.quic_idle_ms) catch |err| switch (err) { 347 listener = quic_server.Listener.bind(alloc, addr, quic_key, o.quic_idle_ms) catch |err| switch (err) {
347 // The QUIC edition of "a daemon is already running", refused for 348 // The QUIC edition of "a daemon is already running", refused for
348 // the same reason: the listener sets no SO_REUSEADDR, so rather 349 // the same reason: the listener sets no SO_REUSEADDR, so rather
349 // than silently splitting a port's datagrams with the daemon 350 // than silently splitting a port's datagrams with the daemon
src/quic.zig
Old New
@@ -0,0 +1,472 @@
1 //! The QUIC vocabulary both ends share: the C import, the pre-shared key,
2 //! the wire constants, and the egress ring's lifetime discipline.
3 //!
4 //! Nothing here knows what a listener or a connection is. quic_server.zig
5 //! owns the listener, quic_client.zig owns the client transport, and both
6 //! import this — which is what makes "the two ends agree" a fact about one
7 //! file rather than a convention two files are trusted to keep.
8 const std = @import("std");
9
10 /// The C view of the QUIC stack. Exported because the client transport
11 /// shares it: two @cImport blocks over the same headers produce two
12 /// *distinct* Zig types, so a client with its own would find that
13 /// `ngtcp2_vec` is not `ngtcp2_vec`. One import, one type universe.
14 pub const c = @cImport({
15 @cInclude("ngtcp2/ngtcp2.h");
16 @cInclude("ngtcp2/ngtcp2_crypto.h");
17 @cInclude("ngtcp2/ngtcp2_crypto_wolfssl.h");
18 @cInclude("wolfssl/options.h");
19 @cInclude("wolfssl/ssl.h");
20 });
21
22 pub const key_len = 32;
23
24 /// mux's conventional QUIC port. Both parsers reach for it when the user
25 /// names no port; it lives here because this module is the one thing both
26 /// binaries already import.
27 pub const default_port: u16 = 4433;
28
29 /// How long a QUIC connection tolerates silence before declaring the peer
30 /// gone. Long enough that a quiet terminal is not a suspicious one, short
31 /// enough that a client which has genuinely vanished stops being served
32 /// within a few seconds of keepalives failing — keepalives run at a third
33 /// of it, so an idle session is never the thing that trips it. Tunable on
34 /// both binaries because the reconnect tests need death declared on a
35 /// schedule they can wait for.
36 ///
37 /// Here for the same reason as `default_port`: two copies of a number both
38 /// binaries default to are two numbers, and they drift in silence.
39 pub const default_idle_ms: u32 = 15_000;
40
41 /// The pre-shared key, and the rules for getting one off disk.
42 ///
43 /// A key file is exactly as sensitive as an ssh private key, so it is held
44 /// to the same standard: readable by nobody but its owner. Refusing is the
45 /// whole point — a daemon that starts anyway with a world-readable key has
46 /// authenticated nothing, and would do it silently.
47 pub const Key = struct {
48 bytes: [key_len]u8,
49
50 pub const LoadError = error{
51 KeyFileMissing,
52 KeyFilePermissive,
53 KeyFileMalformed,
54 };
55
56 /// Accepts either 32 raw bytes or 64 hex characters (trailing
57 /// whitespace ignored, so `xxd -p` and a text editor both work).
58 pub fn load(path: []const u8) !Key {
59 const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
60 error.FileNotFound => return error.KeyFileMissing,
61 else => return err,
62 };
63 defer file.close();
64
65 const st = try file.stat();
66 // Group or other bits set = refuse, exactly as ssh does for a
67 // private key. Checked before the contents are read, so a bad-mode
68 // key is never even loaded into memory.
69 if (st.mode & 0o077 != 0) return error.KeyFilePermissive;
70
71 var buf: [128]u8 = undefined;
72 const n = try file.readAll(&buf);
73
74 // A file of exactly 32 bytes is a raw key, taken WITHOUT trimming.
75 // Trimming first looks harmless and is not: a random key ends in one
76 // of " \t\r\n" about one time in 64, and such keys were silently
77 // shortened to 31 bytes and rejected as malformed — so
78 // `head -c 32 /dev/urandom > k` failed for a few percent of the keys
79 // it generated. Rare, random, and looking like the user's fault,
80 // which is the worst shape a bug can have. Found by review probe.
81 if (n == key_len) {
82 var k: Key = undefined;
83 @memcpy(&k.bytes, buf[0..key_len]);
84 return k;
85 }
86
87 // Anything else may carry a text editor's trailing newline: trim
88 // and retry, which is the case the trimming actually exists for.
89 const raw = std.mem.trimRight(u8, buf[0..n], " \t\r\n");
90 if (raw.len == key_len) {
91 var k: Key = undefined;
92 @memcpy(&k.bytes, raw);
93 return k;
94 }
95 if (raw.len == key_len * 2) {
96 var k: Key = undefined;
97 _ = std.fmt.hexToBytes(&k.bytes, raw) catch return error.KeyFileMalformed;
98 return k;
99 }
100 return error.KeyFileMalformed;
101 }
102 };
103
104 /// Test helper: chmod a file inside a Dir. `Dir.chmod` applies to the
105 /// directory itself, not to an entry in it.
106 fn chmodAt(dir: std.fs.Dir, sub: []const u8, mode: std.posix.mode_t) !void {
107 const f = try dir.openFile(sub, .{});
108 defer f.close();
109 try f.chmod(mode);
110 }
111
112 test "Key.load: accepts 32 raw bytes and 64 hex chars, owner-only" {
113 var tmp = std.testing.tmpDir(.{});
114 defer tmp.cleanup();
115
116 const raw = [_]u8{0xAB} ** key_len;
117 try tmp.dir.writeFile(.{ .sub_path = "raw.key", .data = &raw });
118 try chmodAt(tmp.dir, "raw.key", 0o600);
119
120 var hex: [key_len * 2]u8 = undefined;
121 _ = try std.fmt.bufPrint(&hex, "{x}", .{&raw});
122 // A key pasted by a human ends in a newline; that must not change it.
123 try tmp.dir.writeFile(.{ .sub_path = "hex.key", .data = hex ++ "\n" });
124 try chmodAt(tmp.dir, "hex.key", 0o600);
125
126 var path_buf: [256]u8 = undefined;
127 const dir = try tmp.dir.realpath(".", &path_buf);
128 var jb: [512]u8 = undefined;
129
130 const from_raw = try Key.load(try std.fmt.bufPrint(&jb, "{s}/raw.key", .{dir}));
131 try std.testing.expectEqualSlices(u8, &raw, &from_raw.bytes);
132
133 const from_hex = try Key.load(try std.fmt.bufPrint(&jb, "{s}/hex.key", .{dir}));
134 try std.testing.expectEqualSlices(u8, &raw, &from_hex.bytes);
135 }
136
137 test "Key.load: a raw key whose last byte is whitespace is still a key" {
138 var tmp = std.testing.tmpDir(.{});
139 defer tmp.cleanup();
140 var path_buf: [256]u8 = undefined;
141 const dir = try tmp.dir.realpath(".", &path_buf);
142 var jb: [512]u8 = undefined;
143
144 // Found by a review probe, not by me. Kept because the probe was
145 // transient: a bug with no test is a bug with a return ticket.
146 inline for (.{ '\n', '\r', '\t', ' ' }) |last| {
147 var raw = [_]u8{0x7F} ** key_len;
148 raw[key_len - 1] = last;
149 try tmp.dir.writeFile(.{ .sub_path = "ws.key", .data = &raw });
150 try chmodAt(tmp.dir, "ws.key", 0o600);
151 const k = try Key.load(try std.fmt.bufPrint(&jb, "{s}/ws.key", .{dir}));
152 try std.testing.expectEqualSlices(u8, &raw, &k.bytes);
153 }
154
155 // ...while 32 key bytes plus an editor's newline (33 on disk) still
156 // loads, which is what the trimming is for.
157 const clean = [_]u8{0x42} ** key_len;
158 try tmp.dir.writeFile(.{ .sub_path = "nl.key", .data = clean ++ "\n" });
159 try chmodAt(tmp.dir, "nl.key", 0o600);
160 const k2 = try Key.load(try std.fmt.bufPrint(&jb, "{s}/nl.key", .{dir}));
161 try std.testing.expectEqualSlices(u8, &clean, &k2.bytes);
162 }
163
164 test "Key.load: refuses a permissive mode, a missing file, and a bad length" {
165 var tmp = std.testing.tmpDir(.{});
166 defer tmp.cleanup();
167 var path_buf: [256]u8 = undefined;
168 const dir = try tmp.dir.realpath(".", &path_buf);
169 var jb: [512]u8 = undefined;
170
171 const raw = [_]u8{0xCD} ** key_len;
172
173 // Group-readable: refused, like an ssh private key.
174 try tmp.dir.writeFile(.{ .sub_path = "group.key", .data = &raw });
175 try chmodAt(tmp.dir, "group.key", 0o640);
176 try std.testing.expectError(
177 error.KeyFilePermissive,
178 Key.load(try std.fmt.bufPrint(&jb, "{s}/group.key", .{dir})),
179 );
180
181 // World-readable: same.
182 try tmp.dir.writeFile(.{ .sub_path = "world.key", .data = &raw });
183 try chmodAt(tmp.dir, "world.key", 0o604);
184 try std.testing.expectError(
185 error.KeyFilePermissive,
186 Key.load(try std.fmt.bufPrint(&jb, "{s}/world.key", .{dir})),
187 );
188
189 try std.testing.expectError(
190 error.KeyFileMissing,
191 Key.load(try std.fmt.bufPrint(&jb, "{s}/nope.key", .{dir})),
192 );
193
194 // Right mode, wrong content: neither 32 raw nor 64 hex.
195 try tmp.dir.writeFile(.{ .sub_path = "short.key", .data = "too short" });
196 try chmodAt(tmp.dir, "short.key", 0o600);
197 try std.testing.expectError(
198 error.KeyFileMalformed,
199 Key.load(try std.fmt.bufPrint(&jb, "{s}/short.key", .{dir})),
200 );
201
202 // 64 characters, but not hex.
203 try tmp.dir.writeFile(.{ .sub_path = "nothex.key", .data = "z" ** 64 });
204 try chmodAt(tmp.dir, "nothex.key", 0o600);
205 try std.testing.expectError(
206 error.KeyFileMalformed,
207 Key.load(try std.fmt.bufPrint(&jb, "{s}/nothex.key", .{dir})),
208 );
209 }
210
211 pub const max_udp = 1452; // conservative IPv4 datagram that avoids fragmenting
212
213 pub const psk_identity: [*:0]const u8 = "mux";
214 pub const psk_ciphersuite: [*:0]const u8 = "TLS13-AES128-GCM-SHA256";
215 pub const alpn = "\x03mux";
216
217 /// How many outbound bytes one connection may hold. Sized to the stream
218 /// window the peer advertises, because holding much more than the peer will
219 /// let us send buys nothing: past this the daemon's own `pending_cap` is the
220 /// right place for the backlog to sit and be judged.
221 pub const egress_cap = 256 * 1024;
222
223 /// Outbound stream bytes, in a ring that never moves a byte once written.
224 ///
225 /// A ring rather than a growable buffer, because ngtcp2 does NOT copy stream
226 /// payload: `ngtcp2_conn_writev_stream` stores the *vector* it is handed —
227 /// `ngtcp2_vec_copy` is a memcpy of base+len, not of the bytes — in its
228 /// retransmission queue, and re-reads those bytes if the packet is lost. A
229 /// buffer that reallocates on append, or that clears once the last byte has
230 /// been handed over, therefore leaves ngtcp2 holding a freed or recycled
231 /// pointer, and it dies inside `ngtcp2_pkt_encode_stream_frame`. It did:
232 /// rarely, only under loss, and only in the one test big enough to overflow
233 /// a socket buffer — which is the worst shape a bug can have.
234 ///
235 /// So the invariant, and everything here exists to hold it: **a byte handed
236 /// to ngtcp2 does not move or get overwritten until the peer acknowledges
237 /// it.** `head` advances only from `acked_stream_data_offset`; a write can
238 /// only land in the free space that leaves. Being fixed in size is the other
239 /// half of the point — a full ring is the backpressure signal.
240 pub const Egress = struct {
241 buf: []u8,
242 /// The oldest byte the peer has not acknowledged.
243 head: usize = 0,
244 /// Bytes from `head` still owed: handed to ngtcp2 and unacknowledged,
245 /// plus not yet handed over.
246 held: usize = 0,
247 /// The tail of `held` that ngtcp2 has not taken yet.
248 unsent: usize = 0,
249
250 pub fn deinit(self: *Egress, alloc: std.mem.Allocator) void {
251 alloc.free(self.buf);
252 self.* = .{ .buf = &.{} };
253 }
254
255 pub fn freeSpace(self: *const Egress) usize {
256 return self.buf.len - self.held;
257 }
258
259 /// Take what fits and report how much that was. A short return is not an
260 /// error, it is the whole mechanism: the caller keeps the remainder and
261 /// is thereby the one holding — and bounding — the backlog.
262 pub fn push(self: *Egress, bytes: []const u8) usize {
263 const n = @min(bytes.len, self.freeSpace());
264 if (n == 0) return 0;
265 const start = (self.head + self.held) % self.buf.len;
266 const first = @min(n, self.buf.len - start);
267 @memcpy(self.buf[start..][0..first], bytes[0..first]);
268 if (first < n) @memcpy(self.buf[0 .. n - first], bytes[first..n]);
269 self.held += n;
270 self.unsent += n;
271 return n;
272 }
273
274 /// The unsent region as up to two vectors — two when it wraps, which is
275 /// the price of never moving a byte, and ngtcp2 takes a vector array
276 /// precisely so that price is payable.
277 pub fn vecs(self: *const Egress, out: *[2]c.ngtcp2_vec) usize {
278 if (self.unsent == 0) return 0;
279 const start = (self.head + (self.held - self.unsent)) % self.buf.len;
280 const first = @min(self.unsent, self.buf.len - start);
281 out[0] = .{ .base = self.buf.ptr + start, .len = first };
282 if (first == self.unsent) return 1;
283 out[1] = .{ .base = self.buf.ptr, .len = self.unsent - first };
284 return 2;
285 }
286
287 /// ngtcp2 took `n` bytes off the unsent region. They stay exactly where
288 /// they are — it now has pointers to them.
289 pub fn took(self: *Egress, n: usize) void {
290 self.unsent -= @min(n, self.unsent);
291 }
292
293 /// The peer acknowledged `n` more bytes. ngtcp2 documents this callback
294 /// as arriving "sequentially in increasing order of offset without any
295 /// overlap", so a running count IS the acknowledged prefix, and this is
296 /// the only thing that ever frees space.
297 pub fn ack(self: *Egress, n: usize) void {
298 // A deinit'd ring has a zero-length buffer, and the modulo below
299 // would divide by zero. Reachable because an ack can arrive for a
300 // connection whose egress has already been torn down.
301 if (self.buf.len == 0) return;
302 const taken = @min(n, self.held - self.unsent);
303 self.head = (self.head + taken) % self.buf.len;
304 self.held -= taken;
305 }
306 };
307
308 /// What one `writev_stream` return means for the egress ring — and, more to
309 /// the point, IN WHAT ORDER.
310 ///
311 /// The ordering is the whole reason this is a function rather than three
312 /// copies of two ifs. ngtcp2 can commit `ndatalen` — advancing the stream
313 /// offset it will retransmit from — and still return an error afterwards
314 /// (NOMEM out of rtb_add, say). Account for the bytes first or they stay
315 /// counted as unsent, get offered again at an offset the peer has moved
316 /// past, and the stream desynchronises somewhere far away from here.
317 pub const WriteAction = enum {
318 /// The call failed. Stop draining; the bytes are already accounted.
319 stop,
320 /// Nothing more to send right now.
321 brk,
322 /// A packet was produced; send it and go round again.
323 cont,
324 };
325
326 pub fn accountWrite(out: *Egress, wrote: c.ngtcp2_ssize, n: c.ngtcp2_ssize) WriteAction {
327 if (wrote > 0) out.took(@intCast(wrote));
328 if (n < 0) return .stop;
329 if (n == 0) return .brk;
330 return .cont;
331 }
332
333 test "Egress: a byte does not move until it is acked, and the ring wraps" {
334 const alloc = std.testing.allocator;
335 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
336 defer e.deinit(alloc);
337
338 try std.testing.expectEqual(@as(usize, 5), e.push("hello"));
339 var v: [2]c.ngtcp2_vec = undefined;
340 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
341 try std.testing.expectEqual(@as(usize, 5), v[0].len);
342 const base = e.buf.ptr;
343 try std.testing.expectEqual(base, v[0].base);
344
345 // ngtcp2 takes three. Those three now have a pointer pointing at them
346 // and must not move; the two behind them are still ours to offer.
347 e.took(3);
348 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
349 try std.testing.expectEqual(@as(usize, 2), v[0].len);
350 try std.testing.expectEqual(base + 3, v[0].base);
351
352 // Room is what is left after everything HELD, sent or not — the three
353 // in ngtcp2's hands are not free space just because they left the box.
354 try std.testing.expectEqual(@as(usize, 3), e.freeSpace());
355 try std.testing.expectEqual(@as(usize, 3), e.push("world"));
356 try std.testing.expectEqual(@as(usize, 0), e.push("x"));
357
358 // An acknowledgement is the only thing that frees anything.
359 e.ack(3);
360 try std.testing.expectEqual(@as(usize, 3), e.freeSpace());
361
362 // ...and the write that follows wraps rather than shifting a byte.
363 try std.testing.expectEqual(@as(usize, 3), e.push("abc"));
364 try std.testing.expectEqual(@as(usize, 2), e.vecs(&v));
365 try std.testing.expectEqualStrings("lowor", v[0].base[0..v[0].len]);
366 try std.testing.expectEqualStrings("abc", v[1].base[0..v[1].len]);
367 }
368
369 test "Egress: an ack can never free more than is outstanding" {
370 const alloc = std.testing.allocator;
371 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
372 defer e.deinit(alloc);
373
374 _ = e.push("abcd");
375 e.took(2);
376 // Two are in flight and two are still unsent. A callback claiming more
377 // than is outstanding must not consume the unsent ones — they have
378 // never been on the wire and cannot have been acknowledged.
379 e.ack(99);
380 try std.testing.expectEqual(@as(usize, 2), e.held);
381 try std.testing.expectEqual(@as(usize, 2), e.unsent);
382 var v: [2]c.ngtcp2_vec = undefined;
383 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
384 try std.testing.expectEqualStrings("cd", v[0].base[0..v[0].len]);
385 }
386
387 test "accountWrite: bytes ngtcp2 committed are accounted even when the call failed" {
388 const alloc = std.testing.allocator;
389 var e: Egress = .{ .buf = try alloc.alloc(u8, 16) };
390 defer e.deinit(alloc);
391 _ = e.push("abcdefgh");
392 try std.testing.expectEqual(@as(usize, 8), e.unsent);
393
394 // The case that has no fault injection available and would otherwise go
395 // untested: ngtcp2 committed four bytes of stream data — its offset has
396 // moved — and THEN returned an error. Accounting after the check would
397 // leave those four counted as unsent, so the next drain would offer them
398 // again at an offset the peer is already past.
399 try std.testing.expectEqual(WriteAction.stop, accountWrite(&e, 4, -1));
400 try std.testing.expectEqual(@as(usize, 4), e.unsent);
401
402 // The ordinary returns, for completeness of the contract.
403 try std.testing.expectEqual(WriteAction.brk, accountWrite(&e, 0, 0));
404 try std.testing.expectEqual(WriteAction.cont, accountWrite(&e, 4, 120));
405 try std.testing.expectEqual(@as(usize, 0), e.unsent);
406 }
407
408 test "Egress: an ack against a torn-down ring is ignored, not a division by zero" {
409 const alloc = std.testing.allocator;
410 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
411 _ = e.push("abcd");
412 e.took(4);
413
414 // Teardown leaves a zero-length buffer behind, and an ack can still
415 // arrive for it: ngtcp2 delivers acked_stream_data during its own
416 // shutdown, after the owner has freed the ring. The modulo in `ack`
417 // divides by buf.len, so without the guard this is a division by zero
418 // on a path nobody would think to look at.
419 e.deinit(alloc);
420 try std.testing.expectEqual(@as(usize, 0), e.buf.len);
421 e.ack(4);
422 try std.testing.expectEqual(@as(usize, 0), e.held);
423 }
424
425 pub fn timestampNs() u64 {
426 const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch return 0;
427 return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
428 }
429
430 /// A third of the idle timeout, so two keepalives can go unanswered before
431 /// the connection is called dead. Never zero: ngtcp2 reads a zero timeout as
432 /// "disabled", exactly as it reads UINT64_MAX, so a small idle_ms rounding
433 /// down would silently restore the behaviour the keepalive exists to prevent.
434 pub fn keepAliveNs(idle_ms: u64) u64 {
435 return @max(1, idle_ms / 3) * 1_000_000;
436 }
437
438 test "keepAlive: a third of the idle timeout, and never disabled" {
439 try std.testing.expectEqual(@as(u64, 5_000_000_000), keepAliveNs(15_000));
440 try std.testing.expectEqual(@as(u64, 500_000_000), keepAliveNs(1500));
441 // Zero would mean "no keepalive" to ngtcp2 — the opposite of what a
442 // small idle timeout is asking for.
443 try std.testing.expectEqual(@as(u64, 1_000_000), keepAliveNs(1));
444 try std.testing.expectEqual(@as(u64, 1_000_000), keepAliveNs(2));
445 }
446
447 pub fn randCb(dest: [*c]u8, destlen: usize, _: [*c]const c.ngtcp2_rand_ctx) callconv(.c) void {
448 std.crypto.random.bytes(dest[0..destlen]);
449 }
450
451 pub fn getNewCidCb(
452 _: ?*c.ngtcp2_conn,
453 cid: [*c]c.ngtcp2_cid,
454 token: [*c]c.ngtcp2_stateless_reset_token,
455 cidlen: usize,
456 _: ?*anyopaque,
457 ) callconv(.c) c_int {
458 std.crypto.random.bytes(cid.*.data[0..cidlen]);
459 cid.*.datalen = cidlen;
460 std.crypto.random.bytes(&token.*.data);
461 return 0;
462 }
463
464 /// The ngtcp2_path literal, spelled once. Six call sites built this
465 /// by hand and the seventh would have drifted.
466 pub fn pathFrom(local: anytype, local_len: c.socklen_t, remote: anytype, remote_len: c.socklen_t) c.ngtcp2_path {
467 return .{
468 .local = .{ .addr = @ptrCast(local), .addrlen = local_len },
469 .remote = .{ .addr = @ptrCast(remote), .addrlen = remote_len },
470 .user_data = null,
471 };
472 }
src/quic_client.zig
Old New
@@ -273,11 +273,7 @@ pub const Client = struct {
273 // has to see a dead transport on a schedule a test can wait for. 273 // has to see a dead transport on a schedule a test can wait for.
274 params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000; 274 params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000;
275 275
276 var path: c.ngtcp2_path = .{ 276 var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len);
277 .local = .{ .addr = @ptrCast(&self.local), .addrlen = self.local_len },
278 .remote = .{ .addr = @ptrCast(&self.remote), .addrlen = self.remote_len },
279 .user_data = null,
280 };
281 var conn: ?*c.ngtcp2_conn = null; 277 var conn: ?*c.ngtcp2_conn = null;
282 if (c.ngtcp2_conn_client_new_versioned( 278 if (c.ngtcp2_conn_client_new_versioned(
283 &conn, 279 &conn,
@@ -386,11 +382,7 @@ pub const Client = struct {
386 }; 382 };
387 if (n == 0) continue; 383 if (n == 0) continue;
388 const conn = self.conn orelse return; 384 const conn = self.conn orelse return;
389 var path: c.ngtcp2_path = .{ 385 var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len);
390 .local = .{ .addr = @ptrCast(&self.local), .addrlen = self.local_len },
391 .remote = .{ .addr = @ptrCast(&self.remote), .addrlen = self.remote_len },
392 .user_data = null,
393 };
394 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; 386 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 };
395 const rv = blk: { 387 const rv = blk: {
396 self.ngtcp2_depth += 1; 388 self.ngtcp2_depth += 1;
src/quic_server.zig
Old New
@@ -1,5 +1,7 @@
1 //! muxd's QUIC listener: one UDP socket, N authenticated connections, each 1 //! muxd's QUIC listener, and only the listener: one UDP socket, N
2 //! carrying exactly one bidirectional stream of opaque bytes. 2 //! authenticated connections, each carrying exactly one bidirectional stream
3 //! of opaque bytes. The vocabulary both ends share — the C import, the key,
4 //! the egress ring — is quic.zig's, and this file imports it.
3 //! 5 //!
4 //! This file follows proxy.zig's discipline and for the same reason: it 6 //! This file follows proxy.zig's discipline and for the same reason: it
5 //! knows NOTHING about the frame protocol it carries. It moves bytes 7 //! knows NOTHING about the frame protocol it carries. It moves bytes
@@ -13,206 +15,13 @@
13 //! for the integration assessment this implements. 15 //! for the integration assessment this implements.
14 const std = @import("std"); 16 const std = @import("std");
15 17
16 /// The C view of the QUIC stack. Exported because the client transport 18 const quic = @import("quic");
17 /// shares it: two @cImport blocks over the same headers produce two
18 /// *distinct* Zig types, so a client with its own would find that
19 /// `ngtcp2_vec` is not `ngtcp2_vec`. One import, one type universe.
20 pub const c = @cImport({
21 @cInclude("ngtcp2/ngtcp2.h");
22 @cInclude("ngtcp2/ngtcp2_crypto.h");
23 @cInclude("ngtcp2/ngtcp2_crypto_wolfssl.h");
24 @cInclude("wolfssl/options.h");
25 @cInclude("wolfssl/ssl.h");
26 });
27
28 pub const key_len = 32;
29
30 /// mux's conventional QUIC port. Both parsers reach for it when the user
31 /// names no port; it lives here because this module is the one thing both
32 /// binaries already import.
33 pub const default_port: u16 = 4433;
34
35 /// How long a QUIC connection tolerates silence before declaring the peer
36 /// gone. Long enough that a quiet terminal is not a suspicious one, short
37 /// enough that a client which has genuinely vanished stops being served
38 /// within a few seconds of keepalives failing — keepalives run at a third
39 /// of it, so an idle session is never the thing that trips it. Tunable on
40 /// both binaries because the reconnect tests need death declared on a
41 /// schedule they can wait for.
42 ///
43 /// Here for the same reason as `default_port`: two copies of a number both
44 /// binaries default to are two numbers, and they drift in silence.
45 pub const default_idle_ms: u32 = 15_000;
46
47 /// The pre-shared key, and the rules for getting one off disk.
48 ///
49 /// A key file is exactly as sensitive as an ssh private key, so it is held
50 /// to the same standard: readable by nobody but its owner. Refusing is the
51 /// whole point — a daemon that starts anyway with a world-readable key has
52 /// authenticated nothing, and would do it silently.
53 pub const Key = struct {
54 bytes: [key_len]u8,
55
56 pub const LoadError = error{
57 KeyFileMissing,
58 KeyFilePermissive,
59 KeyFileMalformed,
60 };
61
62 /// Accepts either 32 raw bytes or 64 hex characters (trailing
63 /// whitespace ignored, so `xxd -p` and a text editor both work).
64 pub fn load(path: []const u8) !Key {
65 const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
66 error.FileNotFound => return error.KeyFileMissing,
67 else => return err,
68 };
69 defer file.close();
70
71 const st = try file.stat();
72 // Group or other bits set = refuse, exactly as ssh does for a
73 // private key. Checked before the contents are read, so a bad-mode
74 // key is never even loaded into memory.
75 if (st.mode & 0o077 != 0) return error.KeyFilePermissive;
76
77 var buf: [128]u8 = undefined;
78 const n = try file.readAll(&buf);
79
80 // A file of exactly 32 bytes is a raw key, taken WITHOUT trimming.
81 // Trimming first looks harmless and is not: a random key ends in one
82 // of " \t\r\n" about one time in 64, and such keys were silently
83 // shortened to 31 bytes and rejected as malformed — so
84 // `head -c 32 /dev/urandom > k` failed for a few percent of the keys
85 // it generated. Rare, random, and looking like the user's fault,
86 // which is the worst shape a bug can have. Found by review probe.
87 if (n == key_len) {
88 var k: Key = undefined;
89 @memcpy(&k.bytes, buf[0..key_len]);
90 return k;
91 }
92
93 // Anything else may carry a text editor's trailing newline: trim
94 // and retry, which is the case the trimming actually exists for.
95 const raw = std.mem.trimRight(u8, buf[0..n], " \t\r\n");
96 if (raw.len == key_len) {
97 var k: Key = undefined;
98 @memcpy(&k.bytes, raw);
99 return k;
100 }
101 if (raw.len == key_len * 2) {
102 var k: Key = undefined;
103 _ = std.fmt.hexToBytes(&k.bytes, raw) catch return error.KeyFileMalformed;
104 return k;
105 }
106 return error.KeyFileMalformed;
107 }
108 };
109
110 /// Test helper: chmod a file inside a Dir. `Dir.chmod` applies to the
111 /// directory itself, not to an entry in it.
112 fn chmodAt(dir: std.fs.Dir, sub: []const u8, mode: std.posix.mode_t) !void {
113 const f = try dir.openFile(sub, .{});
114 defer f.close();
115 try f.chmod(mode);
116 }
117 19
118 test "Key.load: accepts 32 raw bytes and 64 hex chars, owner-only" { 20 /// The C view of the QUIC stack, imported once in quic.zig and shared:
119 var tmp = std.testing.tmpDir(.{}); 21 /// two @cImport blocks over the same headers produce two *distinct* Zig
120 defer tmp.cleanup(); 22 /// types, so a second block here would find that `ngtcp2_vec` is not
121 23 /// `ngtcp2_vec`. One import, one type universe.
122 const raw = [_]u8{0xAB} ** key_len; 24 const c = quic.c;
123 try tmp.dir.writeFile(.{ .sub_path = "raw.key", .data = &raw });
124 try chmodAt(tmp.dir, "raw.key", 0o600);
125
126 var hex: [key_len * 2]u8 = undefined;
127 _ = try std.fmt.bufPrint(&hex, "{x}", .{&raw});
128 // A key pasted by a human ends in a newline; that must not change it.
129 try tmp.dir.writeFile(.{ .sub_path = "hex.key", .data = hex ++ "\n" });
130 try chmodAt(tmp.dir, "hex.key", 0o600);
131
132 var path_buf: [256]u8 = undefined;
133 const dir = try tmp.dir.realpath(".", &path_buf);
134 var jb: [512]u8 = undefined;
135
136 const from_raw = try Key.load(try std.fmt.bufPrint(&jb, "{s}/raw.key", .{dir}));
137 try std.testing.expectEqualSlices(u8, &raw, &from_raw.bytes);
138
139 const from_hex = try Key.load(try std.fmt.bufPrint(&jb, "{s}/hex.key", .{dir}));
140 try std.testing.expectEqualSlices(u8, &raw, &from_hex.bytes);
141 }
142
143 test "Key.load: a raw key whose last byte is whitespace is still a key" {
144 var tmp = std.testing.tmpDir(.{});
145 defer tmp.cleanup();
146 var path_buf: [256]u8 = undefined;
147 const dir = try tmp.dir.realpath(".", &path_buf);
148 var jb: [512]u8 = undefined;
149
150 // Found by a review probe, not by me. Kept because the probe was
151 // transient: a bug with no test is a bug with a return ticket.
152 inline for (.{ '\n', '\r', '\t', ' ' }) |last| {
153 var raw = [_]u8{0x7F} ** key_len;
154 raw[key_len - 1] = last;
155 try tmp.dir.writeFile(.{ .sub_path = "ws.key", .data = &raw });
156 try chmodAt(tmp.dir, "ws.key", 0o600);
157 const k = try Key.load(try std.fmt.bufPrint(&jb, "{s}/ws.key", .{dir}));
158 try std.testing.expectEqualSlices(u8, &raw, &k.bytes);
159 }
160
161 // ...while 32 key bytes plus an editor's newline (33 on disk) still
162 // loads, which is what the trimming is for.
163 const clean = [_]u8{0x42} ** key_len;
164 try tmp.dir.writeFile(.{ .sub_path = "nl.key", .data = clean ++ "\n" });
165 try chmodAt(tmp.dir, "nl.key", 0o600);
166 const k2 = try Key.load(try std.fmt.bufPrint(&jb, "{s}/nl.key", .{dir}));
167 try std.testing.expectEqualSlices(u8, &clean, &k2.bytes);
168 }
169
170 test "Key.load: refuses a permissive mode, a missing file, and a bad length" {
171 var tmp = std.testing.tmpDir(.{});
172 defer tmp.cleanup();
173 var path_buf: [256]u8 = undefined;
174 const dir = try tmp.dir.realpath(".", &path_buf);
175 var jb: [512]u8 = undefined;
176
177 const raw = [_]u8{0xCD} ** key_len;
178
179 // Group-readable: refused, like an ssh private key.
180 try tmp.dir.writeFile(.{ .sub_path = "group.key", .data = &raw });
181 try chmodAt(tmp.dir, "group.key", 0o640);
182 try std.testing.expectError(
183 error.KeyFilePermissive,
184 Key.load(try std.fmt.bufPrint(&jb, "{s}/group.key", .{dir})),
185 );
186
187 // World-readable: same.
188 try tmp.dir.writeFile(.{ .sub_path = "world.key", .data = &raw });
189 try chmodAt(tmp.dir, "world.key", 0o604);
190 try std.testing.expectError(
191 error.KeyFilePermissive,
192 Key.load(try std.fmt.bufPrint(&jb, "{s}/world.key", .{dir})),
193 );
194
195 try std.testing.expectError(
196 error.KeyFileMissing,
197 Key.load(try std.fmt.bufPrint(&jb, "{s}/nope.key", .{dir})),
198 );
199
200 // Right mode, wrong content: neither 32 raw nor 64 hex.
201 try tmp.dir.writeFile(.{ .sub_path = "short.key", .data = "too short" });
202 try chmodAt(tmp.dir, "short.key", 0o600);
203 try std.testing.expectError(
204 error.KeyFileMalformed,
205 Key.load(try std.fmt.bufPrint(&jb, "{s}/short.key", .{dir})),
206 );
207
208 // 64 characters, but not hex.
209 try tmp.dir.writeFile(.{ .sub_path = "nothex.key", .data = "z" ** 64 });
210 try chmodAt(tmp.dir, "nothex.key", 0o600);
211 try std.testing.expectError(
212 error.KeyFileMalformed,
213 Key.load(try std.fmt.bufPrint(&jb, "{s}/nothex.key", .{dir})),
214 );
215 }
216 25
217 // --------------------------------------------------------------------------- 26 // ---------------------------------------------------------------------------
218 // The listener 27 // The listener
@@ -254,13 +63,12 @@ const max_conns = 16;
254 /// primary CID is matched separately, a cache that ever did fill would 63 /// primary CID is matched separately, a cache that ever did fill would
255 /// degrade to the old primary-only behaviour rather than misroute. 64 /// degrade to the old primary-only behaviour rather than misroute.
256 const max_cids = 16; 65 const max_cids = 16;
257 pub const max_udp = 1452; // conservative IPv4 datagram that avoids fragmenting
258 66
259 /// wolfSSL's PSK callbacks carry no user pointer, so the key has to be 67 /// wolfSSL's PSK callbacks carry no user pointer, so the key has to be
260 /// reachable without one. A daemon runs a single listener, which makes a 68 /// reachable without one. A daemon runs a single listener, which makes a
261 /// module-level key correct rather than merely convenient — but it is the 69 /// module-level key correct rather than merely convenient — but it is the
262 /// reason `Listener.init` refuses a second concurrent listener. 70 /// reason `Listener.init` refuses a second concurrent listener.
263 var g_key: ?Key = null; 71 var g_key: ?quic.Key = null;
264 var g_listener_live: bool = false; 72 var g_listener_live: bool = false;
265 73
266 fn pskServerCb( 74 fn pskServerCb(
@@ -274,133 +82,13 @@ fn pskServerCb(
274 // The out-param is `const char **` — the trap the spike recorded, and 82 // The out-param is `const char **` — the trap the spike recorded, and
275 // the reason this is written out rather than copied from the client 83 // the reason this is written out rather than copied from the client
276 // callback, which takes a different shape. 84 // callback, which takes a different shape.
277 if (ciphersuite) |cs| cs.* = psk_ciphersuite; 85 if (ciphersuite) |cs| cs.* = quic.psk_ciphersuite;
278 const k = g_key orelse return 0; 86 const k = g_key orelse return 0;
279 if (identity == null) return 0; 87 if (identity == null) return 0;
280 if (std.mem.orderZ(u8, identity, psk_identity) != .eq) return 0; 88 if (std.mem.orderZ(u8, identity, quic.psk_identity) != .eq) return 0;
281 if (key_max < key_len) return 0; 89 if (key_max < quic.key_len) return 0;
282 @memcpy(key_out[0..key_len], &k.bytes); 90 @memcpy(key_out[0..quic.key_len], &k.bytes);
283 return key_len; 91 return quic.key_len;
284 }
285
286 pub const psk_identity: [*:0]const u8 = "mux";
287 pub const psk_ciphersuite: [*:0]const u8 = "TLS13-AES128-GCM-SHA256";
288 pub const alpn = "\x03mux";
289
290 /// How many outbound bytes one connection may hold. Sized to the stream
291 /// window the peer advertises, because holding much more than the peer will
292 /// let us send buys nothing: past this the daemon's own `pending_cap` is the
293 /// right place for the backlog to sit and be judged.
294 pub const egress_cap = 256 * 1024;
295
296 /// Outbound stream bytes, in a ring that never moves a byte once written.
297 ///
298 /// A ring rather than a growable buffer, because ngtcp2 does NOT copy stream
299 /// payload: `ngtcp2_conn_writev_stream` stores the *vector* it is handed —
300 /// `ngtcp2_vec_copy` is a memcpy of base+len, not of the bytes — in its
301 /// retransmission queue, and re-reads those bytes if the packet is lost. A
302 /// buffer that reallocates on append, or that clears once the last byte has
303 /// been handed over, therefore leaves ngtcp2 holding a freed or recycled
304 /// pointer, and it dies inside `ngtcp2_pkt_encode_stream_frame`. It did:
305 /// rarely, only under loss, and only in the one test big enough to overflow
306 /// a socket buffer — which is the worst shape a bug can have.
307 ///
308 /// So the invariant, and everything here exists to hold it: **a byte handed
309 /// to ngtcp2 does not move or get overwritten until the peer acknowledges
310 /// it.** `head` advances only from `acked_stream_data_offset`; a write can
311 /// only land in the free space that leaves. Being fixed in size is the other
312 /// half of the point — a full ring is the backpressure signal.
313 pub const Egress = struct {
314 buf: []u8,
315 /// The oldest byte the peer has not acknowledged.
316 head: usize = 0,
317 /// Bytes from `head` still owed: handed to ngtcp2 and unacknowledged,
318 /// plus not yet handed over.
319 held: usize = 0,
320 /// The tail of `held` that ngtcp2 has not taken yet.
321 unsent: usize = 0,
322
323 pub fn deinit(self: *Egress, alloc: std.mem.Allocator) void {
324 alloc.free(self.buf);
325 self.* = .{ .buf = &.{} };
326 }
327
328 pub fn freeSpace(self: *const Egress) usize {
329 return self.buf.len - self.held;
330 }
331
332 /// Take what fits and report how much that was. A short return is not an
333 /// error, it is the whole mechanism: the caller keeps the remainder and
334 /// is thereby the one holding — and bounding — the backlog.
335 pub fn push(self: *Egress, bytes: []const u8) usize {
336 const n = @min(bytes.len, self.freeSpace());
337 if (n == 0) return 0;
338 const start = (self.head + self.held) % self.buf.len;
339 const first = @min(n, self.buf.len - start);
340 @memcpy(self.buf[start..][0..first], bytes[0..first]);
341 if (first < n) @memcpy(self.buf[0 .. n - first], bytes[first..n]);
342 self.held += n;
343 self.unsent += n;
344 return n;
345 }
346
347 /// The unsent region as up to two vectors — two when it wraps, which is
348 /// the price of never moving a byte, and ngtcp2 takes a vector array
349 /// precisely so that price is payable.
350 pub fn vecs(self: *const Egress, out: *[2]c.ngtcp2_vec) usize {
351 if (self.unsent == 0) return 0;
352 const start = (self.head + (self.held - self.unsent)) % self.buf.len;
353 const first = @min(self.unsent, self.buf.len - start);
354 out[0] = .{ .base = self.buf.ptr + start, .len = first };
355 if (first == self.unsent) return 1;
356 out[1] = .{ .base = self.buf.ptr, .len = self.unsent - first };
357 return 2;
358 }
359
360 /// ngtcp2 took `n` bytes off the unsent region. They stay exactly where
361 /// they are — it now has pointers to them.
362 pub fn took(self: *Egress, n: usize) void {
363 self.unsent -= @min(n, self.unsent);
364 }
365
366 /// The peer acknowledged `n` more bytes. ngtcp2 documents this callback
367 /// as arriving "sequentially in increasing order of offset without any
368 /// overlap", so a running count IS the acknowledged prefix, and this is
369 /// the only thing that ever frees space.
370 pub fn ack(self: *Egress, n: usize) void {
371 // A deinit'd ring has a zero-length buffer, and the modulo below
372 // would divide by zero. Reachable because an ack can arrive for a
373 // connection whose egress has already been torn down.
374 if (self.buf.len == 0) return;
375 const taken = @min(n, self.held - self.unsent);
376 self.head = (self.head + taken) % self.buf.len;
377 self.held -= taken;
378 }
379 };
380
381 /// What one `writev_stream` return means for the egress ring — and, more to
382 /// the point, IN WHAT ORDER.
383 ///
384 /// The ordering is the whole reason this is a function rather than three
385 /// copies of two ifs. ngtcp2 can commit `ndatalen` — advancing the stream
386 /// offset it will retransmit from — and still return an error afterwards
387 /// (NOMEM out of rtb_add, say). Account for the bytes first or they stay
388 /// counted as unsent, get offered again at an offset the peer has moved
389 /// past, and the stream desynchronises somewhere far away from here.
390 pub const WriteAction = enum {
391 /// The call failed. Stop draining; the bytes are already accounted.
392 stop,
393 /// Nothing more to send right now.
394 brk,
395 /// A packet was produced; send it and go round again.
396 cont,
397 };
398
399 pub fn accountWrite(out: *Egress, wrote: c.ngtcp2_ssize, n: c.ngtcp2_ssize) WriteAction {
400 if (wrote > 0) out.took(@intCast(wrote));
401 if (n < 0) return .stop;
402 if (n == 0) return .brk;
403 return .cont;
404 } 92 }
405 93
406 /// One authenticated peer: an ngtcp2 connection, its TLS object, and the 94 /// One authenticated peer: an ngtcp2 connection, its TLS object, and the
@@ -429,7 +117,7 @@ const Conn = struct {
429 close_state: enum { open, closing_quiet, closing_notify } = .open, 117 close_state: enum { open, closing_quiet, closing_notify } = .open,
430 stream_id: i64 = -1, 118 stream_id: i64 = -1,
431 /// Bytes owed to this peer. See Egress: they do not move until acked. 119 /// Bytes owed to this peer. See Egress: they do not move until acked.
432 out: Egress, 120 out: quic.Egress,
433 opened: bool = false, 121 opened: bool = false,
434 122
435 /// Re-read the advertised CIDs from ngtcp2. Lazy, because the answer 123 /// Re-read the advertised CIDs from ngtcp2. Lazy, because the answer
@@ -474,10 +162,6 @@ fn getConnCb(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn {
474 return cn.conn; 162 return cn.conn;
475 } 163 }
476 164
477 fn randCb(dest: [*c]u8, destlen: usize, _: [*c]const c.ngtcp2_rand_ctx) callconv(.c) void {
478 std.crypto.random.bytes(dest[0..destlen]);
479 }
480
481 /// The handler a listener carries between `bind` and `setHandler`: it 165 /// The handler a listener carries between `bind` and `setHandler`: it
482 /// exists so that window has no null to check on every packet. 166 /// exists so that window has no null to check on every packet.
483 fn ignoreOpen(_: *anyopaque, _: u64) void {} 167 fn ignoreOpen(_: *anyopaque, _: u64) void {}
@@ -499,7 +183,7 @@ fn serverGetNewCidCb(
499 cidlen: usize, 183 cidlen: usize,
500 user_data: ?*anyopaque, 184 user_data: ?*anyopaque,
501 ) callconv(.c) c_int { 185 ) callconv(.c) c_int {
502 const rv = getNewCidCb(conn, cid, token, cidlen, user_data); 186 const rv = quic.getNewCidCb(conn, cid, token, cidlen, user_data);
503 const cn: *Conn = @ptrCast(@alignCast(user_data.?)); 187 const cn: *Conn = @ptrCast(@alignCast(user_data.?));
504 cn.cids_dirty = true; 188 cn.cids_dirty = true;
505 return rv; 189 return rv;
@@ -517,19 +201,6 @@ fn serverRemoveCidCb(
517 return 0; 201 return 0;
518 } 202 }
519 203
520 fn getNewCidCb(
521 _: ?*c.ngtcp2_conn,
522 cid: [*c]c.ngtcp2_cid,
523 token: [*c]c.ngtcp2_stateless_reset_token,
524 cidlen: usize,
525 _: ?*anyopaque,
526 ) callconv(.c) c_int {
527 std.crypto.random.bytes(cid.*.data[0..cidlen]);
528 cid.*.datalen = cidlen;
529 std.crypto.random.bytes(&token.*.data);
530 return 0;
531 }
532
533 fn handshakeCompletedCb(_: ?*c.ngtcp2_conn, user_data: ?*anyopaque) callconv(.c) c_int { 204 fn handshakeCompletedCb(_: ?*c.ngtcp2_conn, user_data: ?*anyopaque) callconv(.c) c_int {
534 const cn: *Conn = @ptrCast(@alignCast(user_data.?)); 205 const cn: *Conn = @ptrCast(@alignCast(user_data.?));
535 cn.opened = true; 206 cn.opened = true;
@@ -644,7 +315,7 @@ pub const Listener = struct {
644 pub fn bind( 315 pub fn bind(
645 alloc: std.mem.Allocator, 316 alloc: std.mem.Allocator,
646 bind_addr: std.net.Address, 317 bind_addr: std.net.Address,
647 key: Key, 318 key: quic.Key,
648 idle_ms: u64, 319 idle_ms: u64,
649 ) !*Listener { 320 ) !*Listener {
650 return init(alloc, bind_addr, key, .{ 321 return init(alloc, bind_addr, key, .{
@@ -668,7 +339,7 @@ pub const Listener = struct {
668 pub fn init( 339 pub fn init(
669 alloc: std.mem.Allocator, 340 alloc: std.mem.Allocator,
670 bind_addr: std.net.Address, 341 bind_addr: std.net.Address,
671 key: Key, 342 key: quic.Key,
672 handler: Handler, 343 handler: Handler,
673 idle_ms: u64, 344 idle_ms: u64,
674 ) !*Listener { 345 ) !*Listener {
@@ -696,7 +367,7 @@ pub const Listener = struct {
696 if (c.ngtcp2_crypto_wolfssl_configure_server_context(ctx) != 0) return error.TlsInit; 367 if (c.ngtcp2_crypto_wolfssl_configure_server_context(ctx) != 0) return error.TlsInit;
697 c.wolfSSL_CTX_set_psk_server_tls13_callback(ctx, pskServerCb); 368 c.wolfSSL_CTX_set_psk_server_tls13_callback(ctx, pskServerCb);
698 _ = c.wolfSSL_CTX_use_psk_identity_hint(ctx, ""); 369 _ = c.wolfSSL_CTX_use_psk_identity_hint(ctx, "");
699 _ = c.wolfSSL_CTX_set_cipher_list(ctx, psk_ciphersuite); 370 _ = c.wolfSSL_CTX_set_cipher_list(ctx, quic.psk_ciphersuite);
700 371
701 const self = try alloc.create(Listener); 372 const self = try alloc.create(Listener);
702 errdefer alloc.destroy(self); 373 errdefer alloc.destroy(self);
@@ -900,7 +571,7 @@ pub const Listener = struct {
900 from_len, 571 from_len,
901 &hd.dcid, 572 &hd.dcid,
902 60 * 1_000_000_000, // a token is good for 60s 573 60 * 1_000_000_000, // a token is good for 60s
903 timestampNs(), 574 quic.timestampNs(),
904 ) != 0) return; 575 ) != 0) return;
905 576
906 const slot = for (&self.conns) |*sl| { 577 const slot = for (&self.conns) |*sl| {
@@ -908,7 +579,7 @@ pub const Listener = struct {
908 } else return; // full: drop, the peer will retry 579 } else return; // full: drop, the peer will retry
909 580
910 const cn = self.alloc.create(Conn) catch return; 581 const cn = self.alloc.create(Conn) catch return;
911 const ring = self.alloc.alloc(u8, egress_cap) catch { 582 const ring = self.alloc.alloc(u8, quic.egress_cap) catch {
912 self.alloc.destroy(cn); 583 self.alloc.destroy(cn);
913 return; 584 return;
914 }; 585 };
@@ -950,7 +621,7 @@ pub const Listener = struct {
950 cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb; 621 cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
951 cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb; 622 cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb;
952 cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb; 623 cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb;
953 cbs.rand = randCb; 624 cbs.rand = quic.randCb;
954 cbs.get_new_connection_id2 = serverGetNewCidCb; 625 cbs.get_new_connection_id2 = serverGetNewCidCb;
955 cbs.remove_connection_id = serverRemoveCidCb; 626 cbs.remove_connection_id = serverRemoveCidCb;
956 cbs.handshake_completed = handshakeCompletedCb; 627 cbs.handshake_completed = handshakeCompletedCb;
@@ -960,7 +631,7 @@ pub const Listener = struct {
960 631
961 var settings: c.ngtcp2_settings = undefined; 632 var settings: c.ngtcp2_settings = undefined;
962 c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); 633 c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings);
963 settings.initial_ts = timestampNs(); 634 settings.initial_ts = quic.timestampNs();
964 settings.token = hd.token; 635 settings.token = hd.token;
965 settings.tokenlen = hd.tokenlen; 636 settings.tokenlen = hd.tokenlen;
966 637
@@ -978,11 +649,7 @@ pub const Listener = struct {
978 // transport to be declared dead quickly and deterministically. 649 // transport to be declared dead quickly and deterministically.
979 params.max_idle_timeout = self.idle_ms * 1_000_000; 650 params.max_idle_timeout = self.idle_ms * 1_000_000;
980 651
981 var path: c.ngtcp2_path = .{ 652 var path = quic.pathFrom(&cn.local_storage, cn.local_len, from, from_len);
982 .local = .{ .addr = @ptrCast(&cn.local_storage), .addrlen = cn.local_len },
983 .remote = .{ .addr = @ptrCast(from), .addrlen = from_len },
984 .user_data = null,
985 };
986 var conn: ?*c.ngtcp2_conn = null; 653 var conn: ?*c.ngtcp2_conn = null;
987 if (c.ngtcp2_conn_server_new_versioned( 654 if (c.ngtcp2_conn_server_new_versioned(
988 &conn, 655 &conn,
@@ -1013,7 +680,7 @@ pub const Listener = struct {
1013 // screen. The keep-alive PING is ack-eliciting, so its ACK restarts 680 // screen. The keep-alive PING is ack-eliciting, so its ACK restarts
1014 // the idle timer at both ends; a peer that has genuinely vanished 681 // the idle timer at both ends; a peer that has genuinely vanished
1015 // answers nothing and still times out on schedule. 682 // answers nothing and still times out on schedule.
1016 c.ngtcp2_conn_set_keep_alive_timeout(conn, keepAliveNs(self.idle_ms)); 683 c.ngtcp2_conn_set_keep_alive_timeout(conn, quic.keepAliveNs(self.idle_ms));
1017 slot.* = cn; 684 slot.* = cn;
1018 685
1019 self.feed(slot, pkt, from, from_len); 686 self.feed(slot, pkt, from, from_len);
@@ -1039,11 +706,11 @@ pub const Listener = struct {
1039 from_len, 706 from_len,
1040 &scid, 707 &scid,
1041 &hd.dcid, 708 &hd.dcid,
1042 timestampNs(), 709 quic.timestampNs(),
1043 ); 710 );
1044 if (tlen < 0) return; 711 if (tlen < 0) return;
1045 712
1046 var buf: [max_udp]u8 = undefined; 713 var buf: [quic.max_udp]u8 = undefined;
1047 const n = c.ngtcp2_crypto_write_retry( 714 const n = c.ngtcp2_crypto_write_retry(
1048 &buf, 715 &buf,
1049 buf.len, 716 buf.len,
@@ -1068,7 +735,7 @@ pub const Listener = struct {
1068 // today; this costs a compare and stops that being load-bearing. 735 // today; this costs a compare and stops that being load-bearing.
1069 if (cap_ms < 0) return cap_ms; 736 if (cap_ms < 0) return cap_ms;
1070 var best: i32 = cap_ms; 737 var best: i32 = cap_ms;
1071 const now = timestampNs(); 738 const now = quic.timestampNs();
1072 for (self.conns) |slot| { 739 for (self.conns) |slot| {
1073 const cn = slot orelse continue; 740 const cn = slot orelse continue;
1074 const conn = cn.conn orelse continue; 741 const conn = cn.conn orelse continue;
@@ -1086,7 +753,7 @@ pub const Listener = struct {
1086 /// Service every connection whose deadline has passed. Called by the 753 /// Service every connection whose deadline has passed. Called by the
1087 /// daemon after each poll, whether or not the socket was readable. 754 /// daemon after each poll, whether or not the socket was readable.
1088 pub fn tick(self: *Listener) void { 755 pub fn tick(self: *Listener) void {
1089 const now = timestampNs(); 756 const now = quic.timestampNs();
1090 for (&self.conns) |*slot| { 757 for (&self.conns) |*slot| {
1091 const cn = slot.* orelse continue; 758 const cn = slot.* orelse continue;
1092 const conn = cn.conn orelse continue; 759 const conn = cn.conn orelse continue;
@@ -1156,16 +823,12 @@ pub const Listener = struct {
1156 ) void { 823 ) void {
1157 const cn = slot.* orelse return; 824 const cn = slot.* orelse return;
1158 const conn = cn.conn orelse return; 825 const conn = cn.conn orelse return;
1159 var path: c.ngtcp2_path = .{ 826 var path = quic.pathFrom(&cn.local_storage, cn.local_len, from, from_len);
1160 .local = .{ .addr = @ptrCast(&cn.local_storage), .addrlen = cn.local_len },
1161 .remote = .{ .addr = @ptrCast(from), .addrlen = from_len },
1162 .user_data = null,
1163 };
1164 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; 827 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 };
1165 const rv = blk: { 828 const rv = blk: {
1166 self.ngtcp2_depth += 1; 829 self.ngtcp2_depth += 1;
1167 defer self.ngtcp2_depth -= 1; 830 defer self.ngtcp2_depth -= 1;
1168 break :blk c.ngtcp2_conn_read_pkt(conn, &path, &pi, pkt.ptr, pkt.len, timestampNs()); 831 break :blk c.ngtcp2_conn_read_pkt(conn, &path, &pi, pkt.ptr, pkt.len, quic.timestampNs());
1169 }; 832 };
1170 // Reaped before anything else touches the table: a connection the 833 // Reaped before anything else touches the table: a connection the
1171 // handler closed mid-callback is gone from here on. 834 // handler closed mid-callback is gone from here on.
@@ -1195,7 +858,7 @@ pub const Listener = struct {
1195 // one run in a few hundred somewhere else entirely. 858 // one run in a few hundred somewhere else entirely.
1196 std.debug.assert(self.ngtcp2_depth == 0); 859 std.debug.assert(self.ngtcp2_depth == 0);
1197 const conn = cn.conn orelse return; 860 const conn = cn.conn orelse return;
1198 var buf: [max_udp]u8 = undefined; 861 var buf: [quic.max_udp]u8 = undefined;
1199 // Set when the peer's stream window is full. Everything that is not 862 // Set when the peer's stream window is full. Everything that is not
1200 // stream data still has to leave. 863 // stream data still has to leave.
1201 var stream_blocked = false; 864 var stream_blocked = false;
@@ -1225,7 +888,7 @@ pub const Listener = struct {
1225 sid, 888 sid,
1226 if (vcnt > 0) &vecs else null, 889 if (vcnt > 0) &vecs else null,
1227 vcnt, 890 vcnt,
1228 timestampNs(), 891 quic.timestampNs(),
1229 ); 892 );
1230 if (n == c.NGTCP2_ERR_STREAM_DATA_BLOCKED or n == c.NGTCP2_ERR_STREAM_SHUT_WR) { 893 if (n == c.NGTCP2_ERR_STREAM_DATA_BLOCKED or n == c.NGTCP2_ERR_STREAM_SHUT_WR) {
1231 // A documented return, not a failure: the peer has no window 894 // A documented return, not a failure: the peer has no window
@@ -1237,7 +900,7 @@ pub const Listener = struct {
1237 stream_blocked = true; 900 stream_blocked = true;
1238 continue; 901 continue;
1239 } 902 }
1240 switch (accountWrite(&cn.out, wrote, n)) { 903 switch (quic.accountWrite(&cn.out, wrote, n)) {
1241 .stop => return, 904 .stop => return,
1242 .brk => break, 905 .brk => break,
1243 .cont => {}, 906 .cont => {},
@@ -1276,28 +939,6 @@ pub const Listener = struct {
1276 } 939 }
1277 }; 940 };
1278 941
1279 /// A third of the idle timeout, so two keepalives can go unanswered before
1280 /// the connection is called dead. Never zero: ngtcp2 reads a zero timeout as
1281 /// "disabled", exactly as it reads UINT64_MAX, so a small idle_ms rounding
1282 /// down would silently restore the behaviour the keepalive exists to prevent.
1283 pub fn keepAliveNs(idle_ms: u64) u64 {
1284 return @max(1, idle_ms / 3) * 1_000_000;
1285 }
1286
1287 test "keepAlive: a third of the idle timeout, and never disabled" {
1288 try std.testing.expectEqual(@as(u64, 5_000_000_000), keepAliveNs(15_000));
1289 try std.testing.expectEqual(@as(u64, 500_000_000), keepAliveNs(1500));
1290 // Zero would mean "no keepalive" to ngtcp2 — the opposite of what a
1291 // small idle timeout is asking for.
1292 try std.testing.expectEqual(@as(u64, 1_000_000), keepAliveNs(1));
1293 try std.testing.expectEqual(@as(u64, 1_000_000), keepAliveNs(2));
1294 }
1295
1296 pub fn timestampNs() u64 {
1297 const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch return 0;
1298 return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
1299 }
1300
1301 // --------------------------------------------------------------------------- 942 // ---------------------------------------------------------------------------
1302 // Tests: a real handshake against a real client, in one process. 943 // Tests: a real handshake against a real client, in one process.
1303 // 944 //
@@ -1306,7 +947,7 @@ pub fn timestampNs() u64 {
1306 // handshake either completes against a real peer or it does not. 947 // handshake either completes against a real peer or it does not.
1307 // --------------------------------------------------------------------------- 948 // ---------------------------------------------------------------------------
1308 949
1309 var g_client_key: Key = undefined; 950 var g_client_key: quic.Key = undefined;
1310 951
1311 fn pskClientCb( 952 fn pskClientCb(
1312 ssl: ?*c.WOLFSSL, 953 ssl: ?*c.WOLFSSL,
@@ -1319,11 +960,11 @@ fn pskClientCb(
1319 ) callconv(.c) c_uint { 960 ) callconv(.c) c_uint {
1320 _ = ssl; 961 _ = ssl;
1321 _ = hint; 962 _ = hint;
1322 if (id_max < 4 or key_max < key_len) return 0; 963 if (id_max < 4 or key_max < quic.key_len) return 0;
1323 @memcpy(identity[0..4], "mux\x00"); 964 @memcpy(identity[0..4], "mux\x00");
1324 @memcpy(key_out[0..key_len], &g_client_key.bytes); 965 @memcpy(key_out[0..quic.key_len], &g_client_key.bytes);
1325 if (ciphersuite) |cs| cs.* = psk_ciphersuite; 966 if (ciphersuite) |cs| cs.* = quic.psk_ciphersuite;
1326 return key_len; 967 return quic.key_len;
1327 } 968 }
1328 969
1329 /// Test scaffolding, exported so the daemon's own tests can drive a real 970 /// Test scaffolding, exported so the daemon's own tests can drive a real
@@ -1411,7 +1052,7 @@ pub const TestClient = struct {
1411 return 0; 1052 return 0;
1412 } 1053 }
1413 1054
1414 pub fn init(server_addr: std.net.Address, key: Key) !TestClient { 1055 pub fn init(server_addr: std.net.Address, key: quic.Key) !TestClient {
1415 g_client_key = key; 1056 g_client_key = key;
1416 const fd = try std.posix.socket( 1057 const fd = try std.posix.socket(
1417 server_addr.any.family, 1058 server_addr.any.family,
@@ -1441,12 +1082,12 @@ pub const TestClient = struct {
1441 self.ssl_ctx = ctx; 1082 self.ssl_ctx = ctx;
1442 if (c.ngtcp2_crypto_wolfssl_configure_client_context(ctx) != 0) return error.TlsInit; 1083 if (c.ngtcp2_crypto_wolfssl_configure_client_context(ctx) != 0) return error.TlsInit;
1443 c.wolfSSL_CTX_set_psk_client_tls13_callback(ctx, pskClientCb); 1084 c.wolfSSL_CTX_set_psk_client_tls13_callback(ctx, pskClientCb);
1444 _ = c.wolfSSL_CTX_set_cipher_list(ctx, psk_ciphersuite); 1085 _ = c.wolfSSL_CTX_set_cipher_list(ctx, quic.psk_ciphersuite);
1445 const ssl = c.wolfSSL_new(ctx) orelse return error.TlsInit; 1086 const ssl = c.wolfSSL_new(ctx) orelse return error.TlsInit;
1446 self.ssl = ssl; 1087 self.ssl = ssl;
1447 self.conn_ref = .{ .get_conn = getConn, .user_data = self }; 1088 self.conn_ref = .{ .get_conn = getConn, .user_data = self };
1448 _ = c.wolfSSL_set_app_data(ssl, &self.conn_ref); 1089 _ = c.wolfSSL_set_app_data(ssl, &self.conn_ref);
1449 _ = c.wolfSSL_UseALPN(ssl, @constCast(alpn[1..].ptr), alpn.len - 1, c.WOLFSSL_ALPN_FAILED_ON_MISMATCH); 1090 _ = c.wolfSSL_UseALPN(ssl, @constCast(quic.alpn[1..].ptr), quic.alpn.len - 1, c.WOLFSSL_ALPN_FAILED_ON_MISMATCH);
1450 1091
1451 var dcid: c.ngtcp2_cid = undefined; 1092 var dcid: c.ngtcp2_cid = undefined;
1452 dcid.datalen = 16; 1093 dcid.datalen = 16;
@@ -1467,15 +1108,15 @@ pub const TestClient = struct {
1467 cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb; 1108 cbs.delete_crypto_cipher_ctx = c.ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
1468 cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb; 1109 cbs.get_path_challenge_data = c.ngtcp2_crypto_get_path_challenge_data_cb;
1469 cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb; 1110 cbs.version_negotiation = c.ngtcp2_crypto_version_negotiation_cb;
1470 cbs.rand = randCb; 1111 cbs.rand = quic.randCb;
1471 cbs.get_new_connection_id2 = getNewCidCb; 1112 cbs.get_new_connection_id2 = quic.getNewCidCb;
1472 cbs.handshake_completed = onHandshake; 1113 cbs.handshake_completed = onHandshake;
1473 cbs.extend_max_local_streams_bidi = onStreams; 1114 cbs.extend_max_local_streams_bidi = onStreams;
1474 cbs.recv_stream_data = onData; 1115 cbs.recv_stream_data = onData;
1475 1116
1476 var settings: c.ngtcp2_settings = undefined; 1117 var settings: c.ngtcp2_settings = undefined;
1477 c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); 1118 c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings);
1478 settings.initial_ts = timestampNs(); 1119 settings.initial_ts = quic.timestampNs();
1479 var params: c.ngtcp2_transport_params = undefined; 1120 var params: c.ngtcp2_transport_params = undefined;
1480 c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, &params); 1121 c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, &params);
1481 params.initial_max_streams_bidi = 4; 1122 params.initial_max_streams_bidi = 4;
@@ -1483,11 +1124,7 @@ pub const TestClient = struct {
1483 params.initial_max_stream_data_bidi_remote = 256 * 1024; 1124 params.initial_max_stream_data_bidi_remote = 256 * 1024;
1484 params.initial_max_data = 1024 * 1024; 1125 params.initial_max_data = 1024 * 1024;
1485 1126
1486 var path: c.ngtcp2_path = .{ 1127 var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len);
1487 .local = .{ .addr = @ptrCast(&self.local), .addrlen = self.local_len },
1488 .remote = .{ .addr = @ptrCast(&self.remote), .addrlen = self.remote_len },
1489 .user_data = null,
1490 };
1491 var conn: ?*c.ngtcp2_conn = null; 1128 var conn: ?*c.ngtcp2_conn = null;
1492 if (c.ngtcp2_conn_client_new_versioned( 1129 if (c.ngtcp2_conn_client_new_versioned(
1493 &conn, 1130 &conn,
@@ -1510,7 +1147,7 @@ pub const TestClient = struct {
1510 1147
1511 pub fn drain(self: *TestClient) void { 1148 pub fn drain(self: *TestClient) void {
1512 const conn = self.conn orelse return; 1149 const conn = self.conn orelse return;
1513 var buf: [max_udp]u8 = undefined; 1150 var buf: [quic.max_udp]u8 = undefined;
1514 while (true) { 1151 while (true) {
1515 var ps: c.ngtcp2_path_storage = undefined; 1152 var ps: c.ngtcp2_path_storage = undefined;
1516 c.ngtcp2_path_storage_zero(&ps); 1153 c.ngtcp2_path_storage_zero(&ps);
@@ -1536,7 +1173,7 @@ pub const TestClient = struct {
1536 sid, 1173 sid,
1537 if (vcnt > 0) &vec else null, 1174 if (vcnt > 0) &vec else null,
1538 vcnt, 1175 vcnt,
1539 timestampNs(), 1176 quic.timestampNs(),
1540 ); 1177 );
1541 // Same ordering as the listener's drain, and for the same 1178 // Same ordering as the listener's drain, and for the same
1542 // reason: ngtcp2 can commit the stream offset and still return 1179 // reason: ngtcp2 can commit the stream offset and still return
@@ -1555,13 +1192,9 @@ pub const TestClient = struct {
1555 const n = std.posix.recv(self.fd, &buf, 0) catch return; 1192 const n = std.posix.recv(self.fd, &buf, 0) catch return;
1556 if (n == 0) return; 1193 if (n == 0) return;
1557 const conn = self.conn orelse return; 1194 const conn = self.conn orelse return;
1558 var path: c.ngtcp2_path = .{ 1195 var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len);
1559 .local = .{ .addr = @ptrCast(&self.local), .addrlen = self.local_len },
1560 .remote = .{ .addr = @ptrCast(&self.remote), .addrlen = self.remote_len },
1561 .user_data = null,
1562 };
1563 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; 1196 var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 };
1564 _ = c.ngtcp2_conn_read_pkt(conn, &path, &pi, &buf, n, timestampNs()); 1197 _ = c.ngtcp2_conn_read_pkt(conn, &path, &pi, &buf, n, quic.timestampNs());
1565 } 1198 }
1566 } 1199 }
1567 1200
@@ -1655,7 +1288,7 @@ fn pump(l: *Listener, cl: *TestClient, ms: u64, done: *const fn (*EchoOwner, *Te
1655 1288
1656 fn loopbackListener( 1289 fn loopbackListener(
1657 alloc: std.mem.Allocator, 1290 alloc: std.mem.Allocator,
1658 key: Key, 1291 key: quic.Key,
1659 owner: *EchoOwner, 1292 owner: *EchoOwner,
1660 idle_ms: u64, 1293 idle_ms: u64,
1661 ) !struct { l: *Listener, addr: std.net.Address } { 1294 ) !struct { l: *Listener, addr: std.net.Address } {
@@ -1670,7 +1303,7 @@ fn loopbackListener(
1670 1303
1671 test "Listener: PSK handshake, Retry, and a payload larger than the initial window" { 1304 test "Listener: PSK handshake, Retry, and a payload larger than the initial window" {
1672 const alloc = std.testing.allocator; 1305 const alloc = std.testing.allocator;
1673 const key: Key = .{ .bytes = [_]u8{0x5A} ** key_len }; 1306 const key: quic.Key = .{ .bytes = [_]u8{0x5A} ** quic.key_len };
1674 1307
1675 var owner: EchoOwner = .{}; 1308 var owner: EchoOwner = .{};
1676 defer owner.deinit(); 1309 defer owner.deinit();
@@ -1752,8 +1385,8 @@ test "Listener: PSK handshake, Retry, and a payload larger than the initial wind
1752 1385
1753 test "Listener: a client holding the wrong key never completes a handshake" { 1386 test "Listener: a client holding the wrong key never completes a handshake" {
1754 const alloc = std.testing.allocator; 1387 const alloc = std.testing.allocator;
1755 const server_key: Key = .{ .bytes = [_]u8{0x11} ** key_len }; 1388 const server_key: quic.Key = .{ .bytes = [_]u8{0x11} ** quic.key_len };
1756 const wrong_key: Key = .{ .bytes = [_]u8{0x22} ** key_len }; 1389 const wrong_key: quic.Key = .{ .bytes = [_]u8{0x22} ** quic.key_len };
1757 1390
1758 var owner: EchoOwner = .{}; 1391 var owner: EchoOwner = .{};
1759 defer owner.deinit(); 1392 defer owner.deinit();
@@ -1790,7 +1423,7 @@ test "Listener: a client holding the wrong key never completes a handshake" {
1790 1423
1791 test "Listener: keepalive carries an idle connection past its idle timeout" { 1424 test "Listener: keepalive carries an idle connection past its idle timeout" {
1792 const alloc = std.testing.allocator; 1425 const alloc = std.testing.allocator;
1793 const key: Key = .{ .bytes = [_]u8{0x3C} ** key_len }; 1426 const key: quic.Key = .{ .bytes = [_]u8{0x3C} ** quic.key_len };
1794 1427
1795 // Short enough that the test is quick, long enough that a handshake 1428 // Short enough that the test is quick, long enough that a handshake
1796 // fits inside it: the keepalive lands at 300ms, the timeout at 900ms. 1429 // fits inside it: the keepalive lands at 300ms, the timeout at 900ms.
@@ -1834,98 +1467,6 @@ test "Listener: keepalive carries an idle connection past its idle timeout" {
1834 try std.testing.expectEqual(@as(usize, 0), owner.closed); 1467 try std.testing.expectEqual(@as(usize, 0), owner.closed);
1835 } 1468 }
1836 1469
1837 test "Egress: a byte does not move until it is acked, and the ring wraps" {
1838 const alloc = std.testing.allocator;
1839 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
1840 defer e.deinit(alloc);
1841
1842 try std.testing.expectEqual(@as(usize, 5), e.push("hello"));
1843 var v: [2]c.ngtcp2_vec = undefined;
1844 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
1845 try std.testing.expectEqual(@as(usize, 5), v[0].len);
1846 const base = e.buf.ptr;
1847 try std.testing.expectEqual(base, v[0].base);
1848
1849 // ngtcp2 takes three. Those three now have a pointer pointing at them
1850 // and must not move; the two behind them are still ours to offer.
1851 e.took(3);
1852 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
1853 try std.testing.expectEqual(@as(usize, 2), v[0].len);
1854 try std.testing.expectEqual(base + 3, v[0].base);
1855
1856 // Room is what is left after everything HELD, sent or not — the three
1857 // in ngtcp2's hands are not free space just because they left the box.
1858 try std.testing.expectEqual(@as(usize, 3), e.freeSpace());
1859 try std.testing.expectEqual(@as(usize, 3), e.push("world"));
1860 try std.testing.expectEqual(@as(usize, 0), e.push("x"));
1861
1862 // An acknowledgement is the only thing that frees anything.
1863 e.ack(3);
1864 try std.testing.expectEqual(@as(usize, 3), e.freeSpace());
1865
1866 // ...and the write that follows wraps rather than shifting a byte.
1867 try std.testing.expectEqual(@as(usize, 3), e.push("abc"));
1868 try std.testing.expectEqual(@as(usize, 2), e.vecs(&v));
1869 try std.testing.expectEqualStrings("lowor", v[0].base[0..v[0].len]);
1870 try std.testing.expectEqualStrings("abc", v[1].base[0..v[1].len]);
1871 }
1872
1873 test "Egress: an ack can never free more than is outstanding" {
1874 const alloc = std.testing.allocator;
1875 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
1876 defer e.deinit(alloc);
1877
1878 _ = e.push("abcd");
1879 e.took(2);
1880 // Two are in flight and two are still unsent. A callback claiming more
1881 // than is outstanding must not consume the unsent ones — they have
1882 // never been on the wire and cannot have been acknowledged.
1883 e.ack(99);
1884 try std.testing.expectEqual(@as(usize, 2), e.held);
1885 try std.testing.expectEqual(@as(usize, 2), e.unsent);
1886 var v: [2]c.ngtcp2_vec = undefined;
1887 try std.testing.expectEqual(@as(usize, 1), e.vecs(&v));
1888 try std.testing.expectEqualStrings("cd", v[0].base[0..v[0].len]);
1889 }
1890
1891 test "accountWrite: bytes ngtcp2 committed are accounted even when the call failed" {
1892 const alloc = std.testing.allocator;
1893 var e: Egress = .{ .buf = try alloc.alloc(u8, 16) };
1894 defer e.deinit(alloc);
1895 _ = e.push("abcdefgh");
1896 try std.testing.expectEqual(@as(usize, 8), e.unsent);
1897
1898 // The case that has no fault injection available and would otherwise go
1899 // untested: ngtcp2 committed four bytes of stream data — its offset has
1900 // moved — and THEN returned an error. Accounting after the check would
1901 // leave those four counted as unsent, so the next drain would offer them
1902 // again at an offset the peer is already past.
1903 try std.testing.expectEqual(WriteAction.stop, accountWrite(&e, 4, -1));
1904 try std.testing.expectEqual(@as(usize, 4), e.unsent);
1905
1906 // The ordinary returns, for completeness of the contract.
1907 try std.testing.expectEqual(WriteAction.brk, accountWrite(&e, 0, 0));
1908 try std.testing.expectEqual(WriteAction.cont, accountWrite(&e, 4, 120));
1909 try std.testing.expectEqual(@as(usize, 0), e.unsent);
1910 }
1911
1912 test "Egress: an ack against a torn-down ring is ignored, not a division by zero" {
1913 const alloc = std.testing.allocator;
1914 var e: Egress = .{ .buf = try alloc.alloc(u8, 8) };
1915 _ = e.push("abcd");
1916 e.took(4);
1917
1918 // Teardown leaves a zero-length buffer behind, and an ack can still
1919 // arrive for it: ngtcp2 delivers acked_stream_data during its own
1920 // shutdown, after the owner has freed the ring. The modulo in `ack`
1921 // divides by buf.len, so without the guard this is a division by zero
1922 // on a path nobody would think to look at.
1923 e.deinit(alloc);
1924 try std.testing.expectEqual(@as(usize, 0), e.buf.len);
1925 e.ack(4);
1926 try std.testing.expectEqual(@as(usize, 0), e.held);
1927 }
1928
1929 /// Get the peer's stream on the record. The server learns a stream id only 1470 /// Get the peer's stream on the record. The server learns a stream id only
1930 /// when data arrives on it — the client opening one locally tells the server 1471 /// when data arrives on it — the client opening one locally tells the server
1931 /// nothing — so a test that wants the server to SEND first has to make the 1472 /// nothing — so a test that wants the server to SEND first has to make the
@@ -1985,7 +1526,7 @@ fn pumpUntil(
1985 1526
1986 test "Listener: bytes survive retransmission, which is what the buffer is for" { 1527 test "Listener: bytes survive retransmission, which is what the buffer is for" {
1987 const alloc = std.testing.allocator; 1528 const alloc = std.testing.allocator;
1988 const key: Key = .{ .bytes = [_]u8{0x6E} ** key_len }; 1529 const key: quic.Key = .{ .bytes = [_]u8{0x6E} ** quic.key_len };
1989 1530
1990 var owner: EchoOwner = .{}; 1531 var owner: EchoOwner = .{};
1991 defer owner.deinit(); 1532 defer owner.deinit();
@@ -2020,7 +1561,7 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" {
2020 1561
2021 // Twice the egress ring, so the ring wraps and every byte's release 1562 // Twice the egress ring, so the ring wraps and every byte's release
2022 // depends on an ack that may itself have been for a retransmission. 1563 // depends on an ack that may itself have been for a retransmission.
2023 const size = 2 * egress_cap; 1564 const size = 2 * quic.egress_cap;
2024 const payload = try alloc.alloc(u8, size); 1565 const payload = try alloc.alloc(u8, size);
2025 defer alloc.free(payload); 1566 defer alloc.free(payload);
2026 for (payload, 0..) |*b, i| b.* = @truncate(i *% 31 +% 7); 1567 for (payload, 0..) |*b, i| b.* = @truncate(i *% 31 +% 7);
@@ -2048,7 +1589,7 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" {
2048 1589
2049 test "Listener.send: takes what fits, refuses when full, and recovers on acks" { 1590 test "Listener.send: takes what fits, refuses when full, and recovers on acks" {
2050 const alloc = std.testing.allocator; 1591 const alloc = std.testing.allocator;
2051 const key: Key = .{ .bytes = [_]u8{0x21} ** key_len }; 1592 const key: quic.Key = .{ .bytes = [_]u8{0x21} ** quic.key_len };
2052 1593
2053 var owner: EchoOwner = .{}; 1594 var owner: EchoOwner = .{};
2054 defer owner.deinit(); 1595 defer owner.deinit();
@@ -2067,7 +1608,7 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" {
2067 1608
2068 try openStream(setup.l, &cl, &owner); 1609 try openStream(setup.l, &cl, &owner);
2069 1610
2070 const big = try alloc.alloc(u8, egress_cap + 4096); 1611 const big = try alloc.alloc(u8, quic.egress_cap + 4096);
2071 defer alloc.free(big); 1612 defer alloc.free(big);
2072 @memset(big, 0x5A); 1613 @memset(big, 0x5A);
2073 1614
@@ -2077,9 +1618,9 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" {
2077 // whole point — an accept-everything send moved that backlog down here 1618 // whole point — an accept-everything send moved that backlog down here
2078 // where pending_cap could never see it. 1619 // where pending_cap could never see it.
2079 const first = try setup.l.send(owner.id, big); 1620 const first = try setup.l.send(owner.id, big);
2080 try std.testing.expectEqual(egress_cap, first); 1621 try std.testing.expectEqual(quic.egress_cap, first);
2081 try std.testing.expectEqual(@as(usize, 0), try setup.l.send(owner.id, "x")); 1622 try std.testing.expectEqual(@as(usize, 0), try setup.l.send(owner.id, "x"));
2082 try std.testing.expectEqual(egress_cap, setup.l.pendingBytes(owner.id)); 1623 try std.testing.expectEqual(quic.egress_cap, setup.l.pendingBytes(owner.id));
2083 1624
2084 // Now let the client read and acknowledge: space comes back, and it 1625 // Now let the client read and acknowledge: space comes back, and it
2085 // comes back from acks rather than from having handed bytes to ngtcp2. 1626 // comes back from acks rather than from having handed bytes to ngtcp2.
@@ -2091,7 +1632,7 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" {
2091 return o.listener.pendingBytes(o.id) == 0; 1632 return o.listener.pendingBytes(o.id) == 0;
2092 } 1633 }
2093 }.f)); 1634 }.f));
2094 try std.testing.expectEqual(egress_cap, cl.echoed); 1635 try std.testing.expectEqual(quic.egress_cap, cl.echoed);
2095 try std.testing.expect(try setup.l.send(owner.id, "room again") > 0); 1636 try std.testing.expect(try setup.l.send(owner.id, "room again") > 0);
2096 1637
2097 // An id nobody owns is an error, not a silent success. 1638 // An id nobody owns is an error, not a silent success.
@@ -2100,7 +1641,7 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" {
2100 1641
2101 test "Listener: a full peer window blocks the stream without stopping the ACKs" { 1642 test "Listener: a full peer window blocks the stream without stopping the ACKs" {
2102 const alloc = std.testing.allocator; 1643 const alloc = std.testing.allocator;
2103 const key: Key = .{ .bytes = [_]u8{0x4B} ** key_len }; 1644 const key: quic.Key = .{ .bytes = [_]u8{0x4B} ** quic.key_len };
2104 1645
2105 // Short enough that a connection nobody is servicing dies inside the 1646 // Short enough that a connection nobody is servicing dies inside the
2106 // test, which is exactly the failure being guarded against: treating 1647 // test, which is exactly the failure being guarded against: treating
@@ -2128,7 +1669,7 @@ test "Listener: a full peer window blocks the stream without stopping the ACKs"
2128 // stream credit runs out and stays out. 1669 // stream credit runs out and stays out.
2129 cl.extend = false; 1670 cl.extend = false;
2130 1671
2131 const big = try alloc.alloc(u8, 4 * egress_cap); 1672 const big = try alloc.alloc(u8, 4 * quic.egress_cap);
2132 defer alloc.free(big); 1673 defer alloc.free(big);
2133 @memset(big, 0x33); 1674 @memset(big, 0x33);
2134 try owner.backlog.appendSlice(alloc, big); 1675 try owner.backlog.appendSlice(alloc, big);
@@ -2151,7 +1692,7 @@ test "Listener: a full peer window blocks the stream without stopping the ACKs"
2151 1692
2152 test "Listener: a packet addressed to any advertised CID reaches its connection" { 1693 test "Listener: a packet addressed to any advertised CID reaches its connection" {
2153 const alloc = std.testing.allocator; 1694 const alloc = std.testing.allocator;
2154 const key: Key = .{ .bytes = [_]u8{0x9C} ** key_len }; 1695 const key: quic.Key = .{ .bytes = [_]u8{0x9C} ** quic.key_len };
2155 1696
2156 var owner: EchoOwner = .{}; 1697 var owner: EchoOwner = .{};
2157 defer owner.deinit(); 1698 defer owner.deinit();
@@ -2256,7 +1797,7 @@ fn probeGotAnything(fd: std.posix.fd_t) bool {
2256 1797
2257 test "Listener: a reply queued just before a close still reaches the peer" { 1798 test "Listener: a reply queued just before a close still reaches the peer" {
2258 const alloc = std.testing.allocator; 1799 const alloc = std.testing.allocator;
2259 const key: Key = .{ .bytes = [_]u8{0x5C} ** key_len }; 1800 const key: quic.Key = .{ .bytes = [_]u8{0x5C} ** quic.key_len };
2260 1801
2261 // The session-full refusal, in miniature: the owner answers and then 1802 // The session-full refusal, in miniature: the owner answers and then
2262 // shuts the connection, both from inside one callback. Since `send` only 1803 // shuts the connection, both from inside one callback. Since `send` only
@@ -2343,7 +1884,7 @@ test "Listener: a reply queued just before a close still reaches the peer" {
2343 1884
2344 test "Listener: closing a connection from inside a receive callback is deferred" { 1885 test "Listener: closing a connection from inside a receive callback is deferred" {
2345 const alloc = std.testing.allocator; 1886 const alloc = std.testing.allocator;
2346 const key: Key = .{ .bytes = [_]u8{0x7E} ** key_len }; 1887 const key: quic.Key = .{ .bytes = [_]u8{0x7E} ** quic.key_len };
2347 1888
2348 // An owner that does what the daemon does when a client says goodbye: 1889 // An owner that does what the daemon does when a client says goodbye:
2349 // closes the connection from inside onData. ngtcp2 is on the stack at 1890 // closes the connection from inside onData. ngtcp2 is on the stack at
src/server.zig
Old New
@@ -10,6 +10,7 @@ const Engine = @import("engine").Engine;
10 const Pty = @import("pty").Pty; 10 const Pty = @import("pty").Pty;
11 const proto = @import("protocol"); 11 const proto = @import("protocol");
12 const quic = @import("quic"); 12 const quic = @import("quic");
13 const quic_server = @import("quic_server");
13 const xdg = @import("xdg"); 14 const xdg = @import("xdg");
14 const TmpDir = @import("testtmp").TmpDir; 15 const TmpDir = @import("testtmp").TmpDir;
15 16
@@ -67,7 +68,7 @@ fn drainWaitMs(remaining: i64, quic_hint: ?i32) i32 {
67 /// this file's own quic test helper are the only things outside 68 /// this file's own quic test helper are the only things outside
68 /// quic_server.zig that have ever needed to ask, and that is not enough to 69 /// quic_server.zig that have ever needed to ask, and that is not enough to
69 /// widen that type's surface. 70 /// widen that type's surface.
70 fn boundUdpPort(l: *quic.Listener) u16 { 71 fn boundUdpPort(l: *quic_server.Listener) u16 {
71 var actual: std.posix.sockaddr.storage = undefined; 72 var actual: std.posix.sockaddr.storage = undefined;
72 var len: std.posix.socklen_t = @sizeOf(@TypeOf(actual)); 73 var len: std.posix.socklen_t = @sizeOf(@TypeOf(actual));
73 std.posix.getsockname(l.pollFd(), @ptrCast(&actual), &len) catch return 0; 74 std.posix.getsockname(l.pollFd(), @ptrCast(&actual), &len) catch return 0;
@@ -275,7 +276,7 @@ const Sink = union(enum) {
275 socket: std.posix.fd_t, 276 socket: std.posix.fd_t,
276 /// A QUIC peer: the listener that owns the shared UDP socket, plus the 277 /// A QUIC peer: the listener that owns the shared UDP socket, plus the
277 /// connection id within it. Note what is NOT here — a descriptor. 278 /// connection id within it. Note what is NOT here — a descriptor.
278 quic: struct { listener: *quic.Listener, id: u64 }, 279 quic: struct { listener: *quic_server.Listener, id: u64 },
279 280
280 /// The descriptor to poll for this client, or -1 for "nothing of its 281 /// The descriptor to poll for this client, or -1 for "nothing of its
281 /// own" — poll(2) ignores negative fds, which is exactly the behaviour 282 /// own" — poll(2) ignores negative fds, which is exactly the behaviour
@@ -401,7 +402,7 @@ pub const Server = struct {
401 /// The QUIC listener, when `--quic` was given. Optional by design: 402 /// The QUIC listener, when `--quic` was given. Optional by design:
402 /// QUIC is opt-in per invocation and the unix socket is unaffected by 403 /// QUIC is opt-in per invocation and the unix socket is unaffected by
403 /// its presence or absence. 404 /// its presence or absence.
404 quic_listener: ?*quic.Listener = null, 405 quic_listener: ?*quic_server.Listener = null,
405 /// True when the listener was bound lazily by endpoint_req, and is 406 /// True when the listener was bound lazily by endpoint_req, and is
406 /// therefore ours to deinit. A listener handed in by main.zig (the 407 /// therefore ours to deinit. A listener handed in by main.zig (the
407 /// explicit --quic path) has its own deferred deinit out there, and 408 /// explicit --quic path) has its own deferred deinit out there, and
@@ -929,7 +930,7 @@ pub const Server = struct {
929 return null; 930 return null;
930 } 931 }
931 932
932 pub fn quicHandler(self: *Server) quic.Handler { 933 pub fn quicHandler(self: *Server) quic_server.Handler {
933 return .{ 934 return .{
934 .ctx = self, 935 .ctx = self,
935 .onOpen = quicOnOpen, 936 .onOpen = quicOnOpen,
@@ -942,7 +943,7 @@ pub const Server = struct {
942 /// server does not own the socket, only the reference — deinit leaves 943 /// server does not own the socket, only the reference — deinit leaves
943 /// the listener to its creator, which keeps the ownership story the 944 /// the listener to its creator, which keeps the ownership story the
944 /// same as the unix listener's. 945 /// same as the unix listener's.
945 pub fn attachQuic(self: *Server, listener: *quic.Listener) void { 946 pub fn attachQuic(self: *Server, listener: *quic_server.Listener) void {
946 // Only before the loop runs: a lazily-bound listener in this field is 947 // Only before the loop runs: a lazily-bound listener in this field is
947 // ours to free (see quic_owned), and overwriting it would leak that 948 // ours to free (see quic_owned), and overwriting it would leak that
948 // one and hand deinit somebody else's to free instead. 949 // one and hand deinit somebody else's to free instead.
@@ -1054,7 +1055,7 @@ pub const Server = struct {
1054 /// here — setting the field IS the integration. 1055 /// here — setting the field IS the integration.
1055 fn lazyBindQuic(self: *Server, key: quic.Key) !u16 { 1056 fn lazyBindQuic(self: *Server, key: quic.Key) !u16 {
1056 const addr = try std.net.Address.parseIp("0.0.0.0", 0); 1057 const addr = try std.net.Address.parseIp("0.0.0.0", 0);
1057 const l = try quic.Listener.bind(self.alloc, addr, key, quic.default_idle_ms); 1058 const l = try quic_server.Listener.bind(self.alloc, addr, key, quic.default_idle_ms);
1058 l.setHandler(self.quicHandler()); 1059 l.setHandler(self.quicHandler());
1059 self.quic_listener = l; 1060 self.quic_listener = l;
1060 self.quic_owned = true; 1061 self.quic_owned = true;
@@ -3897,7 +3898,7 @@ test "Server: a listener attached by the caller survives srv.deinit — ownershi
3897 // under test stopped guarding, and a use-after-free is not a test 3898 // under test stopped guarding, and a use-after-free is not a test
3898 // result anyone can read. 3899 // result anyone can read.
3899 const addr = try std.net.Address.parseIp("0.0.0.0", 0); 3900 const addr = try std.net.Address.parseIp("0.0.0.0", 0);
3900 const l = try quic.Listener.bind(alloc, addr, key, quic.default_idle_ms); 3901 const l = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
3901 const port = boundUdpPort(l); 3902 const port = boundUdpPort(l);
3902 try std.testing.expect(port != 0); 3903 try std.testing.expect(port != 0);
3903 3904
@@ -3926,12 +3927,12 @@ test "Server: a listener attached by the caller survives srv.deinit — ownershi
3926 // double-free hang in unrelated tests. 3927 // double-free hang in unrelated tests.
3927 try std.testing.expectError( 3928 try std.testing.expectError(
3928 error.ListenerAlreadyRunning, 3929 error.ListenerAlreadyRunning,
3929 quic.Listener.bind(alloc, addr, key, quic.default_idle_ms), 3930 quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms),
3930 ); 3931 );
3931 3932
3932 // And it is still ours to free, which is the other half of "survived". 3933 // And it is still ours to free, which is the other half of "survived".
3933 l.deinit(); 3934 l.deinit();
3934 const l2 = try quic.Listener.bind(alloc, addr, key, quic.default_idle_ms); 3935 const l2 = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
3935 l2.deinit(); 3936 l2.deinit();
3936 } 3937 }
3937 3938
@@ -3946,9 +3947,9 @@ test "Server: a listener attached by the caller survives srv.deinit — ownershi
3946 3947
3947 /// Bring a Server up with a QUIC listener bound to an ephemeral loopback 3948 /// Bring a Server up with a QUIC listener bound to an ephemeral loopback
3948 /// port, and hand back the address a client should dial. 3949 /// port, and hand back the address a client should dial.
3949 fn quicTestServer(srv: *Server, key: quic.Key) !struct { l: *quic.Listener, addr: std.net.Address } { 3950 fn quicTestServer(srv: *Server, key: quic.Key) !struct { l: *quic_server.Listener, addr: std.net.Address } {
3950 const bind = try std.net.Address.parseIp("127.0.0.1", 0); 3951 const bind = try std.net.Address.parseIp("127.0.0.1", 0);
3951 const l = try quic.Listener.init(srv.alloc, bind, key, srv.quicHandler(), 5000); 3952 const l = try quic_server.Listener.init(srv.alloc, bind, key, srv.quicHandler(), 5000);
3952 srv.attachQuic(l); 3953 srv.attachQuic(l);
3953 var actual: std.posix.sockaddr.storage = undefined; 3954 var actual: std.posix.sockaddr.storage = undefined;
3954 var len: std.posix.socklen_t = @sizeOf(@TypeOf(actual)); 3955 var len: std.posix.socklen_t = @sizeOf(@TypeOf(actual));
@@ -3961,7 +3962,7 @@ fn quicTestServer(srv: *Server, key: quic.Key) !struct { l: *quic.Listener, addr
3961 /// listener, which is the integration under test. 3962 /// listener, which is the integration under test.
3962 fn quicPump( 3963 fn quicPump(
3963 srv: *Server, 3964 srv: *Server,
3964 clients: []*quic.TestClient, 3965 clients: []*quic_server.TestClient,
3965 budget_ms: u64, 3966 budget_ms: u64,
3966 ctx: anytype, 3967 ctx: anytype,
3967 done: *const fn (@TypeOf(ctx)) bool, 3968 done: *const fn (@TypeOf(ctx)) bool,
@@ -3999,7 +4000,7 @@ fn findFrame(bytes: []const u8, want: proto.MsgType) ?[]const u8 {
3999 return null; 4000 return null;
4000 } 4001 }
4001 4002
4002 fn attachOver(cl: *quic.TestClient, buf: *std.ArrayList(u8), alloc: std.mem.Allocator) !void { 4003 fn attachOver(cl: *quic_server.TestClient, buf: *std.ArrayList(u8), alloc: std.mem.Allocator) !void {
4003 try proto.appendFrame(buf, alloc, .attach, &proto.encodeAttach(80, 24, 0, 0)); 4004 try proto.appendFrame(buf, alloc, .attach, &proto.encodeAttach(80, 24, 0, 0));
4004 cl.out = buf.items; 4005 cl.out = buf.items;
4005 cl.drain(); 4006 cl.drain();
@@ -4091,7 +4092,7 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
4091 const q = try quicTestServer(&srv, key); 4092 const q = try quicTestServer(&srv, key);
4092 defer q.l.deinit(); 4093 defer q.l.deinit();
4093 4094
4094 var cl = try quic.TestClient.init(q.addr, key); 4095 var cl = try quic_server.TestClient.init(q.addr, key);
4095 defer cl.deinit(); 4096 defer cl.deinit();
4096 try cl.start(); 4097 try cl.start();
4097 cl.drain(); 4098 cl.drain();
@@ -4100,9 +4101,9 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea
4100 defer out.deinit(alloc); 4101 defer out.deinit(alloc);
4101 try attachOver(&cl, &out, alloc); 4102 try attachOver(&cl, &out, alloc);
4102 4103
4103 var only = [_]*quic.TestClient{&cl}; 4104 var only = [_]*quic_server.TestClient{&cl};
4104 _ = try quicPump(&srv, &only, 8000, &cl, struct { 4105 _ = try quicPump(&srv, &only, 8000, &cl, struct {
4105 fn f(t: *quic.TestClient) bool { 4106 fn f(t: *quic_server.TestClient) bool {
4106 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; 4107 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null;
4107 } 4108 }
4108 }.f); 4109 }.f);
@@ -4179,7 +4180,7 @@ test "Server: a QUIC client still receives the shell's exit status" {
4179 const q = try quicTestServer(&srv, key); 4180 const q = try quicTestServer(&srv, key);
4180 defer q.l.deinit(); 4181 defer q.l.deinit();
4181 4182
4182 var cl = try quic.TestClient.init(q.addr, key); 4183 var cl = try quic_server.TestClient.init(q.addr, key);
4183 defer cl.deinit(); 4184 defer cl.deinit();
4184 try cl.start(); 4185 try cl.start();
4185 cl.drain(); 4186 cl.drain();
@@ -4190,9 +4191,9 @@ test "Server: a QUIC client still receives the shell's exit status" {
4190 4191
4191 // Attached and carrying state: the frame path works before we start 4192 // Attached and carrying state: the frame path works before we start
4192 // asking about the harder one. 4193 // asking about the harder one.
4193 var only = [_]*quic.TestClient{&cl}; 4194 var only = [_]*quic_server.TestClient{&cl};
4194 _ = try quicPump(&srv, &only, 8000, &cl, struct { 4195 _ = try quicPump(&srv, &only, 8000, &cl, struct {
4195 fn f(t: *quic.TestClient) bool { 4196 fn f(t: *quic_server.TestClient) bool {
4196 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; 4197 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null;
4197 } 4198 }
4198 }.f); 4199 }.f);
@@ -4206,7 +4207,7 @@ test "Server: a QUIC client still receives the shell's exit status" {
4206 cl.drain(); 4207 cl.drain();
4207 4208
4208 const code = try quicPump(&srv, &only, 15000, &cl, struct { 4209 const code = try quicPump(&srv, &only, 15000, &cl, struct {
4209 fn f(t: *quic.TestClient) bool { 4210 fn f(t: *quic_server.TestClient) bool {
4210 return findFrame(t.recv_buf[0..t.recv_len], .exit_status) != null; 4211 return findFrame(t.recv_buf[0..t.recv_len], .exit_status) != null;
4211 } 4212 }
4212 }.f); 4213 }.f);
@@ -4232,11 +4233,11 @@ test "Server: one QUIC client leaving does not disturb the other" {
4232 const q = try quicTestServer(&srv, key); 4233 const q = try quicTestServer(&srv, key);
4233 defer q.l.deinit(); 4234 defer q.l.deinit();
4234 4235
4235 var a = try quic.TestClient.init(q.addr, key); 4236 var a = try quic_server.TestClient.init(q.addr, key);
4236 defer a.deinit(); 4237 defer a.deinit();
4237 try a.start(); 4238 try a.start();
4238 a.drain(); 4239 a.drain();
4239 var b = try quic.TestClient.init(q.addr, key); 4240 var b = try quic_server.TestClient.init(q.addr, key);
4240 defer b.deinit(); 4241 defer b.deinit();
4241 try b.start(); 4242 try b.start();
4242 b.drain(); 4243 b.drain();
@@ -4248,9 +4249,9 @@ test "Server: one QUIC client leaving does not disturb the other" {
4248 try attachOver(&a, &abuf, alloc); 4249 try attachOver(&a, &abuf, alloc);
4249 try attachOver(&b, &bbuf, alloc); 4250 try attachOver(&b, &bbuf, alloc);
4250 4251
4251 var both = [_]*quic.TestClient{ &a, &b }; 4252 var both = [_]*quic_server.TestClient{ &a, &b };
4252 _ = try quicPump(&srv, &both, 10000, &b, struct { 4253 _ = try quicPump(&srv, &both, 10000, &b, struct {
4253 fn f(t: *quic.TestClient) bool { 4254 fn f(t: *quic_server.TestClient) bool {
4254 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; 4255 return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null;
4255 } 4256 }
4256 }.f); 4257 }.f);
@@ -4281,7 +4282,7 @@ test "Server: one QUIC client leaving does not disturb the other" {
4281 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); 4282 var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 });
4282 defer replica.deinit(); 4283 defer replica.deinit();
4283 _ = try quicPump(&srv, &both, 15000, &b, struct { 4284 _ = try quicPump(&srv, &both, 15000, &b, struct {
4284 fn f(t: *quic.TestClient) bool { 4285 fn f(t: *quic_server.TestClient) bool {
4285 return t.recv_len > 0 and findFrame(t.recv_buf[0..t.recv_len], .delta) != null; 4286 return t.recv_len > 0 and findFrame(t.recv_buf[0..t.recv_len], .delta) != null;
4286 } 4287 }
4287 }.f); 4288 }.f);
@@ -4302,7 +4303,7 @@ test "Server: a QUIC client that stops reading is dropped by the cap, not tolera
4302 defer q.l.deinit(); 4303 defer q.l.deinit();
4303 defer srv.deinit(); 4304 defer srv.deinit();
4304 4305
4305 var cl = try quic.TestClient.init(q.addr, key); 4306 var cl = try quic_server.TestClient.init(q.addr, key);
4306 defer cl.deinit(); 4307 defer cl.deinit();
4307 try cl.start(); 4308 try cl.start();
4308 4309
@@ -4310,9 +4311,9 @@ test "Server: a QUIC client that stops reading is dropped by the cap, not tolera
4310 defer bbuf.deinit(alloc); 4311 defer bbuf.deinit(alloc);
4311 try attachOver(&cl, &bbuf, alloc); 4312 try attachOver(&cl, &bbuf, alloc);
4312 4313
4313 var only = [_]*quic.TestClient{&cl}; 4314 var only = [_]*quic_server.TestClient{&cl};
4314 _ = try quicPump(&srv, &only, 8000, &cl, struct { 4315 _ = try quicPump(&srv, &only, 8000, &cl, struct {
4315 fn f(t: *quic.TestClient) bool { 4316 fn f(t: *quic_server.TestClient) bool {
4316 return t.handshake_done and t.echoed > 0; 4317 return t.handshake_done and t.echoed > 0;
4317 } 4318 }
4318 }.f); 4319 }.f);
@@ -4358,7 +4359,7 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" {
4358 defer q.l.deinit(); 4359 defer q.l.deinit();
4359 defer srv.deinit(); 4360 defer srv.deinit();
4360 4361
4361 var cl = try quic.TestClient.init(q.addr, key); 4362 var cl = try quic_server.TestClient.init(q.addr, key);
4362 defer cl.deinit(); 4363 defer cl.deinit();
4363 try cl.start(); 4364 try cl.start();
4364 4365
@@ -4366,9 +4367,9 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" {
4366 defer bbuf.deinit(alloc); 4367 defer bbuf.deinit(alloc);
4367 try attachOver(&cl, &bbuf, alloc); 4368 try attachOver(&cl, &bbuf, alloc);
4368 4369
4369 var only = [_]*quic.TestClient{&cl}; 4370 var only = [_]*quic_server.TestClient{&cl};
4370 _ = try quicPump(&srv, &only, 8000, &cl, struct { 4371 _ = try quicPump(&srv, &only, 8000, &cl, struct {
4371 fn f(t: *quic.TestClient) bool { 4372 fn f(t: *quic_server.TestClient) bool {
4372 return t.handshake_done and t.echoed > 0; 4373 return t.handshake_done and t.echoed > 0;
4373 } 4374 }
4374 }.f); 4375 }.f);
@@ -4393,7 +4394,7 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" {
4393 var done = false; 4394 var done = false;
4394 const t0 = std.time.milliTimestamp(); 4395 const t0 = std.time.milliTimestamp();
4395 const th = try std.Thread.spawn(.{}, struct { 4396 const th = try std.Thread.spawn(.{}, struct {
4396 fn f(client: *quic.TestClient, flag: *bool) void { 4397 fn f(client: *quic_server.TestClient, flag: *bool) void {
4397 // The peer: reads and acknowledges until the daemon says stop. 4398 // The peer: reads and acknowledges until the daemon says stop.
4398 while (!flag.*) { 4399 while (!flag.*) {
4399 client.drain(); 4400 client.drain();
@@ -4581,7 +4582,7 @@ test "Server: stop_req from an attached client is honored too" {
4581 // --------------------------------------------------------------------------- 4582 // ---------------------------------------------------------------------------
4582 // endpoint_req: the lazy QUIC bind. 4583 // endpoint_req: the lazy QUIC bind.
4583 // 4584 //
4584 // Consolidated into two tests on purpose. `quic.Listener` keeps a 4585 // Consolidated into two tests on purpose. `quic_server.Listener` keeps a
4585 // process-global "one listener at a time" latch, so every binding test has to 4586 // process-global "one listener at a time" latch, so every binding test has to
4586 // give it back before the next one asks — fewer tests is fewer places that 4587 // give it back before the next one asks — fewer tests is fewer places that
4587 // can fail to. 4588 // can fail to.
@@ -4687,7 +4688,7 @@ test "Server: endpoint_req binds a listener lazily, answers the same port on bot
4687 // observable for that: the latch is only released by a deinit that 4688 // observable for that: the latch is only released by a deinit that
4688 // happened, so a fresh bind succeeding proves the old one is gone. 4689 // happened, so a fresh bind succeeding proves the old one is gone.
4689 const addr = try std.net.Address.parseIp("0.0.0.0", 0); 4690 const addr = try std.net.Address.parseIp("0.0.0.0", 0);
4690 const l2 = try quic.Listener.bind(alloc, addr, key, quic.default_idle_ms); 4691 const l2 = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
4691 l2.deinit(); 4692 l2.deinit();
4692 } 4693 }
4693 4694