b34e7a1a
refactor: src/quic.zig owns the shared QUIC vocabulary; quic_server is just the listener
a73x 2026-08-12 18:04
Commit message
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, ¶ms); | 1121 | c.ngtcp2_transport_params_default_versioned(c.NGTCP2_TRANSPORT_PARAMS_VERSION, ¶ms); |
| 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 | ||