@amritk/nish-x86_64-linux 0.18.0 → 0.21.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 (104) hide show
  1. package/INSTALL.md +58 -28
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +2759 -0
  4. package/package.json +3 -2
  5. package/runtime/browser.d.ts +14 -0
  6. package/runtime/browser.mjs +88 -0
  7. package/runtime/core/arrays.d.ts +18 -0
  8. package/runtime/core/arrays.mjs +125 -0
  9. package/runtime/core/checks.mjs +20 -0
  10. package/runtime/core/host.d.ts +70 -0
  11. package/runtime/core/host.mjs +311 -0
  12. package/runtime/core/numbers.d.ts +53 -0
  13. package/runtime/core/numbers.mjs +316 -0
  14. package/runtime/core/result.d.ts +41 -0
  15. package/runtime/core/result.mjs +127 -0
  16. package/runtime/core/strings.d.ts +14 -0
  17. package/runtime/core/strings.mjs +169 -0
  18. package/runtime/core.d.ts +9 -0
  19. package/runtime/core.mjs +55 -0
  20. package/runtime/host.d.ts +6 -0
  21. package/runtime/nish.d.ts +99 -5
  22. package/runtime/nish.h +289 -19
  23. package/runtime/nish.mjs +39 -3
  24. package/runtime/node.mjs +383 -0
  25. package/runtime/runtime-aes.c +490 -0
  26. package/runtime/runtime-bytes.c +401 -0
  27. package/runtime/runtime-cross.c +366 -0
  28. package/runtime/runtime-host.c +8 -7
  29. package/runtime/runtime-net.c +93 -34
  30. package/runtime/runtime-os.c +214 -16
  31. package/runtime/runtime-parallel.c +590 -107
  32. package/runtime/runtime-simd.c +235 -11
  33. package/runtime/runtime-term.c +504 -0
  34. package/runtime/runtime-wasm.c +5 -14
  35. package/runtime/runtime.c +257 -17
  36. package/runtime/shim.mjs +6 -915
  37. package/runtime/sqlite/LICENSE.md +85 -0
  38. package/runtime/sqlite/README.md +45 -0
  39. package/runtime/sqlite/nish-sqlite.c +359 -0
  40. package/runtime/sqlite/sqlite3.c +271131 -0
  41. package/runtime/sqlite/sqlite3.h +14447 -0
  42. package/scripts/build.sh +437 -28
  43. package/std/LICENSE-fast-float +23 -0
  44. package/std/LICENSE-fdlibm +8 -0
  45. package/std/README.md +108 -58
  46. package/std/base64.ts +116 -0
  47. package/std/bytes.ts +419 -0
  48. package/std/cli.ts +328 -0
  49. package/std/collections.ts +6 -5
  50. package/std/crypto/LICENSE-crypt_blowfish +26 -0
  51. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  52. package/std/crypto/aes.ts +238 -76
  53. package/std/crypto/base64url.ts +125 -0
  54. package/std/crypto/bcrypt.ts +760 -0
  55. package/std/crypto/chacha20poly1305.ts +43 -9
  56. package/std/crypto/ed25519.ts +840 -0
  57. package/std/crypto/hkdf.ts +304 -12
  58. package/std/crypto/hmac.ts +382 -54
  59. package/std/crypto/p256.ts +932 -61
  60. package/std/crypto/x25519.ts +55 -37
  61. package/std/crypto/x509-random.ts +71 -0
  62. package/std/crypto/x509.ts +20 -131
  63. package/std/json-tokenizer.ts +4237 -0
  64. package/std/json-typed.ts +1014 -0
  65. package/std/json-value.ts +1182 -0
  66. package/std/json.ts +1202 -34
  67. package/std/math.ts +608 -0
  68. package/std/net/buffer-pool.ts +151 -0
  69. package/std/net/hpack.ts +416 -112
  70. package/std/net/http-fields.ts +93 -0
  71. package/std/net/http1-server.ts +501 -62
  72. package/std/net/http1.ts +31 -6
  73. package/std/net/http2-tls.ts +112 -24
  74. package/std/net/http2.ts +333 -75
  75. package/std/net/http3-server.ts +261 -43
  76. package/std/net/http3.ts +187 -28
  77. package/std/net/postgres.ts +1875 -0
  78. package/std/net/prefork.ts +412 -0
  79. package/std/net/quic-listener.ts +108 -11
  80. package/std/net/quic-packet.ts +243 -82
  81. package/std/net/quic-recovery.ts +361 -30
  82. package/std/net/quic-stream.ts +424 -24
  83. package/std/net/quic.ts +519 -167
  84. package/std/net/router.ts +676 -0
  85. package/std/net/tls/codec.ts +426 -27
  86. package/std/net/tls/record-server.ts +478 -132
  87. package/std/net/tls/record.ts +133 -42
  88. package/std/net/tls/schedule.ts +464 -55
  89. package/std/net/tls/ticket.ts +610 -0
  90. package/std/net/tls-tcp.ts +196 -53
  91. package/std/net/tls.ts +1018 -260
  92. package/std/net/url.ts +571 -0
  93. package/std/random.ts +94 -0
  94. package/std/regex.ts +4274 -0
  95. package/std/sqlite.ts +391 -0
  96. package/std/term-io.ts +978 -0
  97. package/std/term-pixels.ts +874 -0
  98. package/std/term-probe.ts +459 -0
  99. package/std/term-rate.ts +993 -0
  100. package/std/term.ts +2453 -0
  101. package/std/text.ts +90 -7
  102. package/std/threads.ts +438 -14
  103. package/std/yaml-lines.ts +141 -0
  104. package/std/yaml.ts +4675 -0
@@ -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())
@@ -68,9 +68,13 @@
68
68
  * of in one burst that overruns a queue on the path.
69
69
  *
70
70
  * let out: u8[] | null = quicListenerTakePaced(conn, now);
71
- * while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
71
+ * while (out !== null) { … send it with conn.datagramEcn … ; 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,13 +83,23 @@
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
  *
93
+ * **ECN** (RFC 9000 §13.4). A carrier whose platform sends the ECN bits it
94
+ * is given sets the connection's `ecnMarking` and sends each datagram with
95
+ * the codepoint the connection marked it with: `conn.datagramEcn` after a
96
+ * `takeDatagramInto` or `quicListenerTakePaced`, `flight.ecn` for a flight.
97
+ * One send carries one codepoint, so a flight ends before a datagram the
98
+ * connection would mark otherwise. The listener's own answers go unmarked.
99
+ *
86
100
  * const flight = new QuicFlight();
87
101
  * const n: i32 = quicListenerTakeFlight(conn, now, buf, 0, toI32(buf.length), flight);
88
- * if (n > 0) { udpSendTo(fd, buf, 0, n, peer, flight.count > 1 ? flight.segment : 0, 0); }
102
+ * if (n > 0) { udpSendTo(fd, buf, 0, n, peer, flight.count > 1 ? flight.segment : 0, flight.ecn); }
89
103
  *
90
104
  * **Sans-IO and deterministic.** The caller gives the time, the client's
91
105
  * address and, once, `QUIC_LISTENER_ENTROPY_SIZE` random bytes: every
@@ -95,9 +109,10 @@
95
109
  * byte for byte.
96
110
  *
97
111
  * **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.
112
+ * `QuicServerConfig`, for the server's lifetime, as plain bytes, since a
113
+ * `Secret` may not be a field (NL2430). `release()` wipes the listener's own,
114
+ * and `Http3Server.stop` the configuration's with them; QUIC-5 in
115
+ * `docs/security/quic.md` records what is left.
101
116
  *
102
117
  * Written from RFC 9000, in this module's own structure; nothing here is
103
118
  * ported from another implementation. Private names carry the
@@ -212,12 +227,38 @@ export class QuicListenerAnswer {
212
227
  retryScid: u8[]
213
228
  kind: i32 = 0
214
229
  retried: boolean = false
230
+ room: i32 = 0
215
231
 
216
232
  constructor(room: i32) {
217
- this.reply = quicListenerRoom(room > 0 ? room : 0)
233
+ this.room = room > 0 ? room : 0
234
+ this.reply = quicListenerRoom(this.room)
218
235
  this.originalDcid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
219
236
  this.retryScid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
220
237
  }
238
+
239
+ /**
240
+ * Zeroes the last answer: `reply` across its whole room and each ID across
241
+ * its 20 bytes, so a Retry's token, a stateless reset's token and the IDs
242
+ * leave no copy beyond the arrays' lengths either. Each is left that long
243
+ * and all zero, `kind` a drop. An answer whose reply grew past its room
244
+ * keeps what lies beyond the longest length it is wiped at; one made with
245
+ * `QUIC_LISTENER_ANSWER_SIZE` never grows.
246
+ */
247
+ wipe(): void {
248
+ quicListenerWipeRoom(this.reply, this.room)
249
+ quicListenerWipeRoom(this.originalDcid, QUIC_MAX_CID_LENGTH)
250
+ quicListenerWipeRoom(this.retryScid, QUIC_MAX_CID_LENGTH)
251
+ this.kind = QUIC_LISTEN_DROP
252
+ this.retried = false
253
+ }
254
+ }
255
+
256
+ /** Grows `bytes` to at least `room` in the room it already has and zeroes all of it. */
257
+ const quicListenerWipeRoom = (bytes: u8[], room: i32): void => {
258
+ if (toI32(bytes.length) < room) {
259
+ quicListenerResize(bytes, room)
260
+ }
261
+ secureZero(bytes)
221
262
  }
222
263
 
223
264
  /** An empty array with room for `size` bytes, so pushing up to that many allocates nothing. */
@@ -365,6 +406,30 @@ export class QuicListener {
365
406
  secureZero(entropy)
366
407
  }
367
408
 
409
+ /**
410
+ * Ends the listener: wipes the seed and the generator's unused bytes, the
411
+ * tag and the HMAC and HKDF scratch, the INVALID_TOKEN close's Initial
412
+ * secrets and key slots, and `handle`'s spare answer, which holds the last
413
+ * one; `handle` and `handleWindow` then drop every datagram they are
414
+ * handed. The other methods still run, under the zeroed seed, and are the
415
+ * caller's not to call. What `handle` returned is the caller's to wipe. The
416
+ * configuration's keys are not the listener's to wipe: `Http3Server.stop`
417
+ * wipes them with the rest of the server.
418
+ */
419
+ release(): void {
420
+ this.usable = false
421
+ secureZero(this.seed)
422
+ secureZero(this.pool)
423
+ this.poolHead = QUIC_LISTENER_POOL
424
+ secureZero(this.tag)
425
+ this.kdf.wipe()
426
+ secureZero(this.initialClient)
427
+ secureZero(this.initialServer)
428
+ this.readSlot.wipe()
429
+ this.writeSlot.wipe()
430
+ this.spare.wipe()
431
+ }
432
+
368
433
  /**
369
434
  * One unpredictable value in 0 to 255: the next byte of HMAC-SHA256 under
370
435
  * the seed of a counter, a block at a time, each byte wiped once taken.
@@ -843,6 +908,24 @@ export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | nu
843
908
  export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
844
909
  now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
845
910
 
911
+ /** The most credit a paced flight waits for before it starts: four datagrams. */
912
+ export const QUIC_LISTENER_FLIGHT_QUANTUM: i32 = 4800
913
+
914
+ /**
915
+ * The credit `quicListenerTakeFlight` waits for before `conn` starts a
916
+ * flight: what its pacer earns in a millisecond, from one datagram up to
917
+ * `QUIC_LISTENER_FLIGHT_QUANTUM`.
918
+ */
919
+ const quicListenerQuantum = (conn: QuicConnection): i32 =>
920
+ conn.recovery.pacerQuantum(QUIC_CONN_DATAGRAM_SIZE, QUIC_LISTENER_FLIGHT_QUANTUM)
921
+
922
+ /**
923
+ * When `quicListenerTakeFlight` next lets `conn` start a flight, in the
924
+ * caller's milliseconds: `now` when it may already.
925
+ */
926
+ export const quicListenerFlightTime = (conn: QuicConnection, now: i64): i64 =>
927
+ now + conn.recovery.pacerDelay(now, quicListenerQuantum(conn))
928
+
846
929
  /** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
847
930
  export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
848
931
 
@@ -852,19 +935,23 @@ export class QuicFlight {
852
935
  count: i32 = 0
853
936
  /** Every datagram's size but the last's, which may be shorter: the `UDP_SEGMENT` to send with. */
854
937
  segment: i32 = 0
938
+ /** The ECN codepoint every datagram of the flight was marked with: the one to send with. */
939
+ ecn: i32 = 0
855
940
  }
856
941
 
857
942
  /**
858
943
  * Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
859
944
  * datagram after datagram, for one GSO send, and answers its length in
860
945
  * 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
946
+ * segment size. A flight starts only once the pacer has the credit
947
+ * `quicListenerFlightTime` waits for, a millisecond's earning of one to four
948
+ * datagrams; then each datagram goes only when the pacer has credit for a
949
+ * full one (RFC 9002 §7.7) and is charged at its real size, as
863
950
  * `quicListenerTakePaced` does, so the flight is the pacer's whole credit
864
951
  * and no more. It stops at the first datagram shorter than
865
952
  * `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
866
- * at `QUIC_LISTENER_FLIGHT_MAX` datagrams, and when `cap` has no room for
867
- * another.
953
+ * at `QUIC_LISTENER_FLIGHT_MAX` datagrams, when `cap` has no room for
954
+ * another, and before a datagram whose ECN codepoint would not be the first's.
868
955
  */
869
956
  export const quicListenerTakeFlight = (
870
957
  conn: QuicConnection,
@@ -876,12 +963,21 @@ export const quicListenerTakeFlight = (
876
963
  ): i32 => {
877
964
  flight.count = 0
878
965
  flight.segment = 0
966
+ flight.ecn = 0
879
967
  let length: i32 = 0
968
+ if (conn.recovery.pacerDelay(now, quicListenerQuantum(conn)) > 0) {
969
+ return length
970
+ }
880
971
  while (
881
972
  flight.count < QUIC_LISTENER_FLIGHT_MAX &&
882
973
  length + QUIC_CONN_DATAGRAM_SIZE <= cap &&
883
974
  conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE) <= 0
884
975
  ) {
976
+ // The codepoint `takeDatagramInto` would mark the next datagram with: one send carries one.
977
+ const next: i32 = conn.ecnMarking ? conn.recovery.ecnCodepoint() : 0
978
+ if (flight.count > 0 && next !== flight.ecn) {
979
+ return length
980
+ }
885
981
  const n: i32 = conn.takeDatagramInto(buf, at + length, now)
886
982
  if (n <= 0) {
887
983
  return length
@@ -889,6 +985,7 @@ export const quicListenerTakeFlight = (
889
985
  conn.recovery.onPaced(now, n)
890
986
  if (flight.count === 0) {
891
987
  flight.segment = n
988
+ flight.ecn = conn.datagramEcn
892
989
  }
893
990
  flight.count = flight.count + 1
894
991
  length = length + n