c651e9a0
refactor: one QUIC client, dialled by the product and by the tests
a73x 2026-08-29 10:01
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -51,7 +51,7 @@ engine and a client can link those folders and paint its own way: | |||
| 51 | |---|---| | 51 | |---|---| |
| 52 | | `src/engine/` | `protocol` `engine` `delta` `replica` `predict` | | 52 | | `src/engine/` | `protocol` `engine` `delta` `replica` `predict` | |
| 53 | | `src/server/` | `server`(+`_agent` `_sessions` `_test_*`) `pty` `quic_server` `cmd` `shellint` `upgrade` | | 53 | | `src/server/` | `server`(+`_agent` `_sessions` `_test_*`) `pty` `quic_server` `cmd` `shellint` `upgrade` | |
| 54 | | `src/client/` | `client` `client_core` `hosts` `handoff` `quic_client` `layout` `wall` `webhub` `keymap` `wasm_core` | | 54 | | `src/client/` | `client` `client_core` `hosts` `handoff` `layout` `wall` `webhub` `keymap` `wasm_core` | |
| 55 | | `src/tui/` | `wallview`(+`wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*`) `interact` `paint` `select` | | 55 | | `src/tui/` | `wallview`(+`wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*`) `interact` `paint` `select` | |
| 56 | | `src/cli/` | `mux`(dispatch) `main`(daemon) `mux_main`(client) `muxa`(agent) `webhub_main`(hub) `flags` `spawn` | | 56 | | `src/cli/` | `mux`(dispatch) `main`(daemon) `mux_main`(client) `muxa`(agent) `webhub_main`(hub) `flags` `spawn` | |
| 57 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | | 57 | | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | |
| @@ -77,7 +77,7 @@ Layers are enforced in the same module table (grep `.layer =` for the graph). | |||
| 77 | | Layer | Modules | | 77 | | Layer | Modules | |
| 78 | |---|---| | 78 | |---|---| |
| 79 | | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` | | 79 | | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` | |
| 80 | | 1 | `client_core` `quic_server` `quic_client` `predict` `spawn`(cli) `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` `select` `upgrade` | | 80 | | 1 | `client_core` `quic_server` `predict` `spawn`(cli) `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` `select` `upgrade` | |
| 81 | | 2 | `server` `agent_main` `interact` `hosts` | | 81 | | 2 | `server` `agent_main` `interact` `hosts` | |
| 82 | | 3 | `client` `daemon_main` | | 82 | | 3 | `client` `daemon_main` | |
| 83 | | 4 | `webhub` `wallview` | | 83 | | 4 | `webhub` `wallview` | |
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -163,12 +163,6 @@ const mod_table = [_]ModSpec{ | |||
| 163 | // table, and deliberately no protocol import — it carries opaque bytes, | 163 | // table, and deliberately no protocol import — it carries opaque bytes, |
| 164 | // exactly as proxy.zig does. | 164 | // exactly as proxy.zig does. |
| 165 | .{ .name = "quic_server", .path = "src/server/quic_server.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, | 165 | .{ .name = "quic_server", .path = "src/server/quic_server.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, |
| 166 | // The client's QUIC transport. Imports the vocabulary module — NOT the | ||
| 167 | // listener — for the pieces both ends must agree on (the key, the egress | ||
| 168 | // ring's lifetime discipline, the PSK identity and ALPN); duplicating | ||
| 169 | // those would make a handshake failure the first sign they had drifted. | ||
| 170 | // It has no business knowing a listener exists, and now it cannot. | ||
| 171 | .{ .name = "quic_client", .path = "src/client/quic_client.zig", .layer = 1, .link_libc = true, .imports = &.{"quic"}, .quic_tests = true }, | ||
| 172 | // Speculative local echo: the overlay and its policy, and deliberately | 166 | // Speculative local echo: the overlay and its policy, and deliberately |
| 173 | // nothing else. No engine import, which is what lets the whole state | 167 | // nothing else. No engine import, which is what lets the whole state |
| 174 | // machine be exercised without a terminal — or a daemon — anywhere in | 168 | // machine be exercised without a terminal — or a daemon — anywhere in |
| @@ -259,10 +253,10 @@ const mod_table = [_]ModSpec{ | |||
| 259 | // The agent-facing client. It speaks frames and owns no terminal, which | 253 | // The agent-facing client. It speaks frames and owns no terminal, which |
| 260 | // is the whole point — it attaches at 0x0 and never claims the grid. | 254 | // is the whole point — it attaches at 0x0 and never claims the grid. |
| 261 | // The transport modules are the CLI client's, minus everything that | 255 | // The transport modules are the CLI client's, minus everything that |
| 262 | // renders: `quic_client` for the remote arm and `xdg` for the one | 256 | // renders: `quic` for the remote arm and `xdg` for the one |
| 263 | // key-resolution rule all three binaries obey. Deliberately still no | 257 | // key-resolution rule all three binaries obey. Deliberately still no |
| 264 | // engine and no replica — muxa has nothing to draw. | 258 | // engine and no replica — muxa has nothing to draw. |
| 265 | .{ .name = "agent_main", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic_client", "quic", "xdg", "cliflags" }, .quic_tests = true }, | 259 | .{ .name = "agent_main", .path = "src/cli/muxa.zig", .layer = 2, .link_libc = true, .imports = &.{ "protocol", "sockpath", "quic", "xdg", "cliflags" }, .quic_tests = true }, |
| 266 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, | 260 | .{ .name = "wsclient", .path = "test/wsclient.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "replica", "protocol", "script" } }, |
| 267 | // ---- layer 3 ---- | 261 | // ---- layer 3 ---- |
| 268 | // Dialling, and what a chord means. The client is the only thing that | 262 | // Dialling, and what a chord means. The client is the only thing that |
| @@ -274,7 +268,7 @@ const mod_table = [_]ModSpec{ | |||
| 274 | // attach records its own tile (the wall is attach history), and the | 268 | // attach records its own tile (the wall is attach history), and the |
| 275 | // chord switches that re-dial from inside client.attach have to record | 269 | // chord switches that re-dial from inside client.attach have to record |
| 276 | // theirs too, so the writer cannot live up in mux_main. | 270 | // theirs too, so the writer cannot live up in mux_main. |
| 277 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic_client", "quic", "handoff", "wall" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 271 | .{ .name = "client", .path = "src/client/client.zig", .layer = 3, .link_libc = true, .imports = &.{ "protocol", "replica", "keymap", "quic", "handoff", "wall" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 278 | // The daemon entrypoint loads the key and constructs the listener, so | 272 | // The daemon entrypoint loads the key and constructs the listener, so |
| 279 | // it needs quic/quic_server directly rather than through the server. | 273 | // it needs quic/quic_server directly rather than through the server. |
| 280 | // `muxd endpoint` prints the announce line handoff spells; sockpath is | 274 | // `muxd endpoint` prints the announce line handoff spells; sockpath is |
| @@ -806,12 +800,12 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 806 | /// they carry the argument parsers — a test that is never built is not a | 800 | /// they carry the argument parsers — a test that is never built is not a |
| 807 | /// test (decisions.md). | 801 | /// test (decisions.md). |
| 808 | const test_order = [_][]const u8{ | 802 | const test_order = [_][]const u8{ |
| 809 | "script", "select", "protocol", "client_core", "interact", "engine", "pty", | 803 | "script", "select", "protocol", "client_core", "interact", "engine", "pty", |
| 810 | "delta", "cmd", "wall", "hosts", "upgrade", "shellint", "replica", | 804 | "delta", "cmd", "wall", "hosts", "upgrade", "shellint", "replica", |
| 811 | "keymap", "webhub", "wallview", "sockpath", "agent_main", "server", "client", | 805 | "keymap", "webhub", "wallview", "sockpath", "agent_main", "server", "client", |
| 812 | "proxy", "client_main", "quic", "quic_server", "daemon_main", "testtmp", "quic_client", | 806 | "proxy", "client_main", "quic", "quic_server", "daemon_main", "testtmp", "predict", |
| 813 | "predict", "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint", | 807 | "rawmode", "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", |
| 814 | "layout", "render", "ptyclient", "hub_main", "wsclient", "cliflags", "mux", | 808 | "render", "ptyclient", "hub_main", "wsclient", "cliflags", "mux", |
| 815 | }; | 809 | }; |
| 816 | 810 | ||
| 817 | comptime { | 811 | comptime { |
src/cli/muxa.zig
| Old | New | ||
|---|---|---|---|
| @@ -17,7 +17,6 @@ | |||
| 17 | const std = @import("std"); | 17 | const std = @import("std"); |
| 18 | const proto = @import("protocol"); | 18 | const proto = @import("protocol"); |
| 19 | const sockpath = @import("sockpath"); | 19 | const sockpath = @import("sockpath"); |
| 20 | const quic_client = @import("quic_client"); | ||
| 21 | const quic = @import("quic"); | 20 | const quic = @import("quic"); |
| 22 | const xdg = @import("xdg"); | 21 | const xdg = @import("xdg"); |
| 23 | const cliflags = @import("cliflags"); | 22 | const cliflags = @import("cliflags"); |
| @@ -307,9 +306,9 @@ test "parseArgs: --quic and --key, and the pairs that make no sense" { | |||
| 307 | /// rotated under us) and would then fail the handshake for a reason that | 306 | /// rotated under us) and would then fail the handshake for a reason that |
| 308 | /// has nothing to do with why the first connection died. | 307 | /// has nothing to do with why the first connection died. |
| 309 | const Quic = struct { | 308 | const Quic = struct { |
| 310 | cl: *quic_client.Client, | 309 | cl: *quic.Client, |
| 311 | addr: std.net.Address, | 310 | addr: std.net.Address, |
| 312 | key: quic_client.Key, | 311 | key: quic.Key, |
| 313 | idle_ms: u32, | 312 | idle_ms: u32, |
| 314 | /// Wall-clock milliseconds the FIRST handshake took, which is this | 313 | /// Wall-clock milliseconds the FIRST handshake took, which is this |
| 315 | /// client's only measurement of how far away the daemon is. `graceMs` | 314 | /// client's only measurement of how far away the daemon is. `graceMs` |
| @@ -372,12 +371,12 @@ const Conn = struct { | |||
| 372 | fn openQuic( | 371 | fn openQuic( |
| 373 | alloc: std.mem.Allocator, | 372 | alloc: std.mem.Allocator, |
| 374 | addr: std.net.Address, | 373 | addr: std.net.Address, |
| 375 | key: quic_client.Key, | 374 | key: quic.Key, |
| 376 | idle_ms: u32, | 375 | idle_ms: u32, |
| 377 | deadline_ms: i64, | 376 | deadline_ms: i64, |
| 378 | ) !Conn { | 377 | ) !Conn { |
| 379 | const started = std.time.milliTimestamp(); | 378 | const started = std.time.milliTimestamp(); |
| 380 | const cl = try quic_client.Client.connect(alloc, addr, key, idle_ms); | 379 | const cl = try quic.Client.connect(alloc, addr, key, idle_ms); |
| 381 | errdefer cl.deinit(); | 380 | errdefer cl.deinit(); |
| 382 | try waitReady(cl, deadline_ms); | 381 | try waitReady(cl, deadline_ms); |
| 383 | return .{ | 382 | return .{ |
| @@ -564,7 +563,7 @@ const Conn = struct { | |||
| 564 | /// it after this point anyway. | 563 | /// it after this point anyway. |
| 565 | fn reconnect(self: *Conn, deadline_ms: i64) !void { | 564 | fn reconnect(self: *Conn, deadline_ms: i64) !void { |
| 566 | const q = &self.link.quic; | 565 | const q = &self.link.quic; |
| 567 | const cl = try quic_client.Client.connect(self.alloc, q.addr, q.key, q.idle_ms); | 566 | const cl = try quic.Client.connect(self.alloc, q.addr, q.key, q.idle_ms); |
| 568 | errdefer cl.deinit(); | 567 | errdefer cl.deinit(); |
| 569 | try waitReady(cl, deadline_ms); | 568 | try waitReady(cl, deadline_ms); |
| 570 | q.cl.deinit(); | 569 | q.cl.deinit(); |
| @@ -611,13 +610,13 @@ test "graceMs: flat over a socket, RTT-derived over QUIC, and capped" { | |||
| 611 | 610 | ||
| 612 | /// Drive a fresh connection until it can carry bytes, or give up. | 611 | /// Drive a fresh connection until it can carry bytes, or give up. |
| 613 | /// | 612 | /// |
| 614 | /// A refused port ends this early — quic_client turns the ICMP unreachable | 613 | /// A refused port ends this early — quic.Client turns the ICMP unreachable |
| 615 | /// into `dead` — so the common mistake (no daemon on that port) costs | 614 | /// into `dead` — so the common mistake (no daemon on that port) costs |
| 616 | /// milliseconds. A blackholed one produces no error at all, and there the | 615 | /// milliseconds. A blackholed one produces no error at all, and there the |
| 617 | /// deadline is the only thing that ends the wait; even an unbounded one | 616 | /// deadline is the only thing that ends the wait; even an unbounded one |
| 618 | /// (`--timeout 0`) terminates, because the connection's own idle timeout | 617 | /// (`--timeout 0`) terminates, because the connection's own idle timeout |
| 619 | /// kills it after `idle_ms`. | 618 | /// kills it after `idle_ms`. |
| 620 | fn waitReady(cl: *quic_client.Client, deadline_ms: i64) !void { | 619 | fn waitReady(cl: *quic.Client, deadline_ms: i64) !void { |
| 621 | while (true) { | 620 | while (true) { |
| 622 | cl.pump(); | 621 | cl.pump(); |
| 623 | if (cl.isReady()) return; | 622 | if (cl.isReady()) return; |
| @@ -635,11 +634,11 @@ fn waitReady(cl: *quic_client.Client, deadline_ms: i64) !void { | |||
| 635 | test "reconnect: redials the same coordinates, and a dead port is a fast no" { | 634 | test "reconnect: redials the same coordinates, and a dead port is a fast no" { |
| 636 | const alloc = std.testing.allocator; | 635 | const alloc = std.testing.allocator; |
| 637 | // 127.0.0.1:1, where nothing listens: the refusal is REAL — an ICMP | 636 | // 127.0.0.1:1, where nothing listens: the refusal is REAL — an ICMP |
| 638 | // unreachable comes back and quic_client acts on it — which is what | 637 | // unreachable comes back and the quic client acts on it — which is what |
| 639 | // lets this exercise the whole redial path (dial, handshake wait, | 638 | // lets this exercise the whole redial path (dial, handshake wait, |
| 640 | // verdict) in a couple of loopback round trips instead of a timeout. | 639 | // verdict) in a couple of loopback round trips instead of a timeout. |
| 641 | const addr = try std.net.Address.parseIp("127.0.0.1", 1); | 640 | const addr = try std.net.Address.parseIp("127.0.0.1", 1); |
| 642 | const key: quic_client.Key = .{ .bytes = [_]u8{7} ** quic_client.key_len }; | 641 | const key: quic.Key = .{ .bytes = [_]u8{7} ** quic.key_len }; |
| 643 | 642 | ||
| 644 | // The dial that stands in for the connection this client had before | 643 | // The dial that stands in for the connection this client had before |
| 645 | // the network went away. It dies for the same reason the redial will, | 644 | // the network went away. It dies for the same reason the redial will, |
| @@ -648,7 +647,7 @@ test "reconnect: redials the same coordinates, and a dead port is a fast no" { | |||
| 648 | const deadline = std.time.milliTimestamp() + 2_000; | 647 | const deadline = std.time.milliTimestamp() + 2_000; |
| 649 | var conn = Conn{ | 648 | var conn = Conn{ |
| 650 | .link = .{ .quic = .{ | 649 | .link = .{ .quic = .{ |
| 651 | .cl = try quic_client.Client.connect(alloc, addr, key, 1_000), | 650 | .cl = try quic.Client.connect(alloc, addr, key, 1_000), |
| 652 | .addr = addr, | 651 | .addr = addr, |
| 653 | .key = key, | 652 | .key = key, |
| 654 | .idle_ms = 1_000, | 653 | .idle_ms = 1_000, |
| @@ -1211,12 +1210,12 @@ fn openQuicConn( | |||
| 1211 | p, | 1210 | p, |
| 1212 | ) }, | 1211 | ) }, |
| 1213 | }; | 1212 | }; |
| 1214 | const key = quic_client.Key.load(key_path) catch |e| { | 1213 | const key = quic.Key.load(key_path) catch |e| { |
| 1215 | // The daemon's words for a key the daemon would also refuse — | 1214 | // The daemon's words for a key the daemon would also refuse — |
| 1216 | // including the group/other-readable refusal, which this binary | 1215 | // including the group/other-readable refusal, which this binary |
| 1217 | // gets for free by loading the key the same way. | 1216 | // gets for free by loading the key the same way. |
| 1218 | var buf: [quic_client.key_refusal_len]u8 = undefined; | 1217 | var buf: [quic.key_refusal_len]u8 = undefined; |
| 1219 | return .{ .exit = fail("quic: unusable key", quic_client.keyRefusalBody(&buf, e, key_path)) }; | 1218 | return .{ .exit = fail("quic: unusable key", quic.keyRefusalBody(&buf, e, key_path)) }; |
| 1220 | }; | 1219 | }; |
| 1221 | const addr = quic.parseAddr(alloc, host_port) catch |e| { | 1220 | const addr = quic.parseAddr(alloc, host_port) catch |e| { |
| 1222 | var buf: [512]u8 = undefined; | 1221 | var buf: [512]u8 = undefined; |
| @@ -1224,7 +1223,7 @@ fn openQuicConn( | |||
| 1224 | @errorName(e); | 1223 | @errorName(e); |
| 1225 | return .{ .exit = fail("quic: cannot read HOST:PORT", detail) }; | 1224 | return .{ .exit = fail("quic: cannot read HOST:PORT", detail) }; |
| 1226 | }; | 1225 | }; |
| 1227 | const conn = Conn.openQuic(alloc, addr, key, quic_client.default_idle_ms, deadline) catch |e| { | 1226 | const conn = Conn.openQuic(alloc, addr, key, quic.default_idle_ms, deadline) catch |e| { |
| 1228 | var buf: [512]u8 = undefined; | 1227 | var buf: [512]u8 = undefined; |
| 1229 | const detail = std.fmt.bufPrint(&buf, "{s}: {s}", .{ host_port, @errorName(e) }) catch | 1228 | const detail = std.fmt.bufPrint(&buf, "{s}: {s}", .{ host_port, @errorName(e) }) catch |
| 1230 | @errorName(e); | 1229 | @errorName(e); |
src/client/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -20,7 +20,6 @@ const Replica = @import("replica").Replica; | |||
| 20 | const keymap = @import("keymap"); | 20 | const keymap = @import("keymap"); |
| 21 | const proto = @import("protocol"); | 21 | const proto = @import("protocol"); |
| 22 | const TmpDir = @import("testtmp").TmpDir; | 22 | const TmpDir = @import("testtmp").TmpDir; |
| 23 | const quic_client = @import("quic_client"); | ||
| 24 | const quic = @import("quic"); | 23 | const quic = @import("quic"); |
| 25 | const handoff = @import("handoff"); | 24 | const handoff = @import("handoff"); |
| 26 | 25 | ||
| @@ -161,8 +160,8 @@ pub fn lostMsg(target: Target, session_epoch: u64) []const u8 { | |||
| 161 | 160 | ||
| 162 | /// The client's name for the shared default; see `quic.default_idle_ms` | 161 | /// The client's name for the shared default; see `quic.default_idle_ms` |
| 163 | /// for what the number means and why it lives there. | 162 | /// for what the number means and why it lives there. |
| 164 | pub const quic_idle_ms_default: u32 = quic_client.default_idle_ms; | 163 | pub const quic_idle_ms_default: u32 = quic.default_idle_ms; |
| 165 | pub const IdleMs = quic_client.IdleMs; | 164 | pub const IdleMs = quic.IdleMs; |
| 166 | 165 | ||
| 167 | /// What a QUIC attach was asked for: where to dial and what key to prove | 166 | /// What a QUIC attach was asked for: where to dial and what key to prove |
| 168 | /// ourselves with. | 167 | /// ourselves with. |
| @@ -257,7 +256,7 @@ pub const Link = union(enum) { | |||
| 257 | /// The connection that IS the transport. `conn` holds the UDP socket in | 256 | /// The connection that IS the transport. `conn` holds the UDP socket in |
| 258 | /// `.r` so the poll path needs no special case, and `.w` is -1 because | 257 | /// `.r` so the poll path needs no special case, and `.w` is -1 because |
| 259 | /// there is nothing to write(2) to — bytes go through the stream layer. | 258 | /// there is nothing to write(2) to — bytes go through the stream layer. |
| 260 | quic: *quic_client.Client, | 259 | quic: *quic.Client, |
| 261 | }; | 260 | }; |
| 262 | 261 | ||
| 263 | /// One live connection to a daemon, however it was reached. The point of the | 262 | /// One live connection to a daemon, however it was reached. The point of the |
| @@ -318,18 +317,18 @@ pub const Transport = struct { | |||
| 318 | } | 317 | } |
| 319 | 318 | ||
| 320 | /// `connect` only creates state, so the handshake wait belongs here: | 319 | /// `connect` only creates state, so the handshake wait belongs here: |
| 321 | /// "open succeeded" means the same on every transport. `budget_ms` is | 320 | /// "open succeeded" means one thing everywhere. `budget_ms` is the |
| 322 | /// the attach budget, not `idle_ms`. | 321 | /// attach budget, not `idle_ms`. |
| 323 | fn quicTransport( | 322 | fn quicTransport( |
| 324 | alloc: std.mem.Allocator, | 323 | alloc: std.mem.Allocator, |
| 325 | addr: std.net.Address, | 324 | addr: std.net.Address, |
| 326 | key: quic_client.Key, | 325 | key: quic.Key, |
| 327 | idle_ms: u32, | 326 | idle_ms: u32, |
| 328 | budget_ms: u32, | 327 | budget_ms: u32, |
| 329 | carry: ?*std.ArrayList(u8), | 328 | carry: ?*std.ArrayList(u8), |
| 330 | abort_fd: std.posix.fd_t, | 329 | abort_fd: std.posix.fd_t, |
| 331 | ) !Transport { | 330 | ) !Transport { |
| 332 | const cl = try quic_client.Client.connect(alloc, addr, key, idle_ms); | 331 | const cl = try quic.Client.connect(alloc, addr, key, idle_ms); |
| 333 | errdefer cl.deinit(); | 332 | errdefer cl.deinit(); |
| 334 | try waitReady(cl, budget_ms, alloc, carry, abort_fd); | 333 | try waitReady(cl, budget_ms, alloc, carry, abort_fd); |
| 335 | return .{ | 334 | return .{ |
| @@ -357,7 +356,7 @@ pub const Transport = struct { | |||
| 357 | // either of the two links below and owns the choice itself. | 356 | // either of the two links below and owns the choice itself. |
| 358 | .hand => |h| return openHandoff(alloc, h, carry, abort_fd), | 357 | .hand => |h| return openHandoff(alloc, h, carry, abort_fd), |
| 359 | .quic => |q| { | 358 | .quic => |q| { |
| 360 | const key = try quic_client.Key.load(q.key_path); | 359 | const key = try quic.Key.load(q.key_path); |
| 361 | const addr = try quic.parseAddr(alloc, q.host_port); | 360 | const addr = try quic.parseAddr(alloc, q.host_port); |
| 362 | return quicTransport(alloc, addr, key, q.idle_ms, q.deadline_ms, carry, abort_fd); | 361 | return quicTransport(alloc, addr, key, q.idle_ms, q.deadline_ms, carry, abort_fd); |
| 363 | }, | 362 | }, |
| @@ -400,7 +399,7 @@ pub const Transport = struct { | |||
| 400 | // which is authoritative. What that costs depends on | 399 | // which is authoritative. What that costs depends on |
| 401 | // how the coordinates are dead: a resolve failure and | 400 | // how the coordinates are dead: a resolve failure and |
| 402 | // a REFUSED port are both instant — the ICMP comes | 401 | // a REFUSED port are both instant — the ICMP comes |
| 403 | // back and quic_client acts on it — while anything | 402 | // back and the quic client acts on it — while anything |
| 404 | // SILENT (blackholed UDP, a listener holding another | 403 | // SILENT (blackholed UDP, a listener holding another |
| 405 | // key) spends the whole deadline first. See | 404 | // key) spends the whole deadline first. See |
| 406 | // handoff.deadline_ms for both numbers. | 405 | // handoff.deadline_ms for both numbers. |
| @@ -582,7 +581,7 @@ pub const Transport = struct { | |||
| 582 | abort_fd: std.posix.fd_t, | 581 | abort_fd: std.posix.fd_t, |
| 583 | ) !Transport { | 582 | ) !Transport { |
| 584 | const addr = try quic.resolveHost(alloc, handoff.dialHost(h.host), ep.port); | 583 | const addr = try quic.resolveHost(alloc, handoff.dialHost(h.host), ep.port); |
| 585 | const key = quic_client.Key{ .bytes = ep.key }; | 584 | const key = quic.Key{ .bytes = ep.key }; |
| 586 | return quicTransport(alloc, addr, key, h.idle_ms, h.deadline_ms, carry, abort_fd); | 585 | return quicTransport(alloc, addr, key, h.idle_ms, h.deadline_ms, carry, abort_fd); |
| 587 | } | 586 | } |
| 588 | 587 | ||
| @@ -725,7 +724,7 @@ pub fn connectAgent(path: []const u8) ?std.posix.fd_t { | |||
| 725 | /// Bounded by the attach budget, not the idle timeout: the time given a | 724 | /// Bounded by the attach budget, not the idle timeout: the time given a |
| 726 | /// handshake is not the time given a quiet session. | 725 | /// handshake is not the time given a quiet session. |
| 727 | fn waitReady( | 726 | fn waitReady( |
| 728 | cl: *quic_client.Client, | 727 | cl: *quic.Client, |
| 729 | budget_ms: u32, | 728 | budget_ms: u32, |
| 730 | alloc: std.mem.Allocator, | 729 | alloc: std.mem.Allocator, |
| 731 | carry: ?*std.ArrayList(u8), | 730 | carry: ?*std.ArrayList(u8), |
| @@ -924,11 +923,11 @@ pub fn openFailure(buf: []u8, target: Target, err: anyerror) OpenFailure { | |||
| 924 | error.KeyFilePermissive, | 923 | error.KeyFilePermissive, |
| 925 | error.KeyFileMalformed, | 924 | error.KeyFileMalformed, |
| 926 | => blk: { | 925 | => blk: { |
| 927 | var body: [quic_client.key_refusal_len]u8 = undefined; | 926 | var body: [quic.key_refusal_len]u8 = undefined; |
| 928 | break :blk failedMsg( | 927 | break :blk failedMsg( |
| 929 | buf, | 928 | buf, |
| 930 | "mux: {s}\n", | 929 | "mux: {s}\n", |
| 931 | .{quic_client.keyRefusalBody(&body, err, q.key_path)}, | 930 | .{quic.keyRefusalBody(&body, err, q.key_path)}, |
| 932 | ); | 931 | ); |
| 933 | }, | 932 | }, |
| 934 | error.MalformedAddress, error.UnknownHostName => failedMsg( | 933 | error.MalformedAddress, error.UnknownHostName => failedMsg( |
| @@ -1533,7 +1532,7 @@ test "handoff: endpoint-none rides the open pipe with no deadline paid" { | |||
| 1533 | test "handoff: dead coordinates are a fast no, and the pipe is the fallback" { | 1532 | test "handoff: dead coordinates are a fast no, and the pipe is the fallback" { |
| 1534 | // A well-formed announce naming 127.0.0.1:1, where nothing listens. | 1533 | // A well-formed announce naming 127.0.0.1:1, where nothing listens. |
| 1535 | // The refusal is REAL — an ICMP unreachable comes back — and both | 1534 | // The refusal is REAL — an ICMP unreachable comes back — and both |
| 1536 | // socket paths in quic_client act on it, so this dial dies | 1535 | // socket paths in the quic client act on it, so this dial dies |
| 1537 | // in about one loopback round trip instead of running deadline_ms out. | 1536 | // in about one loopback round trip instead of running deadline_ms out. |
| 1538 | // The upper bound below is what proves that: it sits far under the | 1537 | // The upper bound below is what proves that: it sits far under the |
| 1539 | // budget, so an implementation that swallowed the refusal again (the | 1538 | // budget, so an implementation that swallowed the refusal again (the |
src/client/quic_client.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,419 +0,0 @@ | |||
| 1 | //! mux's QUIC client transport: one UDP socket, one connection, one | ||
| 2 | //! bidirectional stream of opaque bytes. | ||
| 3 | //! | ||
| 4 | //! The mirror of `quic_server.zig`: it knows NOTHING about the frame | ||
| 5 | //! protocol it carries, which is why there is no `proto` import here. | ||
| 6 | //! Everything both ends must agree on is imported from quic.zig, so there | ||
| 7 | //! is one copy to drift. | ||
| 8 | //! | ||
| 9 | //! Three ngtcp2 constraints: | ||
| 10 | //! | ||
| 11 | //! 1. **ngtcp2 does not copy stream payload.** It keeps the vector it is | ||
| 12 | //! handed and re-reads those bytes to retransmit, so outbound bytes | ||
| 13 | //! must not move or be reused until the peer acknowledges them. | ||
| 14 | //! 2. **A blocked stream is not a dead one.** NGTCP2_ERR_STREAM_DATA_BLOCKED | ||
| 15 | //! is a documented return; abandoning the egress loop on it also | ||
| 16 | //! abandons the ACKs that would reopen the window. | ||
| 17 | //! 3. **Both flow-control windows get extended on consume.** Extending | ||
| 18 | //! only the stream's leaves the connection window to close, and the | ||
| 19 | //! DAEMON stalls a few hundred kilobytes in — a freeze with no error | ||
| 20 | //! on either side. | ||
| 21 | const std = @import("std"); | ||
| 22 | const quic = @import("quic"); | ||
| 23 | |||
| 24 | /// quic.zig's cImport, and deliberately not one of our own: two @cImport | ||
| 25 | /// blocks over the same headers are two distinct type universes, and the | ||
| 26 | /// egress ring hands `ngtcp2_vec`s across this boundary. | ||
| 27 | const c = quic.c; | ||
| 28 | |||
| 29 | pub const Key = quic.Key; | ||
| 30 | pub const key_len = quic.key_len; | ||
| 31 | pub const default_port = quic.default_port; | ||
| 32 | pub const default_idle_ms = quic.default_idle_ms; | ||
| 33 | pub const IdleMs = quic.IdleMs; | ||
| 34 | /// The refusal words, re-exported for the same reason `Key` is: client.zig | ||
| 35 | /// prints the daemon's sentences for a key the daemon would also refuse, | ||
| 36 | /// and it reaches the vocabulary module through this one. | ||
| 37 | pub const key_refusal_len = quic.key_refusal_len; | ||
| 38 | pub const keyRefusalBody = quic.keyRefusalBody; | ||
| 39 | |||
| 40 | /// wolfSSL's PSK callback carries no user pointer, so the key has to be | ||
| 41 | /// reachable without one. A client process runs one connection at a time, | ||
| 42 | /// which makes this correct rather than merely convenient. | ||
| 43 | var g_key: ?Key = null; | ||
| 44 | |||
| 45 | fn pskClientCb( | ||
| 46 | _: ?*c.WOLFSSL, | ||
| 47 | _: [*c]const u8, | ||
| 48 | identity: [*c]u8, | ||
| 49 | id_max: c_uint, | ||
| 50 | key_out: [*c]u8, | ||
| 51 | key_max: c_uint, | ||
| 52 | ciphersuite: [*c][*c]const u8, | ||
| 53 | ) callconv(.c) c_uint { | ||
| 54 | const k = g_key orelse return 0; | ||
| 55 | return quic.answerPsk(k, identity, id_max, key_out, key_max, ciphersuite); | ||
| 56 | } | ||
| 57 | |||
| 58 | fn getConnCb(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn { | ||
| 59 | const self: *Client = @ptrCast(@alignCast(ref.*.user_data)); | ||
| 60 | return self.conn; | ||
| 61 | } | ||
| 62 | |||
| 63 | fn handshakeCompletedCb(_: ?*c.ngtcp2_conn, ud: ?*anyopaque) callconv(.c) c_int { | ||
| 64 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 65 | self.handshake_done = true; | ||
| 66 | return 0; | ||
| 67 | } | ||
| 68 | |||
| 69 | /// The peer granted stream credit: open the one stream this transport uses. | ||
| 70 | /// It cannot be opened before the handshake, which is why this is a callback | ||
| 71 | /// rather than a line in `connect`. | ||
| 72 | fn extendStreamsCb(conn: ?*c.ngtcp2_conn, _: u64, ud: ?*anyopaque) callconv(.c) c_int { | ||
| 73 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 74 | if (self.stream_id == -1) { | ||
| 75 | var sid: i64 = -1; | ||
| 76 | if (c.ngtcp2_conn_open_bidi_stream(conn, &sid, null) == 0) self.stream_id = sid; | ||
| 77 | } | ||
| 78 | return 0; | ||
| 79 | } | ||
| 80 | |||
| 81 | fn recvStreamDataCb( | ||
| 82 | conn: ?*c.ngtcp2_conn, | ||
| 83 | _: u32, | ||
| 84 | stream_id: i64, | ||
| 85 | _: u64, | ||
| 86 | data: [*c]const u8, | ||
| 87 | datalen: usize, | ||
| 88 | ud: ?*anyopaque, | ||
| 89 | _: ?*anyopaque, | ||
| 90 | ) callconv(.c) c_int { | ||
| 91 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 92 | |||
| 93 | // BOTH windows, and the connection-level one is the half that is easy to | ||
| 94 | // forget: extend only the stream and the daemon stops sending a few | ||
| 95 | // hundred kilobytes into a session — a scrollback fetch, a big | ||
| 96 | // snapshot — with no error anywhere. It presents as a freeze. | ||
| 97 | _ = c.ngtcp2_conn_extend_max_stream_offset(conn, stream_id, datalen); | ||
| 98 | _ = c.ngtcp2_conn_extend_max_offset(conn, datalen); | ||
| 99 | |||
| 100 | if (datalen > 0) { | ||
| 101 | self.in.appendSlice(self.alloc, data[0..datalen]) catch { | ||
| 102 | self.dead = true; | ||
| 103 | return 0; | ||
| 104 | }; | ||
| 105 | } | ||
| 106 | return 0; | ||
| 107 | } | ||
| 108 | |||
| 109 | fn ackedStreamDataCb( | ||
| 110 | _: ?*c.ngtcp2_conn, | ||
| 111 | _: i64, | ||
| 112 | _: u64, | ||
| 113 | datalen: u64, | ||
| 114 | ud: ?*anyopaque, | ||
| 115 | _: ?*anyopaque, | ||
| 116 | ) callconv(.c) c_int { | ||
| 117 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 118 | self.out.ack(@intCast(datalen)); | ||
| 119 | return 0; | ||
| 120 | } | ||
| 121 | |||
| 122 | /// One connection to a daemon's QUIC listener. | ||
| 123 | pub const Client = struct { | ||
| 124 | alloc: std.mem.Allocator, | ||
| 125 | fd: std.posix.fd_t, | ||
| 126 | ssl_ctx: ?*c.WOLFSSL_CTX = null, | ||
| 127 | ssl: ?*c.WOLFSSL = null, | ||
| 128 | conn: ?*c.ngtcp2_conn = null, | ||
| 129 | conn_ref: c.ngtcp2_crypto_conn_ref = undefined, | ||
| 130 | remote: std.posix.sockaddr.storage = undefined, | ||
| 131 | remote_len: std.posix.socklen_t = 0, | ||
| 132 | local: std.posix.sockaddr.storage = undefined, | ||
| 133 | local_len: std.posix.socklen_t = 0, | ||
| 134 | stream_id: i64 = -1, | ||
| 135 | handshake_done: bool = false, | ||
| 136 | /// Set once this connection can never carry another byte: an idle | ||
| 137 | /// timeout, a protocol error, a peer that stopped answering. The caller | ||
| 138 | /// reads it as "the transport is gone" and reconnects. | ||
| 139 | dead: bool = false, | ||
| 140 | /// Depth of nesting inside ngtcp2, mirroring the listener's. This side | ||
| 141 | /// has no live re-entrancy today — its callbacks only set flags and | ||
| 142 | /// append to `in`, none of them reaches `send` — so `send` may still | ||
| 143 | /// drain, which is what keeps a keystroke from waiting for the next poll | ||
| 144 | /// cycle. The counter and the assert in `drain` are what make that a | ||
| 145 | /// checked invariant rather than a property someone has to re-verify | ||
| 146 | /// every time a callback grows a line. | ||
| 147 | ngtcp2_depth: u8 = 0, | ||
| 148 | /// Outbound bytes, in the ring that does not move them until they are | ||
| 149 | /// acknowledged. See the module comment for why that matters. | ||
| 150 | out: quic.Egress, | ||
| 151 | /// Stream bytes the caller has not consumed yet. | ||
| 152 | in: std.ArrayList(u8) = .empty, | ||
| 153 | |||
| 154 | pub fn connect( | ||
| 155 | alloc: std.mem.Allocator, | ||
| 156 | addr: std.net.Address, | ||
| 157 | key: Key, | ||
| 158 | idle_ms: u32, | ||
| 159 | ) !*Client { | ||
| 160 | const fd = try std.posix.socket( | ||
| 161 | addr.any.family, | ||
| 162 | std.posix.SOCK.DGRAM | std.posix.SOCK.NONBLOCK | std.posix.SOCK.CLOEXEC, | ||
| 163 | 0, | ||
| 164 | ); | ||
| 165 | errdefer std.posix.close(fd); | ||
| 166 | |||
| 167 | const self = try alloc.create(Client); | ||
| 168 | errdefer alloc.destroy(self); | ||
| 169 | const ring = try alloc.alloc(u8, quic.egress_cap); | ||
| 170 | errdefer alloc.free(ring); | ||
| 171 | |||
| 172 | self.* = .{ | ||
| 173 | .alloc = alloc, | ||
| 174 | .fd = fd, | ||
| 175 | .out = .{ .buf = ring }, | ||
| 176 | .remote_len = addr.getOsSockLen(), | ||
| 177 | .local_len = @sizeOf(std.posix.sockaddr.storage), | ||
| 178 | }; | ||
| 179 | @memcpy( | ||
| 180 | std.mem.asBytes(&self.remote)[0..self.remote_len], | ||
| 181 | std.mem.asBytes(&addr.any)[0..self.remote_len], | ||
| 182 | ); | ||
| 183 | // Connected UDP: the kernel filters out anything from another | ||
| 184 | // address, which is one fewer thing this code has to check. | ||
| 185 | try std.posix.connect(fd, &addr.any, self.remote_len); | ||
| 186 | try std.posix.getsockname(fd, @ptrCast(&self.local), &self.local_len); | ||
| 187 | |||
| 188 | g_key = key; | ||
| 189 | try self.startTls(); | ||
| 190 | try self.startConn(idle_ms); | ||
| 191 | self.drain(); | ||
| 192 | return self; | ||
| 193 | } | ||
| 194 | |||
| 195 | fn startTls(self: *Client) !void { | ||
| 196 | // Set before clientTls, which hands wolfSSL a pointer to it. | ||
| 197 | self.conn_ref = .{ .get_conn = getConnCb, .user_data = self }; | ||
| 198 | const tls = try quic.clientTls(pskClientCb, &self.conn_ref); | ||
| 199 | self.ssl_ctx = tls.ctx; | ||
| 200 | self.ssl = tls.ssl; | ||
| 201 | } | ||
| 202 | |||
| 203 | fn startConn(self: *Client, idle_ms: u32) !void { | ||
| 204 | var dcid: c.ngtcp2_cid = undefined; | ||
| 205 | dcid.datalen = 16; | ||
| 206 | std.crypto.random.bytes(dcid.data[0..16]); | ||
| 207 | var scid: c.ngtcp2_cid = undefined; | ||
| 208 | scid.datalen = 8; | ||
| 209 | std.crypto.random.bytes(scid.data[0..8]); | ||
| 210 | |||
| 211 | var cbs = quic.clientCallbacks(); | ||
| 212 | cbs.handshake_completed = handshakeCompletedCb; | ||
| 213 | cbs.extend_max_local_streams_bidi = extendStreamsCb; | ||
| 214 | cbs.recv_stream_data = recvStreamDataCb; | ||
| 215 | cbs.acked_stream_data_offset = ackedStreamDataCb; | ||
| 216 | |||
| 217 | var settings: c.ngtcp2_settings = undefined; | ||
| 218 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); | ||
| 219 | settings.initial_ts = quic.timestampNs(); | ||
| 220 | |||
| 221 | var params = quic.clientParams(); | ||
| 222 | // Tunable for the same reason as the daemon's: the reconnect loop | ||
| 223 | // has to see a dead transport on a schedule a test can wait for. | ||
| 224 | params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000; | ||
| 225 | |||
| 226 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 227 | var conn: ?*c.ngtcp2_conn = null; | ||
| 228 | if (c.ngtcp2_conn_client_new_versioned( | ||
| 229 | &conn, | ||
| 230 | &dcid, | ||
| 231 | &scid, | ||
| 232 | &path, | ||
| 233 | c.NGTCP2_PROTO_VER_V1, | ||
| 234 | c.NGTCP2_CALLBACKS_VERSION, | ||
| 235 | &cbs, | ||
| 236 | c.NGTCP2_SETTINGS_VERSION, | ||
| 237 | &settings, | ||
| 238 | c.NGTCP2_TRANSPORT_PARAMS_VERSION, | ||
| 239 | ¶ms, | ||
| 240 | null, | ||
| 241 | self, | ||
| 242 | ) != 0) return error.ConnInit; | ||
| 243 | self.conn = conn; | ||
| 244 | c.ngtcp2_conn_set_tls_native_handle(conn, self.ssl); | ||
| 245 | // Silence is not death: a terminal nobody is typing into must not be | ||
| 246 | // dropped at the idle timeout. A third of it, matching the daemon. | ||
| 247 | c.ngtcp2_conn_set_keep_alive_timeout(conn, quic.keepAliveNs(idle_ms)); | ||
| 248 | } | ||
| 249 | |||
| 250 | pub fn deinit(self: *Client) void { | ||
| 251 | self.in.deinit(self.alloc); | ||
| 252 | // ngtcp2 before the egress ring: it holds vectors into that ring for | ||
| 253 | // anything unacknowledged, so freeing the ring first leaves it | ||
| 254 | // reading memory that is already gone for the length of its own | ||
| 255 | // teardown. Same ordering as the listener's Conn.deinit. | ||
| 256 | if (self.conn) |cn| c.ngtcp2_conn_del(cn); | ||
| 257 | self.out.deinit(self.alloc); | ||
| 258 | if (self.ssl) |s| c.wolfSSL_free(s); | ||
| 259 | if (self.ssl_ctx) |x| c.wolfSSL_CTX_free(x); | ||
| 260 | std.posix.close(self.fd); | ||
| 261 | self.alloc.destroy(self); | ||
| 262 | } | ||
| 263 | |||
| 264 | /// Readable means a datagram arrived, not that a frame is waiting. | ||
| 265 | pub fn pollFd(self: *const Client) std.posix.fd_t { | ||
| 266 | return self.fd; | ||
| 267 | } | ||
| 268 | |||
| 269 | /// Handshake finished AND the stream open: a handshake without a stream | ||
| 270 | /// silently holds everything in the ring. | ||
| 271 | pub fn isReady(self: *const Client) bool { | ||
| 272 | return self.handshake_done and self.stream_id != -1 and !self.dead; | ||
| 273 | } | ||
| 274 | |||
| 275 | /// Milliseconds until ngtcp2 next wants servicing, capped. Feeds the | ||
| 276 | /// client's existing poll timeout — no timer fd, same as the daemon. | ||
| 277 | pub fn timeoutMs(self: *Client, cap_ms: i32) i32 { | ||
| 278 | // A negative cap means poll forever; the @intCast below would panic | ||
| 279 | // on it instead of honouring it. | ||
| 280 | if (cap_ms < 0) return cap_ms; | ||
| 281 | const conn = self.conn orelse return cap_ms; | ||
| 282 | const expiry = c.ngtcp2_conn_get_expiry(conn); | ||
| 283 | if (expiry == std.math.maxInt(u64)) return cap_ms; | ||
| 284 | const now = quic.timestampNs(); | ||
| 285 | if (expiry <= now) return 0; | ||
| 286 | return @intCast(@min(@as(u64, @intCast(cap_ms)), (expiry - now) / 1_000_000)); | ||
| 287 | } | ||
| 288 | |||
| 289 | /// One service pass: read, run due timers, push ready egress. Safe to call | ||
| 290 | /// at any time. | ||
| 291 | pub fn pump(self: *Client) void { | ||
| 292 | if (self.dead) return; | ||
| 293 | self.readable(); | ||
| 294 | self.tick(); | ||
| 295 | self.drain(); | ||
| 296 | } | ||
| 297 | |||
| 298 | /// ECONNREFUSED is fatal and reaches whichever syscall runs first, so | ||
| 299 | /// both paths must act. | ||
| 300 | fn sendRecvFailed( | ||
| 301 | self: *Client, | ||
| 302 | err: (std.posix.RecvFromError || std.posix.SendError), | ||
| 303 | ) void { | ||
| 304 | switch (err) { | ||
| 305 | error.ConnectionRefused => self.dead = true, | ||
| 306 | else => {}, | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | fn readable(self: *Client) void { | ||
| 311 | var buf: [65536]u8 = undefined; | ||
| 312 | while (true) { | ||
| 313 | const n = std.posix.recv(self.fd, &buf, 0) catch |err| switch (err) { | ||
| 314 | error.WouldBlock => return, | ||
| 315 | else => { | ||
| 316 | self.sendRecvFailed(err); | ||
| 317 | return; | ||
| 318 | }, | ||
| 319 | }; | ||
| 320 | if (n == 0) continue; | ||
| 321 | const conn = self.conn orelse return; | ||
| 322 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 323 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 324 | const rv = blk: { | ||
| 325 | self.ngtcp2_depth += 1; | ||
| 326 | defer self.ngtcp2_depth -= 1; | ||
| 327 | break :blk c.ngtcp2_conn_read_pkt(conn, &path, &pi, &buf, n, quic.timestampNs()); | ||
| 328 | }; | ||
| 329 | if (rv != 0) { | ||
| 330 | self.dead = true; | ||
| 331 | return; | ||
| 332 | } | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | fn tick(self: *Client) void { | ||
| 337 | const conn = self.conn orelse return; | ||
| 338 | const now = quic.timestampNs(); | ||
| 339 | if (c.ngtcp2_conn_get_expiry(conn) > now) return; | ||
| 340 | // An idle timeout arrives here, which is how the reconnect loop | ||
| 341 | // learns the daemon stopped answering. | ||
| 342 | if (c.ngtcp2_conn_handle_expiry(conn, now) != 0) self.dead = true; | ||
| 343 | } | ||
| 344 | |||
| 345 | fn drain(self: *Client) void { | ||
| 346 | // The listener learned this the hard way: writing to a connection | ||
| 347 | // while ngtcp2 is reading a packet on it corrupts loss detection and | ||
| 348 | // can abort outright. Enforced here so it stays true of this side. | ||
| 349 | std.debug.assert(self.ngtcp2_depth == 0); | ||
| 350 | const conn = self.conn orelse return; | ||
| 351 | var buf: [quic.max_udp]u8 = undefined; | ||
| 352 | var stream_blocked = false; | ||
| 353 | while (true) { | ||
| 354 | var ps: c.ngtcp2_path_storage = undefined; | ||
| 355 | c.ngtcp2_path_storage_zero(&ps); | ||
| 356 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 357 | var wrote: c.ngtcp2_ssize = 0; | ||
| 358 | |||
| 359 | var vecs: [2]c.ngtcp2_vec = undefined; | ||
| 360 | var vcnt: usize = 0; | ||
| 361 | var sid: i64 = -1; | ||
| 362 | if (!stream_blocked and self.stream_id != -1) { | ||
| 363 | vcnt = self.out.vecs(&vecs); | ||
| 364 | if (vcnt > 0) sid = self.stream_id; | ||
| 365 | } | ||
| 366 | |||
| 367 | const n = c.ngtcp2_conn_writev_stream_versioned( | ||
| 368 | conn, | ||
| 369 | &ps.path, | ||
| 370 | c.NGTCP2_PKT_INFO_VERSION, | ||
| 371 | &pi, | ||
| 372 | &buf, | ||
| 373 | buf.len, | ||
| 374 | &wrote, | ||
| 375 | 0, | ||
| 376 | sid, | ||
| 377 | if (vcnt > 0) &vecs else null, | ||
| 378 | vcnt, | ||
| 379 | quic.timestampNs(), | ||
| 380 | ); | ||
| 381 | if (n == c.NGTCP2_ERR_STREAM_DATA_BLOCKED or n == c.NGTCP2_ERR_STREAM_SHUT_WR) { | ||
| 382 | // Documented, not fatal. Retry the same iteration carrying | ||
| 383 | // no stream data so ACKs and keepalives still leave: they | ||
| 384 | // are how the peer's window reopens, and a client that | ||
| 385 | // stopped here would stall exactly as the daemon did. | ||
| 386 | stream_blocked = true; | ||
| 387 | continue; | ||
| 388 | } | ||
| 389 | switch (quic.accountWrite(&self.out, wrote, n)) { | ||
| 390 | .stop => { | ||
| 391 | self.dead = true; | ||
| 392 | return; | ||
| 393 | }, | ||
| 394 | .brk => return, | ||
| 395 | .cont => {}, | ||
| 396 | } | ||
| 397 | _ = std.posix.send(self.fd, buf[0..@intCast(n)], 0) catch |err| { | ||
| 398 | self.sendRecvFailed(err); | ||
| 399 | return; | ||
| 400 | }; | ||
| 401 | } | ||
| 402 | } | ||
| 403 | |||
| 404 | /// A short return is the caller's signal to keep the rest and offer it | ||
| 405 | /// again: the ring is bounded, so the backlog belongs with somebody who | ||
| 406 | /// can see how big it is. | ||
| 407 | pub fn send(self: *Client, bytes: []const u8) usize { | ||
| 408 | if (self.dead or self.stream_id == -1) return 0; | ||
| 409 | const n = self.out.push(bytes); | ||
| 410 | if (n > 0) self.drain(); | ||
| 411 | return n; | ||
| 412 | } | ||
| 413 | }; | ||
| 414 | |||
| 415 | // Plain, not recursive: this module reaches the QUIC stack's @cImport, and | ||
| 416 | // a recursive walk would force-analyze the entire wolfSSL/ngtcp2 namespace. | ||
| 417 | test { | ||
| 418 | std.testing.refAllDecls(@This()); | ||
| 419 | } | ||
src/quic.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,10 +1,12 @@ | |||
| 1 | //! The QUIC vocabulary both ends share: the C import, the pre-shared key, | 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. | 2 | //! the wire constants, and the egress ring's lifetime discipline. |
| 3 | //! | 3 | //! |
| 4 | //! Nothing here knows what a listener or a connection is. quic_server.zig | 4 | //! Nothing here knows what a LISTENER is: quic_server.zig owns that, and |
| 5 | //! owns the listener, quic_client.zig owns the client transport, and both | 5 | //! imports this — which is what makes "the two ends agree" a fact about one |
| 6 | //! import this — which is what makes "the two ends agree" a fact about one | 6 | //! file rather than a convention two files are trusted to keep. The dialling |
| 7 | //! file rather than a convention two files are trusted to keep. | 7 | //! end lives here, below the shared vocabulary, because both the client and |
| 8 | //! the listener's own tests need a real peer and the folder rule forbids the | ||
| 9 | //! server naming a client module. | ||
| 8 | //! | 10 | //! |
| 9 | //! THE OWNERSHIP RULE, because it is load-bearing rather than tidy: this | 11 | //! THE OWNERSHIP RULE, because it is load-bearing rather than tidy: this |
| 10 | //! file holds the one and only @cImport of the QUIC stack. Two blocks over | 12 | //! file holds the one and only @cImport of the QUIC stack. Two blocks over |
| @@ -700,11 +702,11 @@ pub fn getNewCidCb( | |||
| 700 | } | 702 | } |
| 701 | 703 | ||
| 702 | // --------------------------------------------------------------------------- | 704 | // --------------------------------------------------------------------------- |
| 703 | // The client side of the handshake. Two peers dial this daemon — the transport | 705 | // The client side of the handshake, kept separate from `Client` below |
| 704 | // in quic_client.zig and the test peer in quic_server.zig, which cannot import | 706 | // because the listener's tests once dialled with a second copy of it: a |
| 705 | // it (folder rule 2) — and a drift between them fails the handshake with | 707 | // drift between the two failed the handshake with nothing to read but a |
| 706 | // nothing to read but a TLS alert. Each still sets its own key, stream | 708 | // TLS alert. `Client` sets its own key, stream handlers and idle timeout |
| 707 | // handlers and idle timeout on what these hand back. | 709 | // on what these hand back. |
| 708 | // --------------------------------------------------------------------------- | 710 | // --------------------------------------------------------------------------- |
| 709 | 711 | ||
| 710 | /// Identity "mux", the key's bytes, our one ciphersuite: none is a choice. | 712 | /// Identity "mux", the key's bytes, our one ciphersuite: none is a choice. |
| @@ -801,6 +803,413 @@ pub fn pathFrom( | |||
| 801 | }; | 803 | }; |
| 802 | } | 804 | } |
| 803 | 805 | ||
| 806 | // --------------------------------------------------------------------------- | ||
| 807 | // The client transport: one UDP socket, one connection, one bidirectional | ||
| 808 | // stream of opaque bytes. It knows NOTHING about the frame protocol it | ||
| 809 | // carries, which is why `protocol` is not imported here. | ||
| 810 | // | ||
| 811 | // Three ngtcp2 constraints shape it, and the listener across the seam obeys | ||
| 812 | // the same three: | ||
| 813 | // | ||
| 814 | // 1. **ngtcp2 does not copy stream payload.** It keeps the vector it is | ||
| 815 | // handed and re-reads those bytes to retransmit, so outbound bytes | ||
| 816 | // must not move or be reused until the peer acknowledges them. | ||
| 817 | // 2. **A blocked stream is not a dead one.** NGTCP2_ERR_STREAM_DATA_BLOCKED | ||
| 818 | // is a documented return; abandoning the egress loop on it also | ||
| 819 | // abandons the ACKs that would reopen the window. | ||
| 820 | // 3. **Both flow-control windows get extended on consume.** Extending | ||
| 821 | // only the stream's leaves the connection window to close, and the | ||
| 822 | // DAEMON stalls a few hundred kilobytes in — a freeze with no error | ||
| 823 | // on either side. | ||
| 824 | // --------------------------------------------------------------------------- | ||
| 825 | |||
| 826 | /// wolfSSL's PSK callback carries no user pointer, so the key has to be | ||
| 827 | /// reachable without one. A client process runs one connection at a time, | ||
| 828 | /// which makes this correct rather than merely convenient. | ||
| 829 | var g_key: ?Key = null; | ||
| 830 | |||
| 831 | fn pskClientCb( | ||
| 832 | _: ?*c.WOLFSSL, | ||
| 833 | _: [*c]const u8, | ||
| 834 | identity: [*c]u8, | ||
| 835 | id_max: c_uint, | ||
| 836 | key_out: [*c]u8, | ||
| 837 | key_max: c_uint, | ||
| 838 | ciphersuite: [*c][*c]const u8, | ||
| 839 | ) callconv(.c) c_uint { | ||
| 840 | const k = g_key orelse return 0; | ||
| 841 | return answerPsk(k, identity, id_max, key_out, key_max, ciphersuite); | ||
| 842 | } | ||
| 843 | |||
| 844 | fn getConnCb(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn { | ||
| 845 | const self: *Client = @ptrCast(@alignCast(ref.*.user_data)); | ||
| 846 | return self.conn; | ||
| 847 | } | ||
| 848 | |||
| 849 | fn handshakeCompletedCb(_: ?*c.ngtcp2_conn, ud: ?*anyopaque) callconv(.c) c_int { | ||
| 850 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 851 | self.handshake_done = true; | ||
| 852 | return 0; | ||
| 853 | } | ||
| 854 | |||
| 855 | /// The peer granted stream credit: open the one stream this transport uses. | ||
| 856 | /// It cannot be opened before the handshake, which is why this is a callback | ||
| 857 | /// rather than a line in `connect`. | ||
| 858 | fn extendStreamsCb(conn: ?*c.ngtcp2_conn, _: u64, ud: ?*anyopaque) callconv(.c) c_int { | ||
| 859 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 860 | if (self.stream_id == -1) { | ||
| 861 | var sid: i64 = -1; | ||
| 862 | if (c.ngtcp2_conn_open_bidi_stream(conn, &sid, null) == 0) self.stream_id = sid; | ||
| 863 | } | ||
| 864 | return 0; | ||
| 865 | } | ||
| 866 | |||
| 867 | fn recvStreamDataCb( | ||
| 868 | conn: ?*c.ngtcp2_conn, | ||
| 869 | _: u32, | ||
| 870 | stream_id: i64, | ||
| 871 | _: u64, | ||
| 872 | data: [*c]const u8, | ||
| 873 | datalen: usize, | ||
| 874 | ud: ?*anyopaque, | ||
| 875 | _: ?*anyopaque, | ||
| 876 | ) callconv(.c) c_int { | ||
| 877 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 878 | |||
| 879 | // BOTH windows, and the connection-level one is the half that is easy to | ||
| 880 | // forget: extend only the stream and the daemon stops sending a few | ||
| 881 | // hundred kilobytes into a session — a scrollback fetch, a big | ||
| 882 | // snapshot — with no error anywhere. It presents as a freeze. | ||
| 883 | if (self.extend_windows) { | ||
| 884 | _ = c.ngtcp2_conn_extend_max_stream_offset(conn, stream_id, datalen); | ||
| 885 | _ = c.ngtcp2_conn_extend_max_offset(conn, datalen); | ||
| 886 | } | ||
| 887 | |||
| 888 | if (datalen > 0) { | ||
| 889 | self.in.appendSlice(self.alloc, data[0..datalen]) catch { | ||
| 890 | self.dead = true; | ||
| 891 | return 0; | ||
| 892 | }; | ||
| 893 | } | ||
| 894 | return 0; | ||
| 895 | } | ||
| 896 | |||
| 897 | fn ackedStreamDataCb( | ||
| 898 | _: ?*c.ngtcp2_conn, | ||
| 899 | _: i64, | ||
| 900 | _: u64, | ||
| 901 | datalen: u64, | ||
| 902 | ud: ?*anyopaque, | ||
| 903 | _: ?*anyopaque, | ||
| 904 | ) callconv(.c) c_int { | ||
| 905 | const self: *Client = @ptrCast(@alignCast(ud.?)); | ||
| 906 | self.out.ack(@intCast(datalen)); | ||
| 907 | return 0; | ||
| 908 | } | ||
| 909 | |||
| 910 | /// One connection to a daemon's QUIC listener. | ||
| 911 | pub const Client = struct { | ||
| 912 | alloc: std.mem.Allocator, | ||
| 913 | fd: std.posix.fd_t, | ||
| 914 | ssl_ctx: ?*c.WOLFSSL_CTX = null, | ||
| 915 | ssl: ?*c.WOLFSSL = null, | ||
| 916 | conn: ?*c.ngtcp2_conn = null, | ||
| 917 | conn_ref: c.ngtcp2_crypto_conn_ref = undefined, | ||
| 918 | remote: std.posix.sockaddr.storage = undefined, | ||
| 919 | remote_len: std.posix.socklen_t = 0, | ||
| 920 | local: std.posix.sockaddr.storage = undefined, | ||
| 921 | local_len: std.posix.socklen_t = 0, | ||
| 922 | stream_id: i64 = -1, | ||
| 923 | handshake_done: bool = false, | ||
| 924 | /// Set once this connection can never carry another byte: an idle | ||
| 925 | /// timeout, a protocol error, a peer that stopped answering. The caller | ||
| 926 | /// reads it as "the transport is gone" and reconnects. | ||
| 927 | dead: bool = false, | ||
| 928 | /// Depth of nesting inside ngtcp2, mirroring the listener's. This side | ||
| 929 | /// has no live re-entrancy today — its callbacks only set flags and | ||
| 930 | /// append to `in`, none of them reaches `send` — so `send` may still | ||
| 931 | /// drain, which is what keeps a keystroke from waiting for the next poll | ||
| 932 | /// cycle. The counter and the assert in `drain` are what make that a | ||
| 933 | /// checked invariant rather than a property someone has to re-verify | ||
| 934 | /// every time a callback grows a line. | ||
| 935 | ngtcp2_depth: u8 = 0, | ||
| 936 | /// Outbound bytes, in the ring that does not move them until they are | ||
| 937 | /// acknowledged. See the module comment for why that matters. | ||
| 938 | out: Egress, | ||
| 939 | /// Stream bytes the caller has not consumed yet. | ||
| 940 | in: std.ArrayList(u8) = .empty, | ||
| 941 | /// Whether arriving bytes buy the peer more window. A shipping client | ||
| 942 | /// always extends (constraint 3 above); the listener's own tests turn | ||
| 943 | /// it off to reach the blocked-stream branch, which is a documented | ||
| 944 | /// ngtcp2 return rather than a failure and has no other way in. | ||
| 945 | extend_windows: bool = true, | ||
| 946 | |||
| 947 | pub fn connect( | ||
| 948 | alloc: std.mem.Allocator, | ||
| 949 | addr: std.net.Address, | ||
| 950 | key: Key, | ||
| 951 | idle_ms: u32, | ||
| 952 | ) !*Client { | ||
| 953 | const fd = try std.posix.socket( | ||
| 954 | addr.any.family, | ||
| 955 | std.posix.SOCK.DGRAM | std.posix.SOCK.NONBLOCK | std.posix.SOCK.CLOEXEC, | ||
| 956 | 0, | ||
| 957 | ); | ||
| 958 | errdefer std.posix.close(fd); | ||
| 959 | |||
| 960 | const self = try alloc.create(Client); | ||
| 961 | errdefer alloc.destroy(self); | ||
| 962 | const ring = try alloc.alloc(u8, egress_cap); | ||
| 963 | errdefer alloc.free(ring); | ||
| 964 | |||
| 965 | self.* = .{ | ||
| 966 | .alloc = alloc, | ||
| 967 | .fd = fd, | ||
| 968 | .out = .{ .buf = ring }, | ||
| 969 | .remote_len = addr.getOsSockLen(), | ||
| 970 | .local_len = @sizeOf(std.posix.sockaddr.storage), | ||
| 971 | }; | ||
| 972 | @memcpy( | ||
| 973 | std.mem.asBytes(&self.remote)[0..self.remote_len], | ||
| 974 | std.mem.asBytes(&addr.any)[0..self.remote_len], | ||
| 975 | ); | ||
| 976 | // Connected UDP: the kernel filters out anything from another | ||
| 977 | // address, which is one fewer thing this code has to check. | ||
| 978 | try std.posix.connect(fd, &addr.any, self.remote_len); | ||
| 979 | try std.posix.getsockname(fd, @ptrCast(&self.local), &self.local_len); | ||
| 980 | |||
| 981 | g_key = key; | ||
| 982 | try self.startTls(); | ||
| 983 | try self.startConn(idle_ms); | ||
| 984 | self.drain(); | ||
| 985 | return self; | ||
| 986 | } | ||
| 987 | |||
| 988 | fn startTls(self: *Client) !void { | ||
| 989 | // Set before clientTls, which hands wolfSSL a pointer to it. | ||
| 990 | self.conn_ref = .{ .get_conn = getConnCb, .user_data = self }; | ||
| 991 | const tls = try clientTls(pskClientCb, &self.conn_ref); | ||
| 992 | self.ssl_ctx = tls.ctx; | ||
| 993 | self.ssl = tls.ssl; | ||
| 994 | } | ||
| 995 | |||
| 996 | fn startConn(self: *Client, idle_ms: u32) !void { | ||
| 997 | var dcid: c.ngtcp2_cid = undefined; | ||
| 998 | dcid.datalen = 16; | ||
| 999 | std.crypto.random.bytes(dcid.data[0..16]); | ||
| 1000 | var scid: c.ngtcp2_cid = undefined; | ||
| 1001 | scid.datalen = 8; | ||
| 1002 | std.crypto.random.bytes(scid.data[0..8]); | ||
| 1003 | |||
| 1004 | var cbs = clientCallbacks(); | ||
| 1005 | cbs.handshake_completed = handshakeCompletedCb; | ||
| 1006 | cbs.extend_max_local_streams_bidi = extendStreamsCb; | ||
| 1007 | cbs.recv_stream_data = recvStreamDataCb; | ||
| 1008 | cbs.acked_stream_data_offset = ackedStreamDataCb; | ||
| 1009 | |||
| 1010 | var settings: c.ngtcp2_settings = undefined; | ||
| 1011 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); | ||
| 1012 | settings.initial_ts = timestampNs(); | ||
| 1013 | |||
| 1014 | var params = clientParams(); | ||
| 1015 | // Tunable for the same reason as the daemon's: the reconnect loop | ||
| 1016 | // has to see a dead transport on a schedule a test can wait for. | ||
| 1017 | params.max_idle_timeout = @as(u64, idle_ms) * 1_000_000; | ||
| 1018 | |||
| 1019 | var path = pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 1020 | var conn: ?*c.ngtcp2_conn = null; | ||
| 1021 | if (c.ngtcp2_conn_client_new_versioned( | ||
| 1022 | &conn, | ||
| 1023 | &dcid, | ||
| 1024 | &scid, | ||
| 1025 | &path, | ||
| 1026 | c.NGTCP2_PROTO_VER_V1, | ||
| 1027 | c.NGTCP2_CALLBACKS_VERSION, | ||
| 1028 | &cbs, | ||
| 1029 | c.NGTCP2_SETTINGS_VERSION, | ||
| 1030 | &settings, | ||
| 1031 | c.NGTCP2_TRANSPORT_PARAMS_VERSION, | ||
| 1032 | ¶ms, | ||
| 1033 | null, | ||
| 1034 | self, | ||
| 1035 | ) != 0) return error.ConnInit; | ||
| 1036 | self.conn = conn; | ||
| 1037 | c.ngtcp2_conn_set_tls_native_handle(conn, self.ssl); | ||
| 1038 | // Silence is not death: a terminal nobody is typing into must not be | ||
| 1039 | // dropped at the idle timeout. A third of it, matching the daemon. | ||
| 1040 | c.ngtcp2_conn_set_keep_alive_timeout(conn, keepAliveNs(idle_ms)); | ||
| 1041 | } | ||
| 1042 | |||
| 1043 | pub fn deinit(self: *Client) void { | ||
| 1044 | self.in.deinit(self.alloc); | ||
| 1045 | // ngtcp2 before the egress ring: it holds vectors into that ring for | ||
| 1046 | // anything unacknowledged, so freeing the ring first leaves it | ||
| 1047 | // reading memory that is already gone for the length of its own | ||
| 1048 | // teardown. Same ordering as the listener's Conn.deinit. | ||
| 1049 | if (self.conn) |cn| c.ngtcp2_conn_del(cn); | ||
| 1050 | self.out.deinit(self.alloc); | ||
| 1051 | if (self.ssl) |s| c.wolfSSL_free(s); | ||
| 1052 | if (self.ssl_ctx) |x| c.wolfSSL_CTX_free(x); | ||
| 1053 | std.posix.close(self.fd); | ||
| 1054 | self.alloc.destroy(self); | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | /// Readable means a datagram arrived, not that a frame is waiting. | ||
| 1058 | pub fn pollFd(self: *const Client) std.posix.fd_t { | ||
| 1059 | return self.fd; | ||
| 1060 | } | ||
| 1061 | |||
| 1062 | /// Handshake finished AND the stream open: a handshake without a stream | ||
| 1063 | /// silently holds everything in the ring. | ||
| 1064 | pub fn isReady(self: *const Client) bool { | ||
| 1065 | return self.handshake_done and self.stream_id != -1 and !self.dead; | ||
| 1066 | } | ||
| 1067 | |||
| 1068 | /// Milliseconds until ngtcp2 next wants servicing, capped. Feeds the | ||
| 1069 | /// client's existing poll timeout — no timer fd, same as the daemon. | ||
| 1070 | pub fn timeoutMs(self: *Client, cap_ms: i32) i32 { | ||
| 1071 | // A negative cap means poll forever; the @intCast below would panic | ||
| 1072 | // on it instead of honouring it. | ||
| 1073 | if (cap_ms < 0) return cap_ms; | ||
| 1074 | const conn = self.conn orelse return cap_ms; | ||
| 1075 | const expiry = c.ngtcp2_conn_get_expiry(conn); | ||
| 1076 | if (expiry == std.math.maxInt(u64)) return cap_ms; | ||
| 1077 | const now = timestampNs(); | ||
| 1078 | if (expiry <= now) return 0; | ||
| 1079 | return @intCast(@min(@as(u64, @intCast(cap_ms)), (expiry - now) / 1_000_000)); | ||
| 1080 | } | ||
| 1081 | |||
| 1082 | /// One service pass: read, run due timers, push ready egress. Safe to call | ||
| 1083 | /// at any time. | ||
| 1084 | pub fn pump(self: *Client) void { | ||
| 1085 | if (self.dead) return; | ||
| 1086 | self.readable(); | ||
| 1087 | self.tick(); | ||
| 1088 | self.drain(); | ||
| 1089 | } | ||
| 1090 | |||
| 1091 | /// ECONNREFUSED is fatal and reaches whichever syscall runs first, so | ||
| 1092 | /// both paths must act. | ||
| 1093 | fn sendRecvFailed( | ||
| 1094 | self: *Client, | ||
| 1095 | err: (std.posix.RecvFromError || std.posix.SendError), | ||
| 1096 | ) void { | ||
| 1097 | switch (err) { | ||
| 1098 | error.ConnectionRefused => self.dead = true, | ||
| 1099 | else => {}, | ||
| 1100 | } | ||
| 1101 | } | ||
| 1102 | |||
| 1103 | /// Read whatever has arrived, WITHOUT transmitting — `pump` is the pass | ||
| 1104 | /// that also acknowledges. The daemon's tests need the two apart: a | ||
| 1105 | /// client that acks is a client the listener gets an inbound packet | ||
| 1106 | /// from, and an inbound packet carries a drain the test was trying to | ||
| 1107 | /// rule out. | ||
| 1108 | pub fn readable(self: *Client) void { | ||
| 1109 | var buf: [65536]u8 = undefined; | ||
| 1110 | while (true) { | ||
| 1111 | const n = std.posix.recv(self.fd, &buf, 0) catch |err| switch (err) { | ||
| 1112 | error.WouldBlock => return, | ||
| 1113 | else => { | ||
| 1114 | self.sendRecvFailed(err); | ||
| 1115 | return; | ||
| 1116 | }, | ||
| 1117 | }; | ||
| 1118 | if (n == 0) continue; | ||
| 1119 | const conn = self.conn orelse return; | ||
| 1120 | var path = pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 1121 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 1122 | const rv = blk: { | ||
| 1123 | self.ngtcp2_depth += 1; | ||
| 1124 | defer self.ngtcp2_depth -= 1; | ||
| 1125 | break :blk c.ngtcp2_conn_read_pkt(conn, &path, &pi, &buf, n, timestampNs()); | ||
| 1126 | }; | ||
| 1127 | if (rv != 0) { | ||
| 1128 | self.dead = true; | ||
| 1129 | return; | ||
| 1130 | } | ||
| 1131 | } | ||
| 1132 | } | ||
| 1133 | |||
| 1134 | fn tick(self: *Client) void { | ||
| 1135 | const conn = self.conn orelse return; | ||
| 1136 | const now = timestampNs(); | ||
| 1137 | if (c.ngtcp2_conn_get_expiry(conn) > now) return; | ||
| 1138 | // An idle timeout arrives here, which is how the reconnect loop | ||
| 1139 | // learns the daemon stopped answering. | ||
| 1140 | if (c.ngtcp2_conn_handle_expiry(conn, now) != 0) self.dead = true; | ||
| 1141 | } | ||
| 1142 | |||
| 1143 | fn drain(self: *Client) void { | ||
| 1144 | // The listener learned this the hard way: writing to a connection | ||
| 1145 | // while ngtcp2 is reading a packet on it corrupts loss detection and | ||
| 1146 | // can abort outright. Enforced here so it stays true of this side. | ||
| 1147 | std.debug.assert(self.ngtcp2_depth == 0); | ||
| 1148 | const conn = self.conn orelse return; | ||
| 1149 | var buf: [max_udp]u8 = undefined; | ||
| 1150 | var stream_blocked = false; | ||
| 1151 | while (true) { | ||
| 1152 | var ps: c.ngtcp2_path_storage = undefined; | ||
| 1153 | c.ngtcp2_path_storage_zero(&ps); | ||
| 1154 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 1155 | var wrote: c.ngtcp2_ssize = 0; | ||
| 1156 | |||
| 1157 | var vecs: [2]c.ngtcp2_vec = undefined; | ||
| 1158 | var vcnt: usize = 0; | ||
| 1159 | var sid: i64 = -1; | ||
| 1160 | if (!stream_blocked and self.stream_id != -1) { | ||
| 1161 | vcnt = self.out.vecs(&vecs); | ||
| 1162 | if (vcnt > 0) sid = self.stream_id; | ||
| 1163 | } | ||
| 1164 | |||
| 1165 | const n = c.ngtcp2_conn_writev_stream_versioned( | ||
| 1166 | conn, | ||
| 1167 | &ps.path, | ||
| 1168 | c.NGTCP2_PKT_INFO_VERSION, | ||
| 1169 | &pi, | ||
| 1170 | &buf, | ||
| 1171 | buf.len, | ||
| 1172 | &wrote, | ||
| 1173 | 0, | ||
| 1174 | sid, | ||
| 1175 | if (vcnt > 0) &vecs else null, | ||
| 1176 | vcnt, | ||
| 1177 | timestampNs(), | ||
| 1178 | ); | ||
| 1179 | if (n == c.NGTCP2_ERR_STREAM_DATA_BLOCKED or n == c.NGTCP2_ERR_STREAM_SHUT_WR) { | ||
| 1180 | // Documented, not fatal. Retry the same iteration carrying | ||
| 1181 | // no stream data so ACKs and keepalives still leave: they | ||
| 1182 | // are how the peer's window reopens, and a client that | ||
| 1183 | // stopped here would stall exactly as the daemon did. | ||
| 1184 | stream_blocked = true; | ||
| 1185 | continue; | ||
| 1186 | } | ||
| 1187 | switch (accountWrite(&self.out, wrote, n)) { | ||
| 1188 | .stop => { | ||
| 1189 | self.dead = true; | ||
| 1190 | return; | ||
| 1191 | }, | ||
| 1192 | .brk => return, | ||
| 1193 | .cont => {}, | ||
| 1194 | } | ||
| 1195 | _ = std.posix.send(self.fd, buf[0..@intCast(n)], 0) catch |err| { | ||
| 1196 | self.sendRecvFailed(err); | ||
| 1197 | return; | ||
| 1198 | }; | ||
| 1199 | } | ||
| 1200 | } | ||
| 1201 | |||
| 1202 | /// A short return is the caller's signal to keep the rest and offer it | ||
| 1203 | /// again: the ring is bounded, so the backlog belongs with somebody who | ||
| 1204 | /// can see how big it is. | ||
| 1205 | pub fn send(self: *Client, bytes: []const u8) usize { | ||
| 1206 | if (self.dead or self.stream_id == -1) return 0; | ||
| 1207 | const n = self.out.push(bytes); | ||
| 1208 | if (n > 0) self.drain(); | ||
| 1209 | return n; | ||
| 1210 | } | ||
| 1211 | }; | ||
| 1212 | |||
| 804 | // Plain, not recursive: this module reaches the QUIC stack's @cImport, and | 1213 | // Plain, not recursive: this module reaches the QUIC stack's @cImport, and |
| 805 | // a recursive walk would force-analyze the entire wolfSSL/ngtcp2 namespace. | 1214 | // a recursive walk would force-analyze the entire wolfSSL/ngtcp2 namespace. |
| 806 | test { | 1215 | test { |
src/server/quic_server.zig
| Old | New | ||
|---|---|---|---|
| @@ -3,10 +3,8 @@ | |||
| 3 | //! both ends share — the C import, the key, the egress ring — is quic.zig's, | 3 | //! both ends share — the C import, the key, the egress ring — is quic.zig's, |
| 4 | //! and this file imports it. | 4 | //! and this file imports it. |
| 5 | //! | 5 | //! |
| 6 | //! Two things live here, not one. Below the test banner further down sits | 6 | //! The peer the tests below dial with is quic.Client — the shipping one, not |
| 7 | //! `TestClient`, a real QUIC peer the daemon's own tests drive; it is | 7 | //! a second copy of it living here. |
| 8 | //! exported for server.zig's benefit and is scaffolding, not a transport. | ||
| 9 | //! The shipping client is quic_client.zig. | ||
| 10 | //! | 8 | //! |
| 11 | //! This file follows proxy.zig's discipline: it knows NOTHING about the | 9 | //! This file follows proxy.zig's discipline: it knows NOTHING about the |
| 12 | //! frame protocol it carries. If a `proto.` import ever appears here, | 10 | //! frame protocol it carries. If a `proto.` import ever appears here, |
| @@ -947,7 +945,7 @@ pub const Listener = struct { | |||
| 947 | .cont => {}, | 945 | .cont => {}, |
| 948 | } | 946 | } |
| 949 | 947 | ||
| 950 | // No refusal check here, unlike quic_client's send: this socket | 948 | // No refusal check here, unlike quic.Client's send: this socket |
| 951 | // is UNCONNECTED, so the kernel has no peer to attribute an ICMP | 949 | // is UNCONNECTED, so the kernel has no peer to attribute an ICMP |
| 952 | // unreachable to and never delivers one. The asymmetry with the | 950 | // unreachable to and never delivers one. The asymmetry with the |
| 953 | // client is the sockets', not drift. | 951 | // client is the sockets', not drift. |
| @@ -983,246 +981,50 @@ pub const Listener = struct { | |||
| 983 | // --------------------------------------------------------------------------- | 981 | // --------------------------------------------------------------------------- |
| 984 | // Tests: a real handshake against a real client, in one process. | 982 | // Tests: a real handshake against a real client, in one process. |
| 985 | // | 983 | // |
| 986 | // The client below is test scaffolding, and not a stand-in for the real | 984 | // The listener cannot be tested by inspection — a QUIC handshake either |
| 987 | // one: quic_client.zig is the shipping transport. It exists because the | 985 | // completes against a real peer or it does not — so the tests below dial it |
| 988 | // listener cannot be tested by inspection — a QUIC handshake either | 986 | // with the shipping client, wrapped for the two conveniences a test wants. |
| 989 | // completes against a real peer or it does not. | ||
| 990 | // --------------------------------------------------------------------------- | 987 | // --------------------------------------------------------------------------- |
| 991 | 988 | ||
| 992 | var g_client_key: quic.Key = undefined; | 989 | /// The shipping `quic.Client`, plus the two things only a test wants: a |
| 993 | 990 | /// backlog it re-offers as the ring drains — `send` takes what fits, and a | |
| 994 | fn pskClientCb( | 991 | /// payload larger than `egress_cap` needs somebody to hold the rest — and a |
| 995 | ssl: ?*c.WOLFSSL, | 992 | /// value a stack `defer` can deinit. |
| 996 | hint: [*c]const u8, | 993 | /// |
| 997 | identity: [*c]u8, | 994 | /// It replaced a second hand-written QUIC client that lived here. That copy |
| 998 | id_max: c_uint, | 995 | /// existed because the folder rule forbids the server naming a client |
| 999 | key_out: [*c]u8, | 996 | /// module; `quic.Client` moving down to quic.zig, which both ends already |
| 1000 | key_max: c_uint, | 997 | /// import, dissolved the reason rather than the rule. |
| 1001 | ciphersuite: [*c][*c]const u8, | 998 | pub const TestPeer = struct { |
| 1002 | ) callconv(.c) c_uint { | 999 | cl: *quic.Client, |
| 1003 | _ = ssl; | ||
| 1004 | _ = hint; | ||
| 1005 | return quic.answerPsk(g_client_key, identity, id_max, key_out, key_max, ciphersuite); | ||
| 1006 | } | ||
| 1007 | |||
| 1008 | /// Test scaffolding, exported so the daemon's own tests can drive a real | ||
| 1009 | /// QUIC peer. Not the client transport — that is quic_client.zig — and | ||
| 1010 | /// deliberately less: no reconnect, no resumption, a fixed receive buffer. | ||
| 1011 | pub const TestClient = struct { | ||
| 1012 | fd: std.posix.fd_t, | ||
| 1013 | ssl_ctx: ?*c.WOLFSSL_CTX, | ||
| 1014 | ssl: ?*c.WOLFSSL, | ||
| 1015 | conn: ?*c.ngtcp2_conn, | ||
| 1016 | conn_ref: c.ngtcp2_crypto_conn_ref, | ||
| 1017 | remote: std.posix.sockaddr.storage, | ||
| 1018 | remote_len: std.posix.socklen_t, | ||
| 1019 | local: std.posix.sockaddr.storage, | ||
| 1020 | local_len: std.posix.socklen_t, | ||
| 1021 | stream_id: i64 = -1, | ||
| 1022 | handshake_done: bool = false, | ||
| 1023 | echoed: usize = 0, | ||
| 1024 | /// When set, every arriving byte is compared against this at the offset | ||
| 1025 | /// it arrives at. A transport that echoes the right COUNT of the wrong | ||
| 1026 | /// bytes satisfies a length assertion and fails this one. | ||
| 1027 | verify: ?[]const u8 = null, | ||
| 1028 | mismatch: bool = false, | ||
| 1029 | /// Whether to give the peer more stream window as data is consumed. | ||
| 1030 | /// Turning it off is how a test reaches the blocked-stream path, which | ||
| 1031 | /// is a documented return value rather than a failure. | ||
| 1032 | extend: bool = true, | ||
| 1033 | out: []const u8 = &.{}, | 1000 | out: []const u8 = &.{}, |
| 1034 | out_sent: usize = 0, | 1001 | out_sent: usize = 0, |
| 1035 | /// Everything the peer sent, so a test can walk frames out of it. | ||
| 1036 | recv_buf: [128 * 1024]u8 = undefined, | ||
| 1037 | recv_len: usize = 0, | ||
| 1038 | /// Set when the peer's CONNECTION_CLOSE reaches this side. The oracle | ||
| 1039 | /// for closeAll is the client observing the close, not bytes the server | ||
| 1040 | /// queued — a packet not received is an unobserved close. | ||
| 1041 | draining: bool = false, | ||
| 1042 | |||
| 1043 | fn getConn(ref: [*c]c.ngtcp2_crypto_conn_ref) callconv(.c) ?*c.ngtcp2_conn { | ||
| 1044 | const s: *TestClient = @ptrCast(@alignCast(ref.*.user_data)); | ||
| 1045 | return s.conn; | ||
| 1046 | } | ||
| 1047 | 1002 | ||
| 1048 | fn onHandshake(_: ?*c.ngtcp2_conn, ud: ?*anyopaque) callconv(.c) c_int { | 1003 | /// Idle 30s, not the listener's: a peer that PINGed on its own |
| 1049 | const s: *TestClient = @ptrCast(@alignCast(ud.?)); | 1004 | /// schedule would answer the keepalive test's question for it. |
| 1050 | s.handshake_done = true; | 1005 | pub fn init(addr: std.net.Address, key: quic.Key) !TestPeer { |
| 1051 | return 0; | 1006 | return .{ .cl = try quic.Client.connect(std.testing.allocator, addr, key, 30_000) }; |
| 1052 | } | 1007 | } |
| 1053 | 1008 | ||
| 1054 | fn onStreams(cn: ?*c.ngtcp2_conn, _: u64, ud: ?*anyopaque) callconv(.c) c_int { | 1009 | pub fn deinit(self: *TestPeer) void { |
| 1055 | const s: *TestClient = @ptrCast(@alignCast(ud.?)); | 1010 | self.cl.deinit(); |
| 1056 | if (s.stream_id == -1) { | ||
| 1057 | var sid: i64 = -1; | ||
| 1058 | if (c.ngtcp2_conn_open_bidi_stream(cn, &sid, null) == 0) s.stream_id = sid; | ||
| 1059 | } | ||
| 1060 | return 0; | ||
| 1061 | } | 1011 | } |
| 1062 | 1012 | ||
| 1063 | fn onData( | 1013 | /// Bytes to hand the ring, from the top, as room appears. |
| 1064 | cn: ?*c.ngtcp2_conn, | 1014 | pub fn offer(self: *TestPeer, bytes: []const u8) void { |
| 1065 | _: u32, | 1015 | self.out = bytes; |
| 1066 | stream_id: i64, | 1016 | self.out_sent = 0; |
| 1067 | _: u64, | ||
| 1068 | data: [*c]const u8, | ||
| 1069 | datalen: usize, | ||
| 1070 | ud: ?*anyopaque, | ||
| 1071 | _: ?*anyopaque, | ||
| 1072 | ) callconv(.c) c_int { | ||
| 1073 | const s: *TestClient = @ptrCast(@alignCast(ud.?)); | ||
| 1074 | // The client must extend too: without this the SERVER stalls once | ||
| 1075 | // it has echoed a window's worth back, and the large-payload test | ||
| 1076 | // below would hang rather than fail. | ||
| 1077 | if (s.extend) { | ||
| 1078 | _ = c.ngtcp2_conn_extend_max_stream_offset(cn, stream_id, datalen); | ||
| 1079 | _ = c.ngtcp2_conn_extend_max_offset(cn, datalen); | ||
| 1080 | } | ||
| 1081 | if (s.verify) |exp| { | ||
| 1082 | const off = s.echoed; | ||
| 1083 | if (off + datalen > exp.len or | ||
| 1084 | !std.mem.eql(u8, exp[off..][0..datalen], data[0..datalen])) | ||
| 1085 | { | ||
| 1086 | s.mismatch = true; | ||
| 1087 | } | ||
| 1088 | } | ||
| 1089 | s.echoed += datalen; | ||
| 1090 | if (s.recv_len + datalen <= s.recv_buf.len) { | ||
| 1091 | @memcpy(s.recv_buf[s.recv_len..][0..datalen], data[0..datalen]); | ||
| 1092 | s.recv_len += datalen; | ||
| 1093 | } | ||
| 1094 | return 0; | ||
| 1095 | } | 1017 | } |
| 1096 | 1018 | ||
| 1097 | pub fn init(server_addr: std.net.Address, key: quic.Key) !TestClient { | 1019 | /// One service pass: top the ring up, then read, tick and write. |
| 1098 | g_client_key = key; | 1020 | pub fn drain(self: *TestPeer) void { |
| 1099 | const fd = try std.posix.socket( | 1021 | if (self.out_sent < self.out.len) self.out_sent += self.cl.send(self.out[self.out_sent..]); |
| 1100 | server_addr.any.family, | 1022 | self.cl.pump(); |
| 1101 | std.posix.SOCK.DGRAM | std.posix.SOCK.NONBLOCK | std.posix.SOCK.CLOEXEC, | ||
| 1102 | 0, | ||
| 1103 | ); | ||
| 1104 | var self: TestClient = .{ | ||
| 1105 | .fd = fd, | ||
| 1106 | .ssl_ctx = null, | ||
| 1107 | .ssl = null, | ||
| 1108 | .conn = null, | ||
| 1109 | .conn_ref = undefined, | ||
| 1110 | .remote = undefined, | ||
| 1111 | .remote_len = server_addr.getOsSockLen(), | ||
| 1112 | .local = undefined, | ||
| 1113 | .local_len = @sizeOf(std.posix.sockaddr.storage), | ||
| 1114 | }; | ||
| 1115 | @memcpy(std.mem.asBytes(&self.remote)[0..self.remote_len], std.mem.asBytes(&server_addr.any)[0..self.remote_len]); | ||
| 1116 | try std.posix.connect(fd, &server_addr.any, self.remote_len); | ||
| 1117 | try std.posix.getsockname(fd, @ptrCast(&self.local), &self.local_len); | ||
| 1118 | return self; | ||
| 1119 | } | 1023 | } |
| 1120 | 1024 | ||
| 1121 | pub fn start(self: *TestClient) !void { | 1025 | /// Nothing consumes, so this counts and `cl.in.items` is the bytes. |
| 1122 | // Set before clientTls, which hands wolfSSL a pointer to it. | 1026 | pub fn echoed(self: *const TestPeer) usize { |
| 1123 | self.conn_ref = .{ .get_conn = getConn, .user_data = self }; | 1027 | return self.cl.in.items.len; |
| 1124 | const tls = try quic.clientTls(pskClientCb, &self.conn_ref); | ||
| 1125 | self.ssl_ctx = tls.ctx; | ||
| 1126 | const ssl = tls.ssl; | ||
| 1127 | self.ssl = ssl; | ||
| 1128 | |||
| 1129 | var dcid: c.ngtcp2_cid = undefined; | ||
| 1130 | dcid.datalen = 16; | ||
| 1131 | std.crypto.random.bytes(dcid.data[0..16]); | ||
| 1132 | var scid: c.ngtcp2_cid = undefined; | ||
| 1133 | scid.datalen = 8; | ||
| 1134 | std.crypto.random.bytes(scid.data[0..8]); | ||
| 1135 | |||
| 1136 | var cbs = quic.clientCallbacks(); | ||
| 1137 | cbs.handshake_completed = onHandshake; | ||
| 1138 | cbs.extend_max_local_streams_bidi = onStreams; | ||
| 1139 | cbs.recv_stream_data = onData; | ||
| 1140 | |||
| 1141 | var settings: c.ngtcp2_settings = undefined; | ||
| 1142 | c.ngtcp2_settings_default_versioned(c.NGTCP2_SETTINGS_VERSION, &settings); | ||
| 1143 | settings.initial_ts = quic.timestampNs(); | ||
| 1144 | var params = quic.clientParams(); | ||
| 1145 | |||
| 1146 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 1147 | var conn: ?*c.ngtcp2_conn = null; | ||
| 1148 | if (c.ngtcp2_conn_client_new_versioned( | ||
| 1149 | &conn, | ||
| 1150 | &dcid, | ||
| 1151 | &scid, | ||
| 1152 | &path, | ||
| 1153 | c.NGTCP2_PROTO_VER_V1, | ||
| 1154 | c.NGTCP2_CALLBACKS_VERSION, | ||
| 1155 | &cbs, | ||
| 1156 | c.NGTCP2_SETTINGS_VERSION, | ||
| 1157 | &settings, | ||
| 1158 | c.NGTCP2_TRANSPORT_PARAMS_VERSION, | ||
| 1159 | ¶ms, | ||
| 1160 | null, | ||
| 1161 | self, | ||
| 1162 | ) != 0) return error.ConnInit; | ||
| 1163 | self.conn = conn; | ||
| 1164 | c.ngtcp2_conn_set_tls_native_handle(conn, ssl); | ||
| 1165 | } | ||
| 1166 | |||
| 1167 | pub fn drain(self: *TestClient) void { | ||
| 1168 | const conn = self.conn orelse return; | ||
| 1169 | var buf: [quic.max_udp]u8 = undefined; | ||
| 1170 | while (true) { | ||
| 1171 | var ps: c.ngtcp2_path_storage = undefined; | ||
| 1172 | c.ngtcp2_path_storage_zero(&ps); | ||
| 1173 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 1174 | var wrote: c.ngtcp2_ssize = 0; | ||
| 1175 | var vec: c.ngtcp2_vec = undefined; | ||
| 1176 | var vcnt: usize = 0; | ||
| 1177 | var sid: i64 = -1; | ||
| 1178 | if (self.stream_id != -1 and self.out_sent < self.out.len) { | ||
| 1179 | vec = .{ .base = @constCast(self.out.ptr) + self.out_sent, .len = self.out.len - self.out_sent }; | ||
| 1180 | vcnt = 1; | ||
| 1181 | sid = self.stream_id; | ||
| 1182 | } | ||
| 1183 | const n = c.ngtcp2_conn_writev_stream_versioned( | ||
| 1184 | conn, | ||
| 1185 | &ps.path, | ||
| 1186 | c.NGTCP2_PKT_INFO_VERSION, | ||
| 1187 | &pi, | ||
| 1188 | &buf, | ||
| 1189 | buf.len, | ||
| 1190 | &wrote, | ||
| 1191 | 0, | ||
| 1192 | sid, | ||
| 1193 | if (vcnt > 0) &vec else null, | ||
| 1194 | vcnt, | ||
| 1195 | quic.timestampNs(), | ||
| 1196 | ); | ||
| 1197 | // Same ordering as the listener's drain, and for the same | ||
| 1198 | // reason: ngtcp2 can commit the stream offset and still return | ||
| 1199 | // an error, so the bytes are accounted before the check or they | ||
| 1200 | // get offered again at an offset the peer has moved past. | ||
| 1201 | if (wrote > 0) self.out_sent += @intCast(wrote); | ||
| 1202 | if (n < 0) return; | ||
| 1203 | if (n == 0) return; | ||
| 1204 | _ = std.posix.send(self.fd, buf[0..@intCast(n)], 0) catch return; | ||
| 1205 | } | ||
| 1206 | } | ||
| 1207 | |||
| 1208 | pub fn readable(self: *TestClient) void { | ||
| 1209 | var buf: [65536]u8 = undefined; | ||
| 1210 | while (true) { | ||
| 1211 | const n = std.posix.recv(self.fd, &buf, 0) catch return; | ||
| 1212 | if (n == 0) return; | ||
| 1213 | const conn = self.conn orelse return; | ||
| 1214 | var path = quic.pathFrom(&self.local, self.local_len, &self.remote, self.remote_len); | ||
| 1215 | var pi: c.ngtcp2_pkt_info = .{ .ecn = 0 }; | ||
| 1216 | _ = c.ngtcp2_conn_read_pkt(conn, &path, &pi, &buf, n, quic.timestampNs()); | ||
| 1217 | if (c.ngtcp2_conn_in_draining_period(conn) != 0) self.draining = true; | ||
| 1218 | } | ||
| 1219 | } | ||
| 1220 | |||
| 1221 | pub fn deinit(self: *TestClient) void { | ||
| 1222 | if (self.conn) |cn| c.ngtcp2_conn_del(cn); | ||
| 1223 | if (self.ssl) |s| c.wolfSSL_free(s); | ||
| 1224 | if (self.ssl_ctx) |x| c.wolfSSL_CTX_free(x); | ||
| 1225 | std.posix.close(self.fd); | ||
| 1226 | } | 1028 | } |
| 1227 | }; | 1029 | }; |
| 1228 | 1030 | ||
| @@ -1277,18 +1079,17 @@ const EchoOwner = struct { | |||
| 1277 | /// Drive both ends until `done` or the deadline. Single-threaded on purpose: | 1079 | /// Drive both ends until `done` or the deadline. Single-threaded on purpose: |
| 1278 | /// a test that needs threads to make a handshake happen is a test that will | 1080 | /// a test that needs threads to make a handshake happen is a test that will |
| 1279 | /// wedge one day. | 1081 | /// wedge one day. |
| 1280 | fn pump(l: *Listener, cl: *TestClient, ms: u64, done: *const fn (*EchoOwner, *TestClient) bool, owner: *EchoOwner) bool { | 1082 | fn pump(l: *Listener, cl: *TestPeer, ms: u64, done: *const fn (*EchoOwner, *TestPeer) bool, owner: *EchoOwner) bool { |
| 1281 | var waited: u64 = 0; | 1083 | var waited: u64 = 0; |
| 1282 | while (waited < ms) { | 1084 | while (waited < ms) { |
| 1283 | if (done(owner, cl)) return true; | 1085 | if (done(owner, cl)) return true; |
| 1284 | var fds = [_]std.posix.pollfd{ | 1086 | var fds = [_]std.posix.pollfd{ |
| 1285 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1087 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1286 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1088 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1287 | }; | 1089 | }; |
| 1288 | const ready = std.posix.poll(&fds, 10) catch return false; | 1090 | const ready = std.posix.poll(&fds, 10) catch return false; |
| 1289 | if (ready > 0) { | 1091 | if (ready > 0) { |
| 1290 | if (fds[0].revents != 0) l.readable(); | 1092 | if (fds[0].revents != 0) l.readable(); |
| 1291 | if (fds[1].revents != 0) cl.readable(); | ||
| 1292 | } else { | 1093 | } else { |
| 1293 | waited += 10; | 1094 | waited += 10; |
| 1294 | } | 1095 | } |
| @@ -1328,18 +1129,16 @@ test "Listener: PSK handshake, Retry, and a payload larger than the initial wind | |||
| 1328 | const setup = try loopbackListener(alloc, key, &owner, 5000); | 1129 | const setup = try loopbackListener(alloc, key, &owner, 5000); |
| 1329 | defer setup.l.deinit(); | 1130 | defer setup.l.deinit(); |
| 1330 | 1131 | ||
| 1331 | var cl = try TestClient.init(setup.addr, key); | 1132 | var cl = try TestPeer.init(setup.addr, key); |
| 1332 | defer cl.deinit(); | 1133 | defer cl.deinit(); |
| 1333 | try cl.start(); | ||
| 1334 | cl.drain(); | ||
| 1335 | 1134 | ||
| 1336 | // Both sides, not just the client: the client declares the handshake | 1135 | // Both sides, not just the client: the client declares the handshake |
| 1337 | // complete one flight before the server does, so waiting only on the | 1136 | // complete one flight before the server does, so waiting only on the |
| 1338 | // client stops the pump while the server's final flight is still in the | 1137 | // client stops the pump while the server's final flight is still in the |
| 1339 | // air. | 1138 | // air. |
| 1340 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { | 1139 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { |
| 1341 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1140 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1342 | return t.handshake_done and o.opened >= 1; | 1141 | return t.cl.handshake_done and o.opened >= 1; |
| 1343 | } | 1142 | } |
| 1344 | }.f, &owner)); | 1143 | }.f, &owner)); |
| 1345 | 1144 | ||
| @@ -1365,31 +1164,29 @@ test "Listener: PSK handshake, Retry, and a payload larger than the initial wind | |||
| 1365 | // production too, which is why it is worth a test rather than a comment. | 1164 | // production too, which is why it is worth a test rather than a comment. |
| 1366 | const payload = try alloc.alloc(u8, 3 * 1024 * 1024); | 1165 | const payload = try alloc.alloc(u8, 3 * 1024 * 1024); |
| 1367 | defer alloc.free(payload); | 1166 | defer alloc.free(payload); |
| 1368 | // A repeating byte pattern, checked as it arrives rather than counted | 1167 | // A repeating byte pattern, compared byte for byte, not merely counted. |
| 1369 | // at the end. Counting alone cannot tell a working transport from one | 1168 | // Counting alone cannot tell a working transport from one that delivers |
| 1370 | // that delivers the right NUMBER of the wrong bytes — reordered, | 1169 | // the right NUMBER of the wrong bytes — reordered, duplicated, or read |
| 1371 | // duplicated, or read back out of a buffer that had been recycled | 1170 | // back out of a buffer that had been recycled underneath it, which is |
| 1372 | // underneath it, which is precisely the failure this file has already | 1171 | // precisely the failure this file has already had once. |
| 1373 | // had once. | ||
| 1374 | for (payload, 0..) |*b, i| b.* = @truncate(i); | 1172 | for (payload, 0..) |*b, i| b.* = @truncate(i); |
| 1375 | cl.verify = payload; | 1173 | cl.offer(payload); |
| 1376 | cl.out = payload; | ||
| 1377 | cl.drain(); | 1174 | cl.drain(); |
| 1378 | 1175 | ||
| 1379 | const ok = pump(setup.l, &cl, 30_000, struct { | 1176 | const ok = pump(setup.l, &cl, 30_000, struct { |
| 1380 | fn f(_: *EchoOwner, t: *TestClient) bool { | 1177 | fn f(_: *EchoOwner, t: *TestPeer) bool { |
| 1381 | return t.echoed >= 3 * 1024 * 1024; | 1178 | return t.echoed() >= 3 * 1024 * 1024; |
| 1382 | } | 1179 | } |
| 1383 | }.f, &owner); | 1180 | }.f, &owner); |
| 1384 | if (!ok) { | 1181 | if (!ok) { |
| 1385 | std.debug.print( | 1182 | std.debug.print( |
| 1386 | "stalled: server saw {d} bytes, client got {d} of {d} back\n", | 1183 | "stalled: server saw {d} bytes, client got {d} of {d} back\n", |
| 1387 | .{ owner.received, cl.echoed, payload.len }, | 1184 | .{ owner.received, cl.echoed(), payload.len }, |
| 1388 | ); | 1185 | ); |
| 1389 | } | 1186 | } |
| 1390 | try std.testing.expect(ok); | 1187 | try std.testing.expect(ok); |
| 1391 | try std.testing.expectEqual(@as(usize, 3 * 1024 * 1024), owner.received); | 1188 | try std.testing.expectEqual(@as(usize, 3 * 1024 * 1024), owner.received); |
| 1392 | try std.testing.expect(!cl.mismatch); | 1189 | try std.testing.expectEqualSlices(u8, payload, cl.cl.in.items); |
| 1393 | 1190 | ||
| 1394 | // Piggybacking on the one place a LIVE connection with a finite expiry | 1191 | // Piggybacking on the one place a LIVE connection with a finite expiry |
| 1395 | // exists: negative is poll(2)'s "wait forever", and the timeout fold | 1192 | // exists: negative is poll(2)'s "wait forever", and the timeout fold |
| @@ -1411,20 +1208,18 @@ test "Listener: a client holding the wrong key never completes a handshake" { | |||
| 1411 | const setup = try loopbackListener(alloc, server_key, &owner, 5000); | 1208 | const setup = try loopbackListener(alloc, server_key, &owner, 5000); |
| 1412 | defer setup.l.deinit(); | 1209 | defer setup.l.deinit(); |
| 1413 | 1210 | ||
| 1414 | var cl = try TestClient.init(setup.addr, wrong_key); | 1211 | var cl = try TestPeer.init(setup.addr, wrong_key); |
| 1415 | defer cl.deinit(); | 1212 | defer cl.deinit(); |
| 1416 | try cl.start(); | ||
| 1417 | cl.drain(); | ||
| 1418 | 1213 | ||
| 1419 | // Deliberately generous: the point is that it never succeeds, not that | 1214 | // Deliberately generous: the point is that it never succeeds, not that |
| 1420 | // it fails fast. A shorter window could pass for the wrong reason. | 1215 | // it fails fast. A shorter window could pass for the wrong reason. |
| 1421 | _ = pump(setup.l, &cl, 3000, struct { | 1216 | _ = pump(setup.l, &cl, 3000, struct { |
| 1422 | fn f(_: *EchoOwner, t: *TestClient) bool { | 1217 | fn f(_: *EchoOwner, t: *TestPeer) bool { |
| 1423 | return t.handshake_done; | 1218 | return t.cl.handshake_done; |
| 1424 | } | 1219 | } |
| 1425 | }.f, &owner); | 1220 | }.f, &owner); |
| 1426 | 1221 | ||
| 1427 | try std.testing.expect(!cl.handshake_done); | 1222 | try std.testing.expect(!cl.cl.handshake_done); |
| 1428 | try std.testing.expectEqual(@as(usize, 0), owner.opened); | 1223 | try std.testing.expectEqual(@as(usize, 0), owner.opened); |
| 1429 | try std.testing.expectEqual(@as(usize, 0), owner.received); | 1224 | try std.testing.expectEqual(@as(usize, 0), owner.received); |
| 1430 | 1225 | ||
| @@ -1451,13 +1246,11 @@ test "Listener: keepalive carries an idle connection past its idle timeout" { | |||
| 1451 | const setup = try loopbackListener(alloc, key, &owner, idle_ms); | 1246 | const setup = try loopbackListener(alloc, key, &owner, idle_ms); |
| 1452 | defer setup.l.deinit(); | 1247 | defer setup.l.deinit(); |
| 1453 | 1248 | ||
| 1454 | var cl = try TestClient.init(setup.addr, key); | 1249 | var cl = try TestPeer.init(setup.addr, key); |
| 1455 | defer cl.deinit(); | 1250 | defer cl.deinit(); |
| 1456 | try cl.start(); | ||
| 1457 | cl.drain(); | ||
| 1458 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { | 1251 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { |
| 1459 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1252 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1460 | return t.handshake_done and o.opened > 0; | 1253 | return t.cl.handshake_done and o.opened > 0; |
| 1461 | } | 1254 | } |
| 1462 | }.f, &owner)); | 1255 | }.f, &owner)); |
| 1463 | 1256 | ||
| @@ -1466,7 +1259,7 @@ test "Listener: keepalive carries an idle connection past its idle timeout" { | |||
| 1466 | // least this long — the keepalive PINGs it does carry are the whole | 1259 | // least this long — the keepalive PINGs it does carry are the whole |
| 1467 | // point, and they are not application traffic. | 1260 | // point, and they are not application traffic. |
| 1468 | _ = pump(setup.l, &cl, idle_ms * 3, struct { | 1261 | _ = pump(setup.l, &cl, idle_ms * 3, struct { |
| 1469 | fn f(_: *EchoOwner, _: *TestClient) bool { | 1262 | fn f(_: *EchoOwner, _: *TestPeer) bool { |
| 1470 | return false; | 1263 | return false; |
| 1471 | } | 1264 | } |
| 1472 | }.f, &owner); | 1265 | }.f, &owner); |
| @@ -1475,11 +1268,10 @@ test "Listener: keepalive carries an idle connection past its idle timeout" { | |||
| 1475 | try std.testing.expectEqual(@as(usize, 0), owner.closed); | 1268 | try std.testing.expectEqual(@as(usize, 0), owner.closed); |
| 1476 | // ...and the connection is not merely un-reaped but still usable, which | 1269 | // ...and the connection is not merely un-reaped but still usable, which |
| 1477 | // a live conn struct on its own would not prove. | 1270 | // a live conn struct on its own would not prove. |
| 1478 | cl.out = "still-here"; | 1271 | cl.offer("still-here"); |
| 1479 | cl.out_sent = 0; | ||
| 1480 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { | 1272 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { |
| 1481 | fn f(_: *EchoOwner, t: *TestClient) bool { | 1273 | fn f(_: *EchoOwner, t: *TestPeer) bool { |
| 1482 | return t.echoed >= "still-here".len; | 1274 | return t.echoed() >= "still-here".len; |
| 1483 | } | 1275 | } |
| 1484 | }.f, &owner)); | 1276 | }.f, &owner)); |
| 1485 | try std.testing.expectEqual(@as(usize, 0), owner.closed); | 1277 | try std.testing.expectEqual(@as(usize, 0), owner.closed); |
| @@ -1491,44 +1283,41 @@ test "Listener: keepalive carries an idle connection past its idle timeout" { | |||
| 1491 | /// client say something first. Waits until the round trip is acknowledged, | 1283 | /// client say something first. Waits until the round trip is acknowledged, |
| 1492 | /// so the egress ring is empty again and the next assertion is about the | 1284 | /// so the egress ring is empty again and the next assertion is about the |
| 1493 | /// test's own bytes. | 1285 | /// test's own bytes. |
| 1494 | fn openStream(l: *Listener, cl: *TestClient, owner: *EchoOwner) !void { | 1286 | fn openStream(l: *Listener, cl: *TestPeer, owner: *EchoOwner) !void { |
| 1495 | cl.out = "hi"; | 1287 | cl.offer("hi"); |
| 1496 | cl.out_sent = 0; | ||
| 1497 | cl.drain(); | 1288 | cl.drain(); |
| 1498 | const ok = pumpUntil(l, cl, owner, 10_000, struct { | 1289 | const ok = pumpUntil(l, cl, owner, 10_000, struct { |
| 1499 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1290 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1500 | return o.received >= 2 and t.echoed >= 2 and l_pending(o) == 0; | 1291 | return o.received >= 2 and t.echoed() >= 2 and l_pending(o) == 0; |
| 1501 | } | 1292 | } |
| 1502 | fn l_pending(o: *EchoOwner) usize { | 1293 | fn l_pending(o: *EchoOwner) usize { |
| 1503 | return o.listener.pendingBytes(o.id); | 1294 | return o.listener.pendingBytes(o.id); |
| 1504 | } | 1295 | } |
| 1505 | }.f); | 1296 | }.f); |
| 1506 | if (!ok) return error.StreamNeverOpened; | 1297 | if (!ok) return error.StreamNeverOpened; |
| 1507 | cl.out = &.{}; | 1298 | cl.offer(&.{}); |
| 1508 | cl.out_sent = 0; | 1299 | cl.cl.in.clearRetainingCapacity(); |
| 1509 | cl.echoed = 0; | ||
| 1510 | } | 1300 | } |
| 1511 | 1301 | ||
| 1512 | /// `pump` counts only quiet iterations toward its deadline, which is wrong | 1302 | /// `pump` counts only quiet iterations toward its deadline, which is wrong |
| 1513 | /// for a bulk transfer that is busy throughout. | 1303 | /// for a bulk transfer that is busy throughout. |
| 1514 | fn pumpUntil( | 1304 | fn pumpUntil( |
| 1515 | l: *Listener, | 1305 | l: *Listener, |
| 1516 | cl: *TestClient, | 1306 | cl: *TestPeer, |
| 1517 | owner: *EchoOwner, | 1307 | owner: *EchoOwner, |
| 1518 | wall_ms: i64, | 1308 | wall_ms: i64, |
| 1519 | done: *const fn (*EchoOwner, *TestClient) bool, | 1309 | done: *const fn (*EchoOwner, *TestPeer) bool, |
| 1520 | ) bool { | 1310 | ) bool { |
| 1521 | const deadline = std.time.milliTimestamp() + wall_ms; | 1311 | const deadline = std.time.milliTimestamp() + wall_ms; |
| 1522 | while (std.time.milliTimestamp() < deadline) { | 1312 | while (std.time.milliTimestamp() < deadline) { |
| 1523 | if (done(owner, cl)) return true; | 1313 | if (done(owner, cl)) return true; |
| 1524 | var fds = [_]std.posix.pollfd{ | 1314 | var fds = [_]std.posix.pollfd{ |
| 1525 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1315 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1526 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1316 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1527 | }; | 1317 | }; |
| 1528 | const ready = std.posix.poll(&fds, 5) catch return false; | 1318 | const ready = std.posix.poll(&fds, 5) catch return false; |
| 1529 | if (ready > 0) { | 1319 | if (ready > 0) { |
| 1530 | if (fds[0].revents != 0) l.readable(); | 1320 | if (fds[0].revents != 0) l.readable(); |
| 1531 | if (fds[1].revents != 0) cl.readable(); | ||
| 1532 | } | 1321 | } |
| 1533 | l.tick(); | 1322 | l.tick(); |
| 1534 | owner.flush(); | 1323 | owner.flush(); |
| @@ -1549,7 +1338,7 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" { | |||
| 1549 | const setup = try loopbackListener(alloc, key, &owner, 10_000); | 1338 | const setup = try loopbackListener(alloc, key, &owner, 10_000); |
| 1550 | defer setup.l.deinit(); | 1339 | defer setup.l.deinit(); |
| 1551 | 1340 | ||
| 1552 | var cl = try TestClient.init(setup.addr, key); | 1341 | var cl = try TestPeer.init(setup.addr, key); |
| 1553 | defer cl.deinit(); | 1342 | defer cl.deinit(); |
| 1554 | 1343 | ||
| 1555 | // A deliberately tiny receive buffer, which is how loopback is made to | 1344 | // A deliberately tiny receive buffer, which is how loopback is made to |
| @@ -1561,17 +1350,15 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" { | |||
| 1561 | // reached the same path by accident, about one run in twenty, and | 1350 | // reached the same path by accident, about one run in twenty, and |
| 1562 | // segfaulted when it did. | 1351 | // segfaulted when it did. |
| 1563 | try std.posix.setsockopt( | 1352 | try std.posix.setsockopt( |
| 1564 | cl.fd, | 1353 | cl.cl.fd, |
| 1565 | std.posix.SOL.SOCKET, | 1354 | std.posix.SOL.SOCKET, |
| 1566 | std.posix.SO.RCVBUF, | 1355 | std.posix.SO.RCVBUF, |
| 1567 | &std.mem.toBytes(@as(c_int, 64 * 1024)), | 1356 | &std.mem.toBytes(@as(c_int, 64 * 1024)), |
| 1568 | ); | 1357 | ); |
| 1569 | 1358 | ||
| 1570 | try cl.start(); | ||
| 1571 | cl.drain(); | ||
| 1572 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { | 1359 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { |
| 1573 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1360 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1574 | return t.handshake_done and o.opened > 0; | 1361 | return t.cl.handshake_done and o.opened > 0; |
| 1575 | } | 1362 | } |
| 1576 | }.f)); | 1363 | }.f)); |
| 1577 | 1364 | ||
| @@ -1582,14 +1369,12 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" { | |||
| 1582 | defer alloc.free(payload); | 1369 | defer alloc.free(payload); |
| 1583 | for (payload, 0..) |*b, i| b.* = @truncate(i *% 31 +% 7); | 1370 | for (payload, 0..) |*b, i| b.* = @truncate(i *% 31 +% 7); |
| 1584 | 1371 | ||
| 1585 | cl.verify = payload; | 1372 | cl.offer(payload); |
| 1586 | cl.out = payload; | ||
| 1587 | cl.out_sent = 0; | ||
| 1588 | 1373 | ||
| 1589 | const ok = pumpUntil(setup.l, &cl, &owner, 60_000, struct { | 1374 | const ok = pumpUntil(setup.l, &cl, &owner, 60_000, struct { |
| 1590 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1375 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1591 | _ = o; | 1376 | _ = o; |
| 1592 | return t.echoed >= t.verify.?.len; | 1377 | return t.echoed() >= t.out.len; |
| 1593 | } | 1378 | } |
| 1594 | }.f); | 1379 | }.f); |
| 1595 | try std.testing.expect(ok); | 1380 | try std.testing.expect(ok); |
| @@ -1597,8 +1382,8 @@ test "Listener: bytes survive retransmission, which is what the buffer is for" { | |||
| 1597 | // Count AND content. A transport that lost a retransmission and carried | 1382 | // Count AND content. A transport that lost a retransmission and carried |
| 1598 | // on would land the right number of bytes in the wrong order, which no | 1383 | // on would land the right number of bytes in the wrong order, which no |
| 1599 | // length assertion can see. | 1384 | // length assertion can see. |
| 1600 | try std.testing.expectEqual(size, cl.echoed); | 1385 | try std.testing.expectEqual(size, cl.echoed()); |
| 1601 | try std.testing.expect(!cl.mismatch); | 1386 | try std.testing.expectEqualSlices(u8, payload, cl.cl.in.items); |
| 1602 | try std.testing.expectEqual(size, owner.received); | 1387 | try std.testing.expectEqual(size, owner.received); |
| 1603 | try std.testing.expectEqual(@as(usize, 0), owner.closed); | 1388 | try std.testing.expectEqual(@as(usize, 0), owner.closed); |
| 1604 | } | 1389 | } |
| @@ -1612,13 +1397,11 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" { | |||
| 1612 | const setup = try loopbackListener(alloc, key, &owner, 10_000); | 1397 | const setup = try loopbackListener(alloc, key, &owner, 10_000); |
| 1613 | defer setup.l.deinit(); | 1398 | defer setup.l.deinit(); |
| 1614 | 1399 | ||
| 1615 | var cl = try TestClient.init(setup.addr, key); | 1400 | var cl = try TestPeer.init(setup.addr, key); |
| 1616 | defer cl.deinit(); | 1401 | defer cl.deinit(); |
| 1617 | try cl.start(); | ||
| 1618 | cl.drain(); | ||
| 1619 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { | 1402 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { |
| 1620 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1403 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1621 | return t.handshake_done and o.opened > 0; | 1404 | return t.cl.handshake_done and o.opened > 0; |
| 1622 | } | 1405 | } |
| 1623 | }.f)); | 1406 | }.f)); |
| 1624 | 1407 | ||
| @@ -1644,11 +1427,11 @@ test "Listener.send: takes what fits, refuses when full, and recovers on acks" { | |||
| 1644 | // client is not the same event as the ring being free to reuse it, and | 1427 | // client is not the same event as the ring being free to reuse it, and |
| 1645 | // conflating them is what this buffer exists to stop anyone doing. | 1428 | // conflating them is what this buffer exists to stop anyone doing. |
| 1646 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 20_000, struct { | 1429 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 20_000, struct { |
| 1647 | fn f(o: *EchoOwner, _: *TestClient) bool { | 1430 | fn f(o: *EchoOwner, _: *TestPeer) bool { |
| 1648 | return o.listener.pendingBytes(o.id) == 0; | 1431 | return o.listener.pendingBytes(o.id) == 0; |
| 1649 | } | 1432 | } |
| 1650 | }.f)); | 1433 | }.f)); |
| 1651 | try std.testing.expectEqual(quic.egress_cap, cl.echoed); | 1434 | try std.testing.expectEqual(quic.egress_cap, cl.echoed()); |
| 1652 | try std.testing.expect(try setup.l.send(owner.id, "room again") > 0); | 1435 | try std.testing.expect(try setup.l.send(owner.id, "room again") > 0); |
| 1653 | 1436 | ||
| 1654 | // An id nobody owns is an error, not a silent success. | 1437 | // An id nobody owns is an error, not a silent success. |
| @@ -1669,13 +1452,11 @@ test "Listener: a full peer window blocks the stream without stopping the ACKs" | |||
| 1669 | const setup = try loopbackListener(alloc, key, &owner, idle_ms); | 1452 | const setup = try loopbackListener(alloc, key, &owner, idle_ms); |
| 1670 | defer setup.l.deinit(); | 1453 | defer setup.l.deinit(); |
| 1671 | 1454 | ||
| 1672 | var cl = try TestClient.init(setup.addr, key); | 1455 | var cl = try TestPeer.init(setup.addr, key); |
| 1673 | defer cl.deinit(); | 1456 | defer cl.deinit(); |
| 1674 | try cl.start(); | ||
| 1675 | cl.drain(); | ||
| 1676 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { | 1457 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { |
| 1677 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1458 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1678 | return t.handshake_done and o.opened > 0; | 1459 | return t.cl.handshake_done and o.opened > 0; |
| 1679 | } | 1460 | } |
| 1680 | }.f)); | 1461 | }.f)); |
| 1681 | 1462 | ||
| @@ -1683,7 +1464,7 @@ test "Listener: a full peer window blocks the stream without stopping the ACKs" | |||
| 1683 | 1464 | ||
| 1684 | // The client consumes but never grants more window, so the peer's | 1465 | // The client consumes but never grants more window, so the peer's |
| 1685 | // stream credit runs out and stays out. | 1466 | // stream credit runs out and stays out. |
| 1686 | cl.extend = false; | 1467 | cl.cl.extend_windows = false; |
| 1687 | 1468 | ||
| 1688 | const big = try alloc.alloc(u8, 4 * quic.egress_cap); | 1469 | const big = try alloc.alloc(u8, 4 * quic.egress_cap); |
| 1689 | defer alloc.free(big); | 1470 | defer alloc.free(big); |
| @@ -1693,13 +1474,13 @@ test "Listener: a full peer window blocks the stream without stopping the ACKs" | |||
| 1693 | 1474 | ||
| 1694 | // Well past the idle timeout, with the stream blocked the entire time. | 1475 | // Well past the idle timeout, with the stream blocked the entire time. |
| 1695 | _ = pumpUntil(setup.l, &cl, &owner, idle_ms * 3, struct { | 1476 | _ = pumpUntil(setup.l, &cl, &owner, idle_ms * 3, struct { |
| 1696 | fn f(_: *EchoOwner, _: *TestClient) bool { | 1477 | fn f(_: *EchoOwner, _: *TestPeer) bool { |
| 1697 | return false; | 1478 | return false; |
| 1698 | } | 1479 | } |
| 1699 | }.f); | 1480 | }.f); |
| 1700 | 1481 | ||
| 1701 | // The stream really did block — the client cannot have taken it all. | 1482 | // The stream really did block — the client cannot have taken it all. |
| 1702 | try std.testing.expect(cl.echoed < big.len); | 1483 | try std.testing.expect(cl.echoed() < big.len); |
| 1703 | // ...and the connection is alive, which it can only be if packets that | 1484 | // ...and the connection is alive, which it can only be if packets that |
| 1704 | // are not stream data kept flowing while it was blocked. | 1485 | // are not stream data kept flowing while it was blocked. |
| 1705 | try std.testing.expectEqual(@as(usize, 0), owner.closed); | 1486 | try std.testing.expectEqual(@as(usize, 0), owner.closed); |
| @@ -1715,13 +1496,11 @@ test "Listener: a packet addressed to any advertised CID reaches its connection" | |||
| 1715 | const setup = try loopbackListener(alloc, key, &owner, 10_000); | 1496 | const setup = try loopbackListener(alloc, key, &owner, 10_000); |
| 1716 | defer setup.l.deinit(); | 1497 | defer setup.l.deinit(); |
| 1717 | 1498 | ||
| 1718 | var cl = try TestClient.init(setup.addr, key); | 1499 | var cl = try TestPeer.init(setup.addr, key); |
| 1719 | defer cl.deinit(); | 1500 | defer cl.deinit(); |
| 1720 | try cl.start(); | ||
| 1721 | cl.drain(); | ||
| 1722 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { | 1501 | try std.testing.expect(pumpUntil(setup.l, &cl, &owner, 10_000, struct { |
| 1723 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1502 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 1724 | return t.handshake_done and o.opened > 0; | 1503 | return t.cl.handshake_done and o.opened > 0; |
| 1725 | } | 1504 | } |
| 1726 | }.f)); | 1505 | }.f)); |
| 1727 | 1506 | ||
| @@ -1864,34 +1643,31 @@ test "Listener: a reply queued just before a close still reaches the peer" { | |||
| 1864 | try std.posix.getsockname(l.fd, @ptrCast(&actual), &alen); | 1643 | try std.posix.getsockname(l.fd, @ptrCast(&actual), &alen); |
| 1865 | const addr = std.net.Address.initPosix(@ptrCast(@alignCast(&actual))); | 1644 | const addr = std.net.Address.initPosix(@ptrCast(@alignCast(&actual))); |
| 1866 | 1645 | ||
| 1867 | var cl = try TestClient.init(addr, key); | 1646 | var cl = try TestPeer.init(addr, key); |
| 1868 | defer cl.deinit(); | 1647 | defer cl.deinit(); |
| 1869 | try cl.start(); | ||
| 1870 | cl.drain(); | ||
| 1871 | 1648 | ||
| 1872 | cl.out = "attach"; | 1649 | cl.offer("attach"); |
| 1873 | var waited: u64 = 0; | 1650 | var waited: u64 = 0; |
| 1874 | while (waited < 10_000) : (waited += 5) { | 1651 | while (waited < 10_000) : (waited += 5) { |
| 1875 | var fds = [_]std.posix.pollfd{ | 1652 | var fds = [_]std.posix.pollfd{ |
| 1876 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1653 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1877 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1654 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1878 | }; | 1655 | }; |
| 1879 | _ = std.posix.poll(&fds, 5) catch break; | 1656 | _ = std.posix.poll(&fds, 5) catch break; |
| 1880 | if (fds[0].revents != 0) l.readable(); | 1657 | if (fds[0].revents != 0) l.readable(); |
| 1881 | if (fds[1].revents != 0) cl.readable(); | ||
| 1882 | l.tick(); | 1658 | l.tick(); |
| 1883 | cl.drain(); | 1659 | cl.drain(); |
| 1884 | for (l.conns) |slot| { | 1660 | for (l.conns) |slot| { |
| 1885 | if (slot) |cn| l.drain(cn); | 1661 | if (slot) |cn| l.drain(cn); |
| 1886 | } | 1662 | } |
| 1887 | if (cl.recv_len >= "REFUSED-FULL".len) break; | 1663 | if (cl.echoed() >= "REFUSED-FULL".len) break; |
| 1888 | } | 1664 | } |
| 1889 | 1665 | ||
| 1890 | try std.testing.expectEqual(@as(usize, "REFUSED-FULL".len), owner.queued); | 1666 | try std.testing.expectEqual(@as(usize, "REFUSED-FULL".len), owner.queued); |
| 1891 | // The bytes are on the peer, not merely in a ring that was then freed. | 1667 | // The bytes are on the peer, not merely in a ring that was then freed. |
| 1892 | try std.testing.expect(std.mem.indexOf( | 1668 | try std.testing.expect(std.mem.indexOf( |
| 1893 | u8, | 1669 | u8, |
| 1894 | cl.recv_buf[0..cl.recv_len], | 1670 | cl.cl.in.items, |
| 1895 | "REFUSED-FULL", | 1671 | "REFUSED-FULL", |
| 1896 | ) != null); | 1672 | ) != null); |
| 1897 | // ...and the connection really did go. | 1673 | // ...and the connection really did go. |
| @@ -1950,21 +1726,18 @@ test "Listener: closing a connection from inside a receive callback is deferred" | |||
| 1950 | try std.posix.getsockname(l.fd, @ptrCast(&actual), &alen); | 1726 | try std.posix.getsockname(l.fd, @ptrCast(&actual), &alen); |
| 1951 | const addr = std.net.Address.initPosix(@ptrCast(@alignCast(&actual))); | 1727 | const addr = std.net.Address.initPosix(@ptrCast(@alignCast(&actual))); |
| 1952 | 1728 | ||
| 1953 | var cl = try TestClient.init(addr, key); | 1729 | var cl = try TestPeer.init(addr, key); |
| 1954 | defer cl.deinit(); | 1730 | defer cl.deinit(); |
| 1955 | try cl.start(); | ||
| 1956 | cl.drain(); | ||
| 1957 | 1731 | ||
| 1958 | var waited: u64 = 0; | 1732 | var waited: u64 = 0; |
| 1959 | cl.out = "goodbye"; | 1733 | cl.offer("goodbye"); |
| 1960 | while (waited < 10_000 and owner.received == 0) : (waited += 5) { | 1734 | while (waited < 10_000 and owner.received == 0) : (waited += 5) { |
| 1961 | var fds = [_]std.posix.pollfd{ | 1735 | var fds = [_]std.posix.pollfd{ |
| 1962 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1736 | .{ .fd = l.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1963 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 1737 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 1964 | }; | 1738 | }; |
| 1965 | _ = std.posix.poll(&fds, 5) catch break; | 1739 | _ = std.posix.poll(&fds, 5) catch break; |
| 1966 | if (fds[0].revents != 0) l.readable(); | 1740 | if (fds[0].revents != 0) l.readable(); |
| 1967 | if (fds[1].revents != 0) cl.readable(); | ||
| 1968 | l.tick(); | 1741 | l.tick(); |
| 1969 | cl.drain(); | 1742 | cl.drain(); |
| 1970 | for (l.conns) |slot| { | 1743 | for (l.conns) |slot| { |
| @@ -2032,14 +1805,12 @@ test "initFromFd: an adopted bound fd serves a handshake" { | |||
| 2032 | 1805 | ||
| 2033 | try std.testing.expectEqual(expected_port, l.boundAddr().getPort()); | 1806 | try std.testing.expectEqual(expected_port, l.boundAddr().getPort()); |
| 2034 | 1807 | ||
| 2035 | var cl = try TestClient.init(l.boundAddr(), key); | 1808 | var cl = try TestPeer.init(l.boundAddr(), key); |
| 2036 | defer cl.deinit(); | 1809 | defer cl.deinit(); |
| 2037 | try cl.start(); | ||
| 2038 | cl.drain(); | ||
| 2039 | 1810 | ||
| 2040 | try std.testing.expect(pump(l, &cl, 5000, struct { | 1811 | try std.testing.expect(pump(l, &cl, 5000, struct { |
| 2041 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1812 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 2042 | return t.handshake_done and o.opened >= 1; | 1813 | return t.cl.handshake_done and o.opened >= 1; |
| 2043 | } | 1814 | } |
| 2044 | }.f, &owner)); | 1815 | }.f, &owner)); |
| 2045 | try std.testing.expectEqual(@as(usize, 1), owner.opened); | 1816 | try std.testing.expectEqual(@as(usize, 1), owner.opened); |
| @@ -2057,14 +1828,12 @@ test "closeAll: a connected client hears the goodbye" { | |||
| 2057 | const setup = try loopbackListener(alloc, key, &owner, 5000); | 1828 | const setup = try loopbackListener(alloc, key, &owner, 5000); |
| 2058 | defer setup.l.deinit(); | 1829 | defer setup.l.deinit(); |
| 2059 | 1830 | ||
| 2060 | var cl = try TestClient.init(setup.addr, key); | 1831 | var cl = try TestPeer.init(setup.addr, key); |
| 2061 | defer cl.deinit(); | 1832 | defer cl.deinit(); |
| 2062 | try cl.start(); | ||
| 2063 | cl.drain(); | ||
| 2064 | 1833 | ||
| 2065 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { | 1834 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { |
| 2066 | fn f(o: *EchoOwner, t: *TestClient) bool { | 1835 | fn f(o: *EchoOwner, t: *TestPeer) bool { |
| 2067 | return t.handshake_done and o.opened >= 1; | 1836 | return t.cl.handshake_done and o.opened >= 1; |
| 2068 | } | 1837 | } |
| 2069 | }.f, &owner)); | 1838 | }.f, &owner)); |
| 2070 | 1839 | ||
| @@ -2072,13 +1841,16 @@ test "closeAll: a connected client hears the goodbye" { | |||
| 2072 | // packet leaves before the caller proceeds to exec. | 1841 | // packet leaves before the caller proceeds to exec. |
| 2073 | setup.l.closeAll(); | 1842 | setup.l.closeAll(); |
| 2074 | 1843 | ||
| 2075 | // The oracle is the client's observed close, not bytes the server queued. | 1844 | // The oracle is the client's observed close, not bytes the server queued |
| 1845 | // — and specifically the draining period, which only a CONNECTION_CLOSE | ||
| 1846 | // it actually read can enter. `dead` would also be set by any other read | ||
| 1847 | // failure, and would pass for the wrong reason. | ||
| 2076 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { | 1848 | try std.testing.expect(pump(setup.l, &cl, 5000, struct { |
| 2077 | fn f(_: *EchoOwner, t: *TestClient) bool { | 1849 | fn f(_: *EchoOwner, t: *TestPeer) bool { |
| 2078 | return t.draining; | 1850 | return c.ngtcp2_conn_in_draining_period(t.cl.conn.?) != 0; |
| 2079 | } | 1851 | } |
| 2080 | }.f, &owner)); | 1852 | }.f, &owner)); |
| 2081 | try std.testing.expect(cl.draining); | 1853 | try std.testing.expect(c.ngtcp2_conn_in_draining_period(cl.cl.conn.?) != 0); |
| 2082 | } | 1854 | } |
| 2083 | 1855 | ||
| 2084 | // Plain, not recursive: this module reaches the QUIC stack's @cImport, and | 1856 | // Plain, not recursive: this module reaches the QUIC stack's @cImport, and |
src/server/server_test_agent.zig
| Old | New | ||
|---|---|---|---|
| @@ -711,10 +711,8 @@ test "Server: a QUIC client's agent channels die with the client" { | |||
| 711 | const q = try quicTestServer(&srv, key); | 711 | const q = try quicTestServer(&srv, key); |
| 712 | defer q.l.deinit(); | 712 | defer q.l.deinit(); |
| 713 | 713 | ||
| 714 | var cl = try quic_server.TestClient.init(q.addr, key); | 714 | var cl = try quic_server.TestPeer.init(q.addr, key); |
| 715 | defer cl.deinit(); | 715 | defer cl.deinit(); |
| 716 | try cl.start(); | ||
| 717 | cl.drain(); | ||
| 718 | 716 | ||
| 719 | // attachOver cannot say "offering", and the offer rides its own frame | 717 | // attachOver cannot say "offering", and the offer rides its own frame |
| 720 | // re-sent after every attach, so both go out in one flight here. | 718 | // re-sent after every attach, so both go out in one flight here. |
| @@ -725,7 +723,7 @@ test "Server: a QUIC client's agent channels die with the client" { | |||
| 725 | cl.out = cbuf.items; | 723 | cl.out = cbuf.items; |
| 726 | cl.drain(); | 724 | cl.drain(); |
| 727 | 725 | ||
| 728 | var only = [_]*quic_server.TestClient{&cl}; | 726 | var only = [_]*quic_server.TestPeer{&cl}; |
| 729 | try quicPump(&srv, &only, 10000, &srv, struct { | 727 | try quicPump(&srv, &only, 10000, &srv, struct { |
| 730 | fn f(s: *Server) bool { | 728 | fn f(s: *Server) bool { |
| 731 | return s.clients[0] != null and s.clients[0].?.agent_offer; | 729 | return s.clients[0] != null and s.clients[0].?.agent_offer; |
| @@ -737,8 +735,8 @@ test "Server: a QUIC client's agent channels die with the client" { | |||
| 737 | const agent = try std.net.connectUnixSocket(path); | 735 | const agent = try std.net.connectUnixSocket(path); |
| 738 | defer agent.close(); | 736 | defer agent.close(); |
| 739 | try quicPump(&srv, &only, 10000, &cl, struct { | 737 | try quicPump(&srv, &only, 10000, &cl, struct { |
| 740 | fn f(t: *quic_server.TestClient) bool { | 738 | fn f(t: *quic_server.TestPeer) bool { |
| 741 | return findFrame(t.recv_buf[0..t.recv_len], .agent_open) != null; | 739 | return findFrame(t.cl.in.items, .agent_open) != null; |
| 742 | } | 740 | } |
| 743 | }.f); | 741 | }.f); |
| 744 | if (srv.agents.chans[0] == null) return error.NoAgentOpen; | 742 | if (srv.agents.chans[0] == null) return error.NoAgentOpen; |
src/server/server_test_harness.zig
| Old | New | ||
|---|---|---|---|
| @@ -108,7 +108,7 @@ pub fn quicTestServer(srv: *Server, key: quic.Key) !struct { l: *quic_server.Lis | |||
| 108 | /// is the integration under test. | 108 | /// is the integration under test. |
| 109 | pub fn quicPump( | 109 | pub fn quicPump( |
| 110 | srv: *Server, | 110 | srv: *Server, |
| 111 | clients: []*quic_server.TestClient, | 111 | clients: []*quic_server.TestPeer, |
| 112 | budget_ms: u64, | 112 | budget_ms: u64, |
| 113 | ctx: anytype, | 113 | ctx: anytype, |
| 114 | done: *const fn (@TypeOf(ctx)) bool, | 114 | done: *const fn (@TypeOf(ctx)) bool, |
| @@ -118,9 +118,9 @@ pub fn quicPump( | |||
| 118 | if (done(ctx)) return; | 118 | if (done(ctx)) return; |
| 119 | try srv.pumpOnce(5); | 119 | try srv.pumpOnce(5); |
| 120 | for (clients) |cl| { | 120 | for (clients) |cl| { |
| 121 | var pfd = [_]std.posix.pollfd{.{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }}; | ||
| 122 | _ = std.posix.poll(&pfd, 1) catch 0; | ||
| 121 | cl.drain(); | 123 | cl.drain(); |
| 122 | var pfd = [_]std.posix.pollfd{.{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }}; | ||
| 123 | if ((std.posix.poll(&pfd, 1) catch 0) > 0) cl.readable(); | ||
| 124 | } | 124 | } |
| 125 | waited += 6; | 125 | waited += 6; |
| 126 | } | 126 | } |
src/server/server_test_quic.zig
| Old | New | ||
|---|---|---|---|
| @@ -13,7 +13,7 @@ const findFrame = h.findFrame; | |||
| 13 | const quicPump = h.quicPump; | 13 | const quicPump = h.quicPump; |
| 14 | const quicTestServer = h.quicTestServer; | 14 | const quicTestServer = h.quicTestServer; |
| 15 | 15 | ||
| 16 | fn attachOver(cl: *quic_server.TestClient, buf: *std.ArrayList(u8), alloc: std.mem.Allocator) !void { | 16 | fn attachOver(cl: *quic_server.TestPeer, buf: *std.ArrayList(u8), alloc: std.mem.Allocator) !void { |
| 17 | try proto.appendFrame(buf, alloc, .attach, &proto.encodeAttach(80, 24, 0, 0)); | 17 | try proto.appendFrame(buf, alloc, .attach, &proto.encodeAttach(80, 24, 0, 0)); |
| 18 | cl.out = buf.items; | 18 | cl.out = buf.items; |
| 19 | cl.drain(); | 19 | cl.drain(); |
| @@ -105,22 +105,20 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea | |||
| 105 | const q = try quicTestServer(&srv, key); | 105 | const q = try quicTestServer(&srv, key); |
| 106 | defer q.l.deinit(); | 106 | defer q.l.deinit(); |
| 107 | 107 | ||
| 108 | var cl = try quic_server.TestClient.init(q.addr, key); | 108 | var cl = try quic_server.TestPeer.init(q.addr, key); |
| 109 | defer cl.deinit(); | 109 | defer cl.deinit(); |
| 110 | try cl.start(); | ||
| 111 | cl.drain(); | ||
| 112 | 110 | ||
| 113 | var out: std.ArrayList(u8) = .empty; | 111 | var out: std.ArrayList(u8) = .empty; |
| 114 | defer out.deinit(alloc); | 112 | defer out.deinit(alloc); |
| 115 | try attachOver(&cl, &out, alloc); | 113 | try attachOver(&cl, &out, alloc); |
| 116 | 114 | ||
| 117 | var only = [_]*quic_server.TestClient{&cl}; | 115 | var only = [_]*quic_server.TestPeer{&cl}; |
| 118 | try quicPump(&srv, &only, 8000, &cl, struct { | 116 | try quicPump(&srv, &only, 8000, &cl, struct { |
| 119 | fn f(t: *quic_server.TestClient) bool { | 117 | fn f(t: *quic_server.TestPeer) bool { |
| 120 | return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; | 118 | return findFrame(t.cl.in.items, .snapshot) != null; |
| 121 | } | 119 | } |
| 122 | }.f); | 120 | }.f); |
| 123 | try std.testing.expect(findFrame(cl.recv_buf[0..cl.recv_len], .snapshot) != null); | 121 | try std.testing.expect(findFrame(cl.cl.in.items, .snapshot) != null); |
| 124 | 122 | ||
| 125 | // From here the client says NOTHING. That is the whole point: the | 123 | // From here the client says NOTHING. That is the whole point: the |
| 126 | // listener's send only queues, and the two drains that fire on their own | 124 | // listener's send only queues, and the two drains that fire on their own |
| @@ -139,13 +137,13 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea | |||
| 139 | try srv.pumpOnce(5); | 137 | try srv.pumpOnce(5); |
| 140 | cl.drain(); | 138 | cl.drain(); |
| 141 | var pfd = [_]std.posix.pollfd{ | 139 | var pfd = [_]std.posix.pollfd{ |
| 142 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 140 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 143 | }; | 141 | }; |
| 144 | if ((std.posix.poll(&pfd, 1) catch 0) > 0) cl.readable(); | 142 | if ((std.posix.poll(&pfd, 1) catch 0) > 0) cl.cl.readable(); |
| 145 | } | 143 | } |
| 146 | try std.testing.expect(q.l.timeoutMs(1000) > 100); | 144 | try std.testing.expect(q.l.timeoutMs(1000) > 100); |
| 147 | 145 | ||
| 148 | const before = cl.recv_len; | 146 | const before = cl.echoed(); |
| 149 | cl.out = &.{}; | 147 | cl.out = &.{}; |
| 150 | srv.sessions.table[0].?.eng.feed("quiet-client-marker\r\n"); | 148 | srv.sessions.table[0].?.eng.feed("quiet-client-marker\r\n"); |
| 151 | srv.sendUpdate(0); | 149 | srv.sendUpdate(0); |
| @@ -168,15 +166,15 @@ test "Server: output reaches a silent QUIC client without waiting for it to spea | |||
| 168 | // Reading is not transmitting: the client takes whatever already | 166 | // Reading is not transmitting: the client takes whatever already |
| 169 | // arrived, and never gives the server an inbound packet to react to. | 167 | // arrived, and never gives the server an inbound packet to react to. |
| 170 | var tries: usize = 0; | 168 | var tries: usize = 0; |
| 171 | while (tries < 60 and findFrame(cl.recv_buf[0..cl.recv_len], .delta) == null) : (tries += 1) { | 169 | while (tries < 60 and findFrame(cl.cl.in.items, .delta) == null) : (tries += 1) { |
| 172 | var pfd = [_]std.posix.pollfd{ | 170 | var pfd = [_]std.posix.pollfd{ |
| 173 | .{ .fd = cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 171 | .{ .fd = cl.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 174 | }; | 172 | }; |
| 175 | if ((std.posix.poll(&pfd, 2) catch 0) > 0) cl.readable(); | 173 | if ((std.posix.poll(&pfd, 2) catch 0) > 0) cl.cl.readable(); |
| 176 | } | 174 | } |
| 177 | 175 | ||
| 178 | try std.testing.expect(cl.recv_len > before); | 176 | try std.testing.expect(cl.echoed() > before); |
| 179 | try std.testing.expect(findFrame(cl.recv_buf[0..cl.recv_len], .delta) != null); | 177 | try std.testing.expect(findFrame(cl.cl.in.items, .delta) != null); |
| 180 | } | 178 | } |
| 181 | 179 | ||
| 182 | test "Server: a QUIC client still receives the shell's exit status" { | 180 | test "Server: a QUIC client still receives the shell's exit status" { |
| @@ -193,10 +191,8 @@ test "Server: a QUIC client still receives the shell's exit status" { | |||
| 193 | const q = try quicTestServer(&srv, key); | 191 | const q = try quicTestServer(&srv, key); |
| 194 | defer q.l.deinit(); | 192 | defer q.l.deinit(); |
| 195 | 193 | ||
| 196 | var cl = try quic_server.TestClient.init(q.addr, key); | 194 | var cl = try quic_server.TestPeer.init(q.addr, key); |
| 197 | defer cl.deinit(); | 195 | defer cl.deinit(); |
| 198 | try cl.start(); | ||
| 199 | cl.drain(); | ||
| 200 | 196 | ||
| 201 | var out: std.ArrayList(u8) = .empty; | 197 | var out: std.ArrayList(u8) = .empty; |
| 202 | defer out.deinit(alloc); | 198 | defer out.deinit(alloc); |
| @@ -204,13 +200,13 @@ test "Server: a QUIC client still receives the shell's exit status" { | |||
| 204 | 200 | ||
| 205 | // Attached and carrying state: the frame path works before we start | 201 | // Attached and carrying state: the frame path works before we start |
| 206 | // asking about the harder one. | 202 | // asking about the harder one. |
| 207 | var only = [_]*quic_server.TestClient{&cl}; | 203 | var only = [_]*quic_server.TestPeer{&cl}; |
| 208 | try quicPump(&srv, &only, 8000, &cl, struct { | 204 | try quicPump(&srv, &only, 8000, &cl, struct { |
| 209 | fn f(t: *quic_server.TestClient) bool { | 205 | fn f(t: *quic_server.TestPeer) bool { |
| 210 | return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; | 206 | return findFrame(t.cl.in.items, .snapshot) != null; |
| 211 | } | 207 | } |
| 212 | }.f); | 208 | }.f); |
| 213 | try std.testing.expect(findFrame(cl.recv_buf[0..cl.recv_len], .snapshot) != null); | 209 | try std.testing.expect(findFrame(cl.cl.in.items, .snapshot) != null); |
| 214 | 210 | ||
| 215 | // Now the case drainPending gets wrong when a QUIC slot contributes a | 211 | // Now the case drainPending gets wrong when a QUIC slot contributes a |
| 216 | // -1 descriptor: the shell exits, the exit_status is queued, and the | 212 | // -1 descriptor: the shell exits, the exit_status is queued, and the |
| @@ -220,12 +216,12 @@ test "Server: a QUIC client still receives the shell's exit status" { | |||
| 220 | cl.drain(); | 216 | cl.drain(); |
| 221 | 217 | ||
| 222 | try quicPump(&srv, &only, 15000, &cl, struct { | 218 | try quicPump(&srv, &only, 15000, &cl, struct { |
| 223 | fn f(t: *quic_server.TestClient) bool { | 219 | fn f(t: *quic_server.TestPeer) bool { |
| 224 | return findFrame(t.recv_buf[0..t.recv_len], .exit_status) != null; | 220 | return findFrame(t.cl.in.items, .exit_status) != null; |
| 225 | } | 221 | } |
| 226 | }.f); | 222 | }.f); |
| 227 | 223 | ||
| 228 | const status = findFrame(cl.recv_buf[0..cl.recv_len], .exit_status); | 224 | const status = findFrame(cl.cl.in.items, .exit_status); |
| 229 | try std.testing.expect(status != null); | 225 | try std.testing.expect(status != null); |
| 230 | try std.testing.expectEqual(@as(usize, 1), status.?.len); | 226 | try std.testing.expectEqual(@as(usize, 1), status.?.len); |
| 231 | try std.testing.expectEqual(@as(u8, 7), status.?[0]); | 227 | try std.testing.expectEqual(@as(u8, 7), status.?[0]); |
| @@ -245,14 +241,10 @@ test "Server: one QUIC client leaving does not disturb the other" { | |||
| 245 | const q = try quicTestServer(&srv, key); | 241 | const q = try quicTestServer(&srv, key); |
| 246 | defer q.l.deinit(); | 242 | defer q.l.deinit(); |
| 247 | 243 | ||
| 248 | var a = try quic_server.TestClient.init(q.addr, key); | 244 | var a = try quic_server.TestPeer.init(q.addr, key); |
| 249 | defer a.deinit(); | 245 | defer a.deinit(); |
| 250 | try a.start(); | 246 | var b = try quic_server.TestPeer.init(q.addr, key); |
| 251 | a.drain(); | ||
| 252 | var b = try quic_server.TestClient.init(q.addr, key); | ||
| 253 | defer b.deinit(); | 247 | defer b.deinit(); |
| 254 | try b.start(); | ||
| 255 | b.drain(); | ||
| 256 | 248 | ||
| 257 | var abuf: std.ArrayList(u8) = .empty; | 249 | var abuf: std.ArrayList(u8) = .empty; |
| 258 | defer abuf.deinit(alloc); | 250 | defer abuf.deinit(alloc); |
| @@ -261,14 +253,14 @@ test "Server: one QUIC client leaving does not disturb the other" { | |||
| 261 | try attachOver(&a, &abuf, alloc); | 253 | try attachOver(&a, &abuf, alloc); |
| 262 | try attachOver(&b, &bbuf, alloc); | 254 | try attachOver(&b, &bbuf, alloc); |
| 263 | 255 | ||
| 264 | var both = [_]*quic_server.TestClient{ &a, &b }; | 256 | var both = [_]*quic_server.TestPeer{ &a, &b }; |
| 265 | try quicPump(&srv, &both, 10000, &b, struct { | 257 | try quicPump(&srv, &both, 10000, &b, struct { |
| 266 | fn f(t: *quic_server.TestClient) bool { | 258 | fn f(t: *quic_server.TestPeer) bool { |
| 267 | return findFrame(t.recv_buf[0..t.recv_len], .snapshot) != null; | 259 | return findFrame(t.cl.in.items, .snapshot) != null; |
| 268 | } | 260 | } |
| 269 | }.f); | 261 | }.f); |
| 270 | try std.testing.expect(findFrame(a.recv_buf[0..a.recv_len], .snapshot) != null); | 262 | try std.testing.expect(findFrame(a.cl.in.items, .snapshot) != null); |
| 271 | try std.testing.expect(findFrame(b.recv_buf[0..b.recv_len], .snapshot) != null); | 263 | try std.testing.expect(findFrame(b.cl.in.items, .snapshot) != null); |
| 272 | 264 | ||
| 273 | // A leaves. Sink.close() must tear down A's connection and NOT the UDP | 265 | // A leaves. Sink.close() must tear down A's connection and NOT the UDP |
| 274 | // socket every other client is reached through — for a socket client | 266 | // socket every other client is reached through — for a socket client |
| @@ -287,18 +279,18 @@ test "Server: one QUIC client leaving does not disturb the other" { | |||
| 287 | }.f); | 279 | }.f); |
| 288 | 280 | ||
| 289 | // B is still served: a marker typed now must come back to it. | 281 | // B is still served: a marker typed now must come back to it. |
| 290 | const before = b.recv_len; | 282 | const before = b.echoed(); |
| 291 | try proto.appendFrame(&bbuf, alloc, .input, "echo quic-two-ok\n"); | 283 | try proto.appendFrame(&bbuf, alloc, .input, "echo quic-two-ok\n"); |
| 292 | b.out = bbuf.items; | 284 | b.out = bbuf.items; |
| 293 | b.drain(); | 285 | b.drain(); |
| 294 | var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); | 286 | var replica = try Engine.init(alloc, .{ .cols = 80, .rows = 24 }); |
| 295 | defer replica.deinit(); | 287 | defer replica.deinit(); |
| 296 | try quicPump(&srv, &both, 15000, &b, struct { | 288 | try quicPump(&srv, &both, 15000, &b, struct { |
| 297 | fn f(t: *quic_server.TestClient) bool { | 289 | fn f(t: *quic_server.TestPeer) bool { |
| 298 | return t.recv_len > 0 and findFrame(t.recv_buf[0..t.recv_len], .delta) != null; | 290 | return t.echoed() > 0 and findFrame(t.cl.in.items, .delta) != null; |
| 299 | } | 291 | } |
| 300 | }.f); | 292 | }.f); |
| 301 | try std.testing.expect(b.recv_len > before); | 293 | try std.testing.expect(b.echoed() > before); |
| 302 | } | 294 | } |
| 303 | 295 | ||
| 304 | test "Server: a QUIC client that stops reading is dropped by the cap, not tolerated" { | 296 | test "Server: a QUIC client that stops reading is dropped by the cap, not tolerated" { |
| @@ -315,18 +307,17 @@ test "Server: a QUIC client that stops reading is dropped by the cap, not tolera | |||
| 315 | defer q.l.deinit(); | 307 | defer q.l.deinit(); |
| 316 | defer srv.deinit(); | 308 | defer srv.deinit(); |
| 317 | 309 | ||
| 318 | var cl = try quic_server.TestClient.init(q.addr, key); | 310 | var cl = try quic_server.TestPeer.init(q.addr, key); |
| 319 | defer cl.deinit(); | 311 | defer cl.deinit(); |
| 320 | try cl.start(); | ||
| 321 | 312 | ||
| 322 | var bbuf: std.ArrayList(u8) = .empty; | 313 | var bbuf: std.ArrayList(u8) = .empty; |
| 323 | defer bbuf.deinit(alloc); | 314 | defer bbuf.deinit(alloc); |
| 324 | try attachOver(&cl, &bbuf, alloc); | 315 | try attachOver(&cl, &bbuf, alloc); |
| 325 | 316 | ||
| 326 | var only = [_]*quic_server.TestClient{&cl}; | 317 | var only = [_]*quic_server.TestPeer{&cl}; |
| 327 | try quicPump(&srv, &only, 8000, &cl, struct { | 318 | try quicPump(&srv, &only, 8000, &cl, struct { |
| 328 | fn f(t: *quic_server.TestClient) bool { | 319 | fn f(t: *quic_server.TestPeer) bool { |
| 329 | return t.handshake_done and t.echoed > 0; | 320 | return t.cl.handshake_done and t.echoed() > 0; |
| 330 | } | 321 | } |
| 331 | }.f); | 322 | }.f); |
| 332 | try std.testing.expect(srv.clients[0] != null); | 323 | try std.testing.expect(srv.clients[0] != null); |
| @@ -371,22 +362,21 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" { | |||
| 371 | defer q.l.deinit(); | 362 | defer q.l.deinit(); |
| 372 | defer srv.deinit(); | 363 | defer srv.deinit(); |
| 373 | 364 | ||
| 374 | var cl = try quic_server.TestClient.init(q.addr, key); | 365 | var cl = try quic_server.TestPeer.init(q.addr, key); |
| 375 | defer cl.deinit(); | 366 | defer cl.deinit(); |
| 376 | try cl.start(); | ||
| 377 | 367 | ||
| 378 | var bbuf: std.ArrayList(u8) = .empty; | 368 | var bbuf: std.ArrayList(u8) = .empty; |
| 379 | defer bbuf.deinit(alloc); | 369 | defer bbuf.deinit(alloc); |
| 380 | try attachOver(&cl, &bbuf, alloc); | 370 | try attachOver(&cl, &bbuf, alloc); |
| 381 | 371 | ||
| 382 | var only = [_]*quic_server.TestClient{&cl}; | 372 | var only = [_]*quic_server.TestPeer{&cl}; |
| 383 | try quicPump(&srv, &only, 8000, &cl, struct { | 373 | try quicPump(&srv, &only, 8000, &cl, struct { |
| 384 | fn f(t: *quic_server.TestClient) bool { | 374 | fn f(t: *quic_server.TestPeer) bool { |
| 385 | return t.handshake_done and t.echoed > 0; | 375 | return t.cl.handshake_done and t.echoed() > 0; |
| 386 | } | 376 | } |
| 387 | }.f); | 377 | }.f); |
| 388 | try std.testing.expect(srv.clients[0] != null); | 378 | try std.testing.expect(srv.clients[0] != null); |
| 389 | const before = cl.echoed; | 379 | const before = cl.echoed(); |
| 390 | 380 | ||
| 391 | // Queue more than one ring's worth in one go, so the drain cannot | 381 | // Queue more than one ring's worth in one go, so the drain cannot |
| 392 | // possibly finish by handing everything over once: some of it is still | 382 | // possibly finish by handing everything over once: some of it is still |
| @@ -406,14 +396,14 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" { | |||
| 406 | var done = false; | 396 | var done = false; |
| 407 | const t0 = std.time.milliTimestamp(); | 397 | const t0 = std.time.milliTimestamp(); |
| 408 | const th = try std.Thread.spawn(.{}, struct { | 398 | const th = try std.Thread.spawn(.{}, struct { |
| 409 | fn f(client: *quic_server.TestClient, flag: *bool) void { | 399 | fn f(client: *quic_server.TestPeer, flag: *bool) void { |
| 410 | // The peer: reads and acknowledges until the daemon says stop. | 400 | // The peer: reads and acknowledges until the daemon says stop. |
| 411 | while (!flag.*) { | 401 | while (!flag.*) { |
| 412 | client.drain(); | 402 | client.drain(); |
| 413 | var pfd = [_]std.posix.pollfd{ | 403 | var pfd = [_]std.posix.pollfd{ |
| 414 | .{ .fd = client.fd, .events = std.posix.POLL.IN, .revents = 0 }, | 404 | .{ .fd = client.cl.fd, .events = std.posix.POLL.IN, .revents = 0 }, |
| 415 | }; | 405 | }; |
| 416 | if ((std.posix.poll(&pfd, 5) catch 0) > 0) client.readable(); | 406 | if ((std.posix.poll(&pfd, 5) catch 0) > 0) client.cl.readable(); |
| 417 | } | 407 | } |
| 418 | } | 408 | } |
| 419 | }.f, .{ &cl, &done }); | 409 | }.f, .{ &cl, &done }); |
| @@ -425,6 +415,6 @@ test "Server: drainPending waits for a QUIC client's acks, not just its queue" { | |||
| 425 | // Everything owed actually left, and it left inside the budget. | 415 | // Everything owed actually left, and it left inside the budget. |
| 426 | try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.pending.items.len); | 416 | try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.pending.items.len); |
| 427 | try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.sink.inFlight()); | 417 | try std.testing.expectEqual(@as(usize, 0), srv.clients[0].?.sink.inFlight()); |
| 428 | try std.testing.expect(cl.echoed >= before + payload.len); | 418 | try std.testing.expect(cl.echoed() >= before + payload.len); |
| 429 | try std.testing.expect(elapsed < 15_000); | 419 | try std.testing.expect(elapsed < 15_000); |
| 430 | } | 420 | } |