@amritk/nish-x86_64-linux 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +215 -18
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +4 -4
  14. package/runtime/runtime-wasm.c +5 -3
  15. package/runtime/runtime.c +69 -13
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +223 -22
  18. package/std/README.md +81 -51
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +932 -61
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1120 -34
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +211 -31
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-listener.ts +88 -7
  45. package/std/net/quic-packet.ts +243 -82
  46. package/std/net/quic-recovery.ts +361 -30
  47. package/std/net/quic-stream.ts +424 -24
  48. package/std/net/quic.ts +519 -167
  49. package/std/net/router.ts +676 -0
  50. package/std/net/tls/codec.ts +426 -27
  51. package/std/net/tls/record-server.ts +478 -132
  52. package/std/net/tls/record.ts +133 -42
  53. package/std/net/tls/schedule.ts +464 -55
  54. package/std/net/tls/ticket.ts +610 -0
  55. package/std/net/tls-tcp.ts +196 -53
  56. package/std/net/tls.ts +1018 -260
  57. package/std/net/url.ts +571 -0
  58. package/std/random.ts +94 -0
  59. package/std/threads.ts +438 -14
@@ -0,0 +1,412 @@
1
+ /**
2
+ * `nish/net/prefork` — run a server on every core: a supervisor process that
3
+ * starts one worker process per core, restarts one that dies, and stops them
4
+ * all on SIGTERM or SIGINT (`docs/wp42-multicore-server.md` §2.3).
5
+ *
6
+ * import { preforkWorkers } from "nish/net/prefork";
7
+ *
8
+ * export const main = (): number => {
9
+ * const worker: i32 = preforkWorkers(process.argv);
10
+ * if (worker < 0) { return worker === -1 ? 0 : 1; } // the supervisor, done
11
+ * const listener = tcpListenFlags("0.0.0.0", 8080, 1024, 1); // SO_REUSEPORT
12
+ * // ... the single-threaded readiness loop, unchanged ...
13
+ * };
14
+ *
15
+ * **One program, two roles.** `preforkWorkers(process.argv)` answers in two kinds of
16
+ * process. In a worker it answers the worker's index, `0` to `N - 1`, at once,
17
+ * and the program goes on to bind its listener and serve. In the process the
18
+ * user started it supervises, and answers only once every worker has exited:
19
+ * `-1` after a shutdown, `-2` when the workers could not be started or the
20
+ * supervisor could not listen for signals. Each worker binds its own listener
21
+ * with `tcpListenFlags`' flag 1 (`SO_REUSEPORT`), so on Linux the kernel
22
+ * spreads new connections across them and no state is shared:
23
+ * a worker is the single-threaded server it was before, `Arena.used()` flat
24
+ * per worker.
25
+ *
26
+ * **How a worker is told.** The supervisor runs the program again with
27
+ * `--nish-prefork-worker=<index>` appended to its arguments, through `spawn`;
28
+ * on Linux it runs `/proc/self/exe`, so a program started through `PATH` or a
29
+ * relative path finds itself. `preforkArgs(process.argv)` answers the
30
+ * arguments without that flag, for a worker that reads its own; the module
31
+ * takes `process.argv` from its caller because only a program with a `main`
32
+ * has one. The environment is inherited whole, and every descriptor the
33
+ * supervisor holds is close-on-exec, so a worker starts with none of them
34
+ * (`docs/security/runtime.md`, RT-17, RT-19).
35
+ *
36
+ * **How many.** `NISH_WORKERS` when it is a positive whole number, else
37
+ * `PreforkConfig.workers` when that is positive, else
38
+ * `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
39
+ * `SO_REUSEPORT` port without spreading connections across it. The count has
40
+ * no upper bound, so `NISH_WORKERS` is the operator's to set, not a client's.
41
+ *
42
+ * **Restarts.** A worker that exits, by a signal or with any code, is started
43
+ * again at the same index while the supervisor is not stopping. One that dies
44
+ * within `quickExitMs` of starting waits before its restart: `backoffMs` the
45
+ * first time, twice the last wait after that, up to `backoffCapMs`; one that
46
+ * ran longer starts again at once and clears its wait, so a crash loop costs a
47
+ * spawn every `backoffCapMs`, not a core. A restart `spawn` refuses waits on
48
+ * the same backoff and is tried again; only the first start of a worker that
49
+ * fails ends the supervisor, with `PREFORK_SPAWN_FAILED`.
50
+ *
51
+ * **Shutdown.** The supervisor listens through `signalFd()`, made before the
52
+ * first spawn so no signal falls between. On SIGTERM or SIGINT it stops
53
+ * restarting, sends every worker SIGTERM, reaps them for `graceMs`, sends what
54
+ * is left SIGKILL and reaps that. A second signal during the grace period sends
55
+ * SIGKILL at once. The workers get the default dispositions back from `exec`,
56
+ * so a worker that never asked for `signalFd()` ends on the SIGTERM. A
57
+ * supervisor that dies any other way (SIGKILL, the OOM killer, or the default
58
+ * action of SIGHUP or SIGQUIT, which `signalFd()` does not catch) leaves its
59
+ * workers running, reparented to init.
60
+ *
61
+ * Private helpers share the importing program's flat symbol namespace
62
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
63
+ */
64
+ import { netClose, pollAdd, pollCreate, pollWait } from "nish:net"
65
+ import {
66
+ availableParallelism,
67
+ getenv,
68
+ kill,
69
+ monotonicNanos,
70
+ platform,
71
+ readSignal,
72
+ signalFd,
73
+ spawn,
74
+ waitPid,
75
+ } from "nish:process"
76
+
77
+ /** The argument a worker is started with, its index after the `=`. */
78
+ export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
79
+
80
+ /** What `preforkWith` answers in the supervisor once its workers are gone. */
81
+ export const PREFORK_STOPPED: i32 = -1
82
+ /**
83
+ * What `preforkWith` answers when a worker could not be started, or when the
84
+ * supervisor could not listen for SIGTERM and so starts none.
85
+ */
86
+ export const PREFORK_SPAWN_FAILED: i32 = -2
87
+
88
+ /** The supervisor's numbers; every one is checked by `preforkWith`. */
89
+ export class PreforkConfig {
90
+ /** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
91
+ workers: i32 = 0
92
+ /** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
93
+ tickMs: i32 = 100
94
+ /** The first wait before restarting a worker that died young. */
95
+ backoffMs: i32 = 100
96
+ /** The longest wait before a restart. */
97
+ backoffCapMs: i32 = 5000
98
+ /** A worker that exits within this many milliseconds of starting died young. */
99
+ quickExitMs: i32 = 1000
100
+ /** How long the workers get to exit after SIGTERM before SIGKILL. */
101
+ graceMs: i32 = 5000
102
+ }
103
+
104
+ /** True when `s` is one or more ASCII digits and nothing else. */
105
+ const preforkDigits = (s: string): boolean => {
106
+ if (s.length === 0 || s.length > 9) {
107
+ return false
108
+ }
109
+ for (let i: i32 = 0; i < toI32(s.length); i++) {
110
+ const c: i32 = toI32(s.charCodeAt(i))
111
+ if (c < 48 || c > 57) {
112
+ return false
113
+ }
114
+ }
115
+ return true
116
+ }
117
+
118
+ /**
119
+ * How many workers to start: `env` (`NISH_WORKERS`, `null` when unset) when it
120
+ * is a positive whole number, else `requested` when positive, else the
121
+ * platform's default.
122
+ */
123
+ export const preforkWorkerCount = (requested: i32, env: string | null): i32 => {
124
+ if (env !== null && preforkDigits(env)) {
125
+ const n: i32 = parseInt(env)
126
+ if (n > 0) {
127
+ return n
128
+ }
129
+ }
130
+ if (requested > 0) {
131
+ return requested
132
+ }
133
+ if (platform !== "linux") {
134
+ return 1
135
+ }
136
+ const cores: i32 = availableParallelism()
137
+ return cores > 0 ? cores : 1
138
+ }
139
+
140
+ /** The worker index `args` carries in its last element, or -1 when none. */
141
+ export const preforkIndex = (args: string[]): i32 => {
142
+ if (args.length < 2) {
143
+ return -1
144
+ }
145
+ const last: string = args[args.length - 1]
146
+ if (!last.startsWith(PREFORK_WORKER_FLAG)) {
147
+ return -1
148
+ }
149
+ const digits: string = last.substring(PREFORK_WORKER_FLAG.length)
150
+ return preforkDigits(digits) ? parseInt(digits) : -1
151
+ }
152
+
153
+ /** `args` (`process.argv`) without the worker flag, the same in every process. */
154
+ export const preforkArgs = (args: string[]): string[] => {
155
+ const keep: i32 = preforkIndex(args) >= 0 ? toI32(args.length) - 1 : toI32(args.length)
156
+ const out: string[] = []
157
+ for (let i: i32 = 0; i < toI32(args.length); i++) {
158
+ if (i < keep) {
159
+ out.push(args[i])
160
+ }
161
+ }
162
+ return out
163
+ }
164
+
165
+ /** Milliseconds on the monotonic clock. */
166
+ const preforkNowMs = (): i64 => monotonicNanos() / 1000000
167
+
168
+ /** The signals the supervisor stops its workers with. */
169
+ const PREFORK_SIGTERM: i32 = 15
170
+ const PREFORK_SIGKILL: i32 = 9
171
+
172
+ /** One worker's slot in the supervisor. */
173
+ class PreforkSlot {
174
+ /** When the running worker started, or when the waiting one may start. */
175
+ at: i64 = 0
176
+ /** The running worker's pid, 0 while it waits to be started again. */
177
+ pid: i32 = 0
178
+ /** The wait before the last restart, 0 after a worker that ran long. */
179
+ delay: i32 = 0
180
+ }
181
+
182
+ /** The supervisor's state while it runs. */
183
+ class PreforkSupervisor {
184
+ config: PreforkConfig
185
+ program: string
186
+ args: string[]
187
+ slots: PreforkSlot[]
188
+ running: i32 = 0
189
+
190
+ constructor(config: PreforkConfig, args: string[], count: i32) {
191
+ this.config = config
192
+ this.args = preforkArgs(args)
193
+ const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
194
+ this.program = self !== null ? self : this.args[0]
195
+ this.slots = []
196
+ for (let i: i32 = 0; i < count; i++) {
197
+ this.slots.push(new PreforkSlot())
198
+ }
199
+ }
200
+
201
+ /** Start worker `index`; false when `spawn` refused. */
202
+ start(index: i32): boolean {
203
+ const argv: string[] = [this.program]
204
+ for (let i: i32 = 1; i < toI32(this.args.length); i++) {
205
+ argv.push(this.args[i])
206
+ }
207
+ argv.push(`${PREFORK_WORKER_FLAG}${index}`)
208
+ const pid: i32 = spawn(argv)
209
+ if (pid <= 0) {
210
+ return false
211
+ }
212
+ const slot: PreforkSlot = this.slots[index]
213
+ slot.pid = pid
214
+ slot.at = preforkNowMs()
215
+ this.running = this.running + 1
216
+ return true
217
+ }
218
+
219
+ /** The slot `pid` ran in, or -1. */
220
+ find(pid: i32): i32 {
221
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
222
+ if (this.slots[i].pid === pid) {
223
+ return i
224
+ }
225
+ }
226
+ return -1
227
+ }
228
+
229
+ /**
230
+ * Give `slot` the time it may start again: at once after a worker that ran
231
+ * at least `quickExitMs`, else, or when `failed` (`spawn` refused it), after
232
+ * a wait that starts at `backoffMs` and doubles up to `backoffCapMs`.
233
+ */
234
+ backoff(slot: PreforkSlot, now: i64, failed: boolean): void {
235
+ if (failed || now - slot.at < toI64(this.config.quickExitMs)) {
236
+ const cap: i32 = this.config.backoffCapMs
237
+ if (slot.delay === 0) {
238
+ slot.delay = this.config.backoffMs
239
+ } else {
240
+ // Compared before doubling, so a cap near the i32 limit cannot overflow.
241
+ slot.delay = slot.delay > cap / 2 ? cap : slot.delay * 2
242
+ }
243
+ } else {
244
+ slot.delay = 0
245
+ }
246
+ slot.at = now + toI64(slot.delay)
247
+ }
248
+
249
+ /**
250
+ * Reap every worker that has exited. Unless `stopping`, each one's slot is
251
+ * given the time it may start again. A child that is not a worker (one the
252
+ * program spawned itself before calling `preforkWith`) is reaped too, and
253
+ * its status is discarded. Answers false when `waitPid` fails, which means
254
+ * no child is left to wait for.
255
+ */
256
+ reap(stopping: boolean): boolean {
257
+ while (this.running > 0) {
258
+ const done: i32[] = waitPid(-1, 1)
259
+ if (done[0] === -4) {
260
+ continue
261
+ }
262
+ if (done[0] <= 0) {
263
+ return done[0] === 0
264
+ }
265
+ const index: i32 = this.find(done[0])
266
+ if (index < 0) {
267
+ continue
268
+ }
269
+ const slot: PreforkSlot = this.slots[index]
270
+ this.running = this.running - 1
271
+ slot.pid = 0
272
+ if (!stopping) {
273
+ this.backoff(slot, preforkNowMs(), false)
274
+ }
275
+ }
276
+ return true
277
+ }
278
+
279
+ /**
280
+ * Start every waiting worker whose time has come. When `first`, answer false
281
+ * at the first `spawn` that fails; otherwise a failed start waits on the
282
+ * same backoff as a worker that exited at once, and the answer is true.
283
+ */
284
+ restart(first: boolean): boolean {
285
+ const now: i64 = preforkNowMs()
286
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
287
+ const slot: PreforkSlot = this.slots[i]
288
+ if (slot.pid === 0 && slot.at <= now && !this.start(i)) {
289
+ if (first) {
290
+ return false
291
+ }
292
+ console.error(`nish/net/prefork: cannot restart worker ${i}`)
293
+ this.backoff(slot, now, true)
294
+ }
295
+ }
296
+ return true
297
+ }
298
+
299
+ /** Send `sig` to every running worker. */
300
+ signal(sig: i32): void {
301
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
302
+ if (this.slots[i].pid > 0) {
303
+ kill(this.slots[i].pid, sig)
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * SIGTERM, reap until `graceMs` or a second signal, SIGKILL, reap until no
310
+ * worker is left. Two signals that arrive before the supervisor reads the
311
+ * first count as the second here too. Every reap goes through `reap`, so a
312
+ * child that is not a worker is skipped in both phases, and the SIGKILL
313
+ * phase ends only when the last worker is reaped or no child is left.
314
+ */
315
+ stop(loop: i32, signals: i32, ready: i32[]): void {
316
+ this.signal(PREFORK_SIGTERM)
317
+ const deadline: i64 = preforkNowMs() + toI64(this.config.graceMs)
318
+ this.reap(true)
319
+ let forced = false
320
+ while (this.running > 0 && !forced && preforkNowMs() < deadline) {
321
+ const n: i32 = pollWait(loop, ready, this.config.tickMs)
322
+ if (n > 0) {
323
+ readSignal(signals)
324
+ forced = true
325
+ }
326
+ this.reap(true)
327
+ }
328
+ if (this.running > 0) {
329
+ this.signal(PREFORK_SIGKILL)
330
+ while (this.reap(true) && this.running > 0) {
331
+ if (pollWait(loop, ready, this.config.tickMs) > 0) {
332
+ readSignal(signals)
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ /** The first field of `config` the supervisor cannot run with, or "" when none. */
340
+ const preforkInvalid = (config: PreforkConfig): string => {
341
+ if (config.workers < 0) {
342
+ return "workers below 0"
343
+ }
344
+ if (config.tickMs <= 0) {
345
+ return "tickMs not above 0"
346
+ }
347
+ if (config.backoffMs <= 0 || config.backoffCapMs < config.backoffMs) {
348
+ return "backoffMs not above 0 or above backoffCapMs"
349
+ }
350
+ if (config.quickExitMs < 0 || config.graceMs < 0) {
351
+ return "quickExitMs or graceMs below 0"
352
+ }
353
+ return ""
354
+ }
355
+
356
+ /**
357
+ * In a worker, its index. In the supervisor, run the workers until SIGTERM or
358
+ * SIGINT and answer `PREFORK_STOPPED`, or `PREFORK_SPAWN_FAILED` when a worker's
359
+ * first start failed (those already started are stopped first) or no signal
360
+ * listener could be made (no worker is started). Panics on a config
361
+ * `preforkInvalid` names a field of.
362
+ */
363
+ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
364
+ const invalid: string = preforkInvalid(config)
365
+ if (invalid !== "") {
366
+ panic(`nish/net/prefork: ${invalid}`)
367
+ }
368
+ const index: i32 = preforkIndex(args)
369
+ if (index >= 0) {
370
+ return index
371
+ }
372
+ const signals: i32 = signalFd()
373
+ const loop: i32 = pollCreate()
374
+ if (signals < 0 || loop < 0 || pollAdd(loop, signals, 1, 0) < 0) {
375
+ // `signals` is the process's one signal pipe, which every later
376
+ // `signalFd()` answers, so only the loop is closed.
377
+ console.error("nish/net/prefork: cannot listen for SIGTERM")
378
+ if (loop >= 0) {
379
+ netClose(loop)
380
+ }
381
+ return PREFORK_SPAWN_FAILED
382
+ }
383
+ const supervisor = new PreforkSupervisor(
384
+ config,
385
+ args,
386
+ preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
387
+ )
388
+ const ready: i32[] = new Array<i32>(2)
389
+ let result: i32 = PREFORK_STOPPED
390
+ if (!supervisor.restart(true)) {
391
+ console.error(`nish/net/prefork: cannot start ${supervisor.program}`)
392
+ result = PREFORK_SPAWN_FAILED
393
+ } else {
394
+ let running = true
395
+ while (running) {
396
+ const n: i32 = pollWait(loop, ready, config.tickMs)
397
+ if (n > 0) {
398
+ readSignal(signals)
399
+ running = false
400
+ } else {
401
+ supervisor.reap(false)
402
+ supervisor.restart(false)
403
+ }
404
+ }
405
+ }
406
+ supervisor.stop(loop, signals, ready)
407
+ netClose(loop)
408
+ return result
409
+ }
410
+
411
+ /** `preforkWith` with the default `PreforkConfig`. */
412
+ export const preforkWorkers = (args: string[]): i32 => preforkWith(args, new PreforkConfig())
@@ -71,6 +71,10 @@
71
71
  * while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
72
72
  * … and wake at the earlier of conn.deadline() and quicListenerPaceTime(conn, now)
73
73
  *
74
+ * A carrier that sends whole flights with `quicListenerTakeFlight`, below,
75
+ * wakes at `quicListenerFlightTime` instead, as `Http3Server` does: a flight
76
+ * waits for more credit than one datagram, so the pace time can come first.
77
+ *
74
78
  * **GSO and GRO** (`nish:net`'s `UDP_SEGMENT` and `UDP_GRO`). A carrier that
75
79
  * sends with segmentation offload takes a whole flight at once:
76
80
  * `quicListenerTakeFlight` writes as many datagrams as the pacer has credit
@@ -79,7 +83,10 @@
79
83
  * cuts back into datagrams; each is written in place, so a flight allocates
80
84
  * nothing. The flight is paced as a whole: it is as long as the pacer's
81
85
  * credit, and charged to it datagram by datagram, so a burst is never more
82
- * than the initial window however the carrier sends it. On the receiving
86
+ * than the initial window however the carrier sends it. It starts only on a
87
+ * millisecond's earning, up to four datagrams, so a fast connection's pacer,
88
+ * which earns by the microsecond, does not let it out a datagram per send;
89
+ * `quicListenerFlightTime` says when. On the receiving
83
90
  * side, `quicListenerReceiveSegments` hands a connection the datagrams of
84
91
  * one GRO receive, segment by segment, in place.
85
92
  *
@@ -95,9 +102,10 @@
95
102
  * byte for byte.
96
103
  *
97
104
  * **Secrets.** The seed lives in the listener and the static keys in the
98
- * caller's `QuicServerConfig`, for the server's lifetime, as plain bytes,
99
- * since a `Secret` may not be a field (NL2430); QUIC-5 in
100
- * `docs/security/quic.md` records it, with #430.
105
+ * `QuicServerConfig`, for the server's lifetime, as plain bytes, since a
106
+ * `Secret` may not be a field (NL2430). `release()` wipes the listener's own,
107
+ * and `Http3Server.stop` the configuration's with them; QUIC-5 in
108
+ * `docs/security/quic.md` records what is left.
101
109
  *
102
110
  * Written from RFC 9000, in this module's own structure; nothing here is
103
111
  * ported from another implementation. Private names carry the
@@ -212,12 +220,38 @@ export class QuicListenerAnswer {
212
220
  retryScid: u8[]
213
221
  kind: i32 = 0
214
222
  retried: boolean = false
223
+ room: i32 = 0
215
224
 
216
225
  constructor(room: i32) {
217
- this.reply = quicListenerRoom(room > 0 ? room : 0)
226
+ this.room = room > 0 ? room : 0
227
+ this.reply = quicListenerRoom(this.room)
218
228
  this.originalDcid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
219
229
  this.retryScid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
220
230
  }
231
+
232
+ /**
233
+ * Zeroes the last answer: `reply` across its whole room and each ID across
234
+ * its 20 bytes, so a Retry's token, a stateless reset's token and the IDs
235
+ * leave no copy beyond the arrays' lengths either. Each is left that long
236
+ * and all zero, `kind` a drop. An answer whose reply grew past its room
237
+ * keeps what lies beyond the longest length it is wiped at; one made with
238
+ * `QUIC_LISTENER_ANSWER_SIZE` never grows.
239
+ */
240
+ wipe(): void {
241
+ quicListenerWipeRoom(this.reply, this.room)
242
+ quicListenerWipeRoom(this.originalDcid, QUIC_MAX_CID_LENGTH)
243
+ quicListenerWipeRoom(this.retryScid, QUIC_MAX_CID_LENGTH)
244
+ this.kind = QUIC_LISTEN_DROP
245
+ this.retried = false
246
+ }
247
+ }
248
+
249
+ /** Grows `bytes` to at least `room` in the room it already has and zeroes all of it. */
250
+ const quicListenerWipeRoom = (bytes: u8[], room: i32): void => {
251
+ if (toI32(bytes.length) < room) {
252
+ quicListenerResize(bytes, room)
253
+ }
254
+ secureZero(bytes)
221
255
  }
222
256
 
223
257
  /** An empty array with room for `size` bytes, so pushing up to that many allocates nothing. */
@@ -365,6 +399,30 @@ export class QuicListener {
365
399
  secureZero(entropy)
366
400
  }
367
401
 
402
+ /**
403
+ * Ends the listener: wipes the seed and the generator's unused bytes, the
404
+ * tag and the HMAC and HKDF scratch, the INVALID_TOKEN close's Initial
405
+ * secrets and key slots, and `handle`'s spare answer, which holds the last
406
+ * one; `handle` and `handleWindow` then drop every datagram they are
407
+ * handed. The other methods still run, under the zeroed seed, and are the
408
+ * caller's not to call. What `handle` returned is the caller's to wipe. The
409
+ * configuration's keys are not the listener's to wipe: `Http3Server.stop`
410
+ * wipes them with the rest of the server.
411
+ */
412
+ release(): void {
413
+ this.usable = false
414
+ secureZero(this.seed)
415
+ secureZero(this.pool)
416
+ this.poolHead = QUIC_LISTENER_POOL
417
+ secureZero(this.tag)
418
+ this.kdf.wipe()
419
+ secureZero(this.initialClient)
420
+ secureZero(this.initialServer)
421
+ this.readSlot.wipe()
422
+ this.writeSlot.wipe()
423
+ this.spare.wipe()
424
+ }
425
+
368
426
  /**
369
427
  * One unpredictable value in 0 to 255: the next byte of HMAC-SHA256 under
370
428
  * the seed of a counter, a block at a time, each byte wiped once taken.
@@ -843,6 +901,24 @@ export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | nu
843
901
  export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
844
902
  now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
845
903
 
904
+ /** The most credit a paced flight waits for before it starts: four datagrams. */
905
+ export const QUIC_LISTENER_FLIGHT_QUANTUM: i32 = 4800
906
+
907
+ /**
908
+ * The credit `quicListenerTakeFlight` waits for before `conn` starts a
909
+ * flight: what its pacer earns in a millisecond, from one datagram up to
910
+ * `QUIC_LISTENER_FLIGHT_QUANTUM`.
911
+ */
912
+ const quicListenerQuantum = (conn: QuicConnection): i32 =>
913
+ conn.recovery.pacerQuantum(QUIC_CONN_DATAGRAM_SIZE, QUIC_LISTENER_FLIGHT_QUANTUM)
914
+
915
+ /**
916
+ * When `quicListenerTakeFlight` next lets `conn` start a flight, in the
917
+ * caller's milliseconds: `now` when it may already.
918
+ */
919
+ export const quicListenerFlightTime = (conn: QuicConnection, now: i64): i64 =>
920
+ now + conn.recovery.pacerDelay(now, quicListenerQuantum(conn))
921
+
846
922
  /** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
847
923
  export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
848
924
 
@@ -858,8 +934,10 @@ export class QuicFlight {
858
934
  * Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
859
935
  * datagram after datagram, for one GSO send, and answers its length in
860
936
  * bytes (0 for none); `flight` says how many datagrams it holds and the
861
- * segment size. Each datagram goes only when the pacer has credit for a full
862
- * one (RFC 9002 §7.7) and is charged at its real size, as
937
+ * segment size. A flight starts only once the pacer has the credit
938
+ * `quicListenerFlightTime` waits for, a millisecond's earning of one to four
939
+ * datagrams; then each datagram goes only when the pacer has credit for a
940
+ * full one (RFC 9002 §7.7) and is charged at its real size, as
863
941
  * `quicListenerTakePaced` does, so the flight is the pacer's whole credit
864
942
  * and no more. It stops at the first datagram shorter than
865
943
  * `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
@@ -877,6 +955,9 @@ export const quicListenerTakeFlight = (
877
955
  flight.count = 0
878
956
  flight.segment = 0
879
957
  let length: i32 = 0
958
+ if (conn.recovery.pacerDelay(now, quicListenerQuantum(conn)) > 0) {
959
+ return length
960
+ }
880
961
  while (
881
962
  flight.count < QUIC_LISTENER_FLIGHT_MAX &&
882
963
  length + QUIC_CONN_DATAGRAM_SIZE <= cap &&