@amritk/nish-x86_64-linux 0.17.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 (61) 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 +231 -20
  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 +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  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 +1376 -42
  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 +1412 -70
  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 +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -7,19 +7,27 @@
7
7
  * (§10.3). Sans-IO, like the rest of `nish/net`, and stateless in the sense
8
8
  * the RFC means: nothing it answers depends on any connection.
9
9
  *
10
- * import { QuicListener, QuicListenerAnswer, QUIC_LISTEN_ACCEPT } from "nish/net/quic-listener";
10
+ * import { QuicListener, QuicListenerAnswer, QUIC_LISTEN_ACCEPT, QUIC_LISTENER_ANSWER_SIZE } from "nish/net/quic-listener";
11
11
  *
12
12
  * const listener = new QuicListener(config, entropy); // entropy: QUIC_LISTENER_ENTROPY_SIZE random bytes
13
- * // For a datagram whose first packet no connection owns (`ownsConnectionId`):
14
- * const answer: QuicListenerAnswer = listener.handle(datagram, fromAddress, now);
15
- * if (answer.kind === QUIC_LISTEN_ACCEPT) {
13
+ * const answer = new QuicListenerAnswer(QUIC_LISTENER_ANSWER_SIZE); // once, beside the listener
14
+ * // For a datagram buf[at .. at + len) whose first packet no connection owns:
15
+ * if (listener.handleWindow(buf, at, len, fromAddress, now, answer) === QUIC_LISTEN_ACCEPT) {
16
16
  * const conn = new QuicConnection(config, connectionEntropy);
17
17
  * if (answer.retried) { conn.acceptRetry(answer.originalDcid, answer.retryScid); }
18
- * conn.receive(datagram, now);
18
+ * conn.receiveWindow(buf, at, len, now);
19
19
  * } else if (toI32(answer.reply.length) > 0) {
20
20
  * … send answer.reply back to fromAddress …
21
21
  * }
22
22
  *
23
+ * **Nothing is kept per datagram** (H3-3 in `docs/security/http3.md`).
24
+ * `handleWindow` reads the datagram where it lies, its header parsed in
25
+ * place, and writes every answer into the caller's `QuicListenerAnswer`,
26
+ * whose arrays were given their room once; every MAC, key and seal is
27
+ * computed in scratch the listener made at start-up. `handle` is the same
28
+ * over a whole array, into an answer of its own each time, for a caller
29
+ * that keeps them.
30
+ *
23
31
  * **Routing is the caller's.** The listener sees only datagrams the caller
24
32
  * could not hand to a live connection, and it must see only those: a stateless
25
33
  * reset carries the token for the packet's connection ID, and handing the
@@ -39,7 +47,7 @@
39
47
  * server issued, is treated as no token at all (§8.1.3).
40
48
  *
41
49
  * **Stateless resets** (§10.3) end in the token `quicStatelessResetToken`
42
- * derives from the configuration's static key and the packet's connection
50
+ * derives (computed in place, by `quicConnResetTokenInto`) from the configuration's static key and the packet's connection
43
51
  * ID, so a server that restarted with the same key can end the connections
44
52
  * it lost. RFC 9000 §10.3 and §10.3.3 bound them, and each bound is held
45
53
  * here. A reset is sent only for a short header, and is always shorter than
@@ -63,6 +71,10 @@
63
71
  * while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
64
72
  * … and wake at the earlier of conn.deadline() and quicListenerPaceTime(conn, now)
65
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
+ *
66
78
  * **GSO and GRO** (`nish:net`'s `UDP_SEGMENT` and `UDP_GRO`). A carrier that
67
79
  * sends with segmentation offload takes a whole flight at once:
68
80
  * `quicListenerTakeFlight` writes as many datagrams as the pacer has credit
@@ -71,7 +83,10 @@
71
83
  * cuts back into datagrams; each is written in place, so a flight allocates
72
84
  * nothing. The flight is paced as a whole: it is as long as the pacer's
73
85
  * credit, and charged to it datagram by datagram, so a burst is never more
74
- * 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
75
90
  * side, `quicListenerReceiveSegments` hands a connection the datagrams of
76
91
  * one GRO receive, segment by segment, in place.
77
92
  *
@@ -87,16 +102,19 @@
87
102
  * byte for byte.
88
103
  *
89
104
  * **Secrets.** The seed lives in the listener and the static keys in the
90
- * caller's `QuicServerConfig`, for the server's lifetime, as plain bytes,
91
- * since a `Secret` may not be a field (NL2430); QUIC-5 in
92
- * `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.
93
109
  *
94
110
  * Written from RFC 9000, in this module's own structure; nothing here is
95
111
  * ported from another implementation. Private names carry the
96
112
  * `quicListener` prefix (`docs/wp26-stdlib.md` §3e).
97
113
  */
98
- import { timingSafeEqual } from "nish/crypto/ct"
99
- import { hmacSha256 } from "nish/crypto/hmac"
114
+ import { AesKey, aesGcmSeal, aesKeyInto } from "nish/crypto/aes"
115
+ import { timingSafeEqualAt } from "nish/crypto/ct"
116
+ import { HkdfScratch } from "nish/crypto/hkdf"
117
+ import { HmacSha256Scratch } from "nish/crypto/hmac"
100
118
  import {
101
119
  QUIC_AEAD_AES_128_GCM,
102
120
  QUIC_ERR_VERSION,
@@ -104,26 +122,29 @@ import {
104
122
  QUIC_PACKET_INITIAL,
105
123
  QUIC_PACKET_OK,
106
124
  QUIC_PACKET_SHORT,
125
+ QUIC_RETRY_TAG_SIZE,
107
126
  QuicHeader,
108
- QuicInitialSecrets,
109
127
  QuicKeys,
128
+ QuicKeysSlot,
110
129
  QuicPacket,
111
- quicInitialSecrets,
112
- quicKeys,
113
- quicLongHeader,
114
- quicOpenPacket,
115
- quicParseHeader,
116
- quicRetryPacket,
117
- quicSealPacket,
130
+ quicDecryptPayload,
131
+ quicInitialSecretsInto,
132
+ quicKeysInto,
133
+ quicLongHeaderSize,
134
+ quicPacketCopy,
135
+ quicParseHeaderInto,
136
+ quicPutLongHeader,
137
+ quicSealInPlace,
138
+ quicUnprotectHeader,
118
139
  } from "nish/net/quic-packet"
119
- import { QUIC_ERROR_INVALID_TOKEN, QUIC_RESET_TOKEN_SIZE, quicPushConnectionClose } from "nish/net/quic-frame"
140
+ import { QUIC_ERROR_INVALID_TOKEN, QUIC_RESET_TOKEN_SIZE, quicPutConnectionClose } from "nish/net/quic-frame"
120
141
  import {
121
142
  QUIC_CONN_CID_LENGTH,
122
143
  QUIC_CONN_DATAGRAM_SIZE,
123
144
  QUIC_CONN_STATIC_KEY_SIZE,
124
145
  QuicConnection,
125
146
  QuicServerConfig,
126
- quicStatelessResetToken,
147
+ quicConnResetTokenInto,
127
148
  } from "nish/net/quic"
128
149
 
129
150
  // ---- What `handle` answers ----------------------------------------------------
@@ -165,12 +186,33 @@ const QUIC_LISTENER_TOKEN_MAC_SIZE: i32 = 16
165
186
  const QUIC_LISTENER_TOKEN_HEAD: i32 = 10
166
187
  /** Up to this datagram length a reset is exactly one byte shorter (RFC 9000 §10.3). */
167
188
  const QUIC_LISTENER_SHORT_TRIGGER: i32 = 43
189
+ /** The generator's block, one HMAC-SHA256 tag: the bytes `refill` makes at a time. */
190
+ const QUIC_LISTENER_POOL: i32 = 32
191
+ /** The generator's message: its counter, eight bytes. */
192
+ const QUIC_LISTENER_BLOCK: i32 = 8
193
+ /** A typed 0, the start of a whole array, since a bare literal is an `f64` under `--number-mode f64`. */
194
+ const QUIC_LISTENER_FROM: i32 = 0
195
+
196
+ /**
197
+ * The largest answer the listener sends, and so the room `QuicListenerAnswer`
198
+ * makes for `reply`: a stateless reset is at most a full datagram (RFC 9000
199
+ * §10.3); a Version Negotiation packet, which echoes two IDs of up to 255
200
+ * bytes, a Retry and the INVALID_TOKEN close are all shorter.
201
+ */
202
+ export const QUIC_LISTENER_ANSWER_SIZE: i32 = QUIC_CONN_DATAGRAM_SIZE
168
203
 
169
204
  /**
170
- * What `QuicListener.handle` decided about one datagram: `kind` is a
171
- * `QUIC_LISTEN_*`, and `reply` the datagram to send back, empty when there is
172
- * none. For `QUIC_LISTEN_ACCEPT` after a Retry, `retried` is set and
205
+ * What `QuicListener.handleWindow` decided about one datagram: `kind` is a
206
+ * `QUIC_LISTEN_*`, and `reply` the datagram to send back, empty when there
207
+ * is none. For `QUIC_LISTEN_ACCEPT` after a Retry, `retried` is set and
173
208
  * `originalDcid` and `retryScid` are what `QuicConnection.acceptRetry` takes.
209
+ *
210
+ * Make one once, with `QUIC_LISTENER_ANSWER_SIZE` as its `room`, and hand it
211
+ * to every `handleWindow`: the constructor gives `reply` that room and each
212
+ * ID 20 bytes, and each answer refills them in it, its length the answer's,
213
+ * so answering allocates nothing (H3-3). A smaller room still works, and
214
+ * grows the first time an answer needs more; `handle` makes each answer
215
+ * with exactly the room its reply takes.
174
216
  */
175
217
  export class QuicListenerAnswer {
176
218
  reply: u8[]
@@ -178,60 +220,93 @@ export class QuicListenerAnswer {
178
220
  retryScid: u8[]
179
221
  kind: i32 = 0
180
222
  retried: boolean = false
223
+ room: i32 = 0
181
224
 
182
- constructor() {
183
- this.reply = []
184
- this.originalDcid = []
185
- this.retryScid = []
225
+ constructor(room: i32) {
226
+ this.room = room > 0 ? room : 0
227
+ this.reply = quicListenerRoom(this.room)
228
+ this.originalDcid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
229
+ this.retryScid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
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
186
246
  }
187
247
  }
188
248
 
189
- /** The bytes `bytes[from .. from + length)`, copied into an array of exactly that size. */
190
- const quicListenerSlice = (bytes: u8[], from: i32, length: i32): u8[] => {
191
- const out: u8[] = new Array<u8>(length)
192
- const n: i32 = toI32(out.length)
193
- for (let k: i32 = 0; k < n; k += 1) {
194
- if (from + k >= 0 && from + k < toI32(bytes.length)) {
195
- out[k] = bytes[from + k]
196
- }
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)
255
+ }
256
+
257
+ /** An empty array with room for `size` bytes, so pushing up to that many allocates nothing. */
258
+ const quicListenerRoom = (size: i32): u8[] => {
259
+ const out: u8[] = new Array<u8>(size)
260
+ while (out.length > 0) {
261
+ out.pop()
197
262
  }
198
263
  return out
199
264
  }
200
265
 
201
- /** Appends every byte of `bytes` to `out`. */
202
- const quicListenerAppend = (out: u8[], bytes: u8[]): void => {
203
- for (const b of bytes) {
204
- out.push(b)
266
+ /** Sets `bytes`' length to `length` in the room it already has: popped, or pushed with zeros. */
267
+ const quicListenerResize = (bytes: u8[], length: i32): void => {
268
+ while (toI32(bytes.length) > length) {
269
+ bytes.pop()
205
270
  }
271
+ while (toI32(bytes.length) < length) {
272
+ bytes.push(toU8(0))
273
+ }
274
+ }
275
+
276
+ /** Puts `from[at .. at + length)` in `to`, resized to `length` in its own room. */
277
+ const quicListenerRefill = (to: u8[], from: u8[], at: i32, length: i32): void => {
278
+ quicListenerResize(to, length)
279
+ quicPacketCopy(to, 0, from, at, length)
206
280
  }
207
281
 
208
- /** Appends `value`'s low `size` bytes to `out`, big-endian. */
209
- const quicListenerPushNumber = (out: u8[], value: i64, size: i32): void => {
210
- for (let k: i32 = size - 1; k >= 0; k -= 1) {
211
- out.push(toU8(toI32((value >> (toI64(k) * 8)) & 255)))
282
+ /** Writes `value`'s low `size` bytes into `buf` at `at`, big-endian. */
283
+ const quicListenerPutNumber = (buf: u8[], at: i32, value: i64, size: i32): void => {
284
+ for (let k: i32 = 0; k < size; k += 1) {
285
+ if (at + k >= 0 && at + k < toI32(buf.length)) {
286
+ buf[at + k] = toU8(toI32((value >> (toI64(size - 1 - k) * 8)) & 255))
287
+ }
212
288
  }
213
289
  }
214
290
 
215
291
  /**
216
- * A Version Negotiation packet (RFC 9000 §17.2.1) answering a packet from
217
- * `scid` to `dcid`: the IDs swapped, version 0, and version 1 as the only one
218
- * supported. `unused` fills the first byte's low six bits, under the 0x40 the
219
- * RFC asks for so the packet looks like QUIC to a demultiplexer.
292
+ * RFC 9001 §5.8's fixed AES-128-GCM key and nonce for version 1's Retry
293
+ * integrity tag: the listener keeps the key expanded, so a Retry's tag is
294
+ * computed without `quicRetryIntegrityTag`'s expansion, which stores what it
295
+ * allocates and so cannot run in an arena block.
220
296
  *
221
- * TODO(WP34 Q1): every other packet format is `nish/net/quic-packet`'s; this
222
- * belongs beside `quicRetryPacket` there, which this stage may not edit.
297
+ * TODO(WP34 Q1): `nish/net/quic-packet` holds the same two constants and
298
+ * should offer the tag over a kept key; this stage may not edit it.
223
299
  */
224
- const quicListenerVersionNegotiation = (dcid: u8[], scid: u8[], unused: i32): u8[] => {
225
- const out: u8[] = []
226
- out.push(toU8((unused & 0x3f) | 0xc0))
227
- quicListenerPushNumber(out, 0, 4)
228
- out.push(toU8(toI32(scid.length)))
229
- quicListenerAppend(out, scid)
230
- out.push(toU8(toI32(dcid.length)))
231
- quicListenerAppend(out, dcid)
232
- quicListenerPushNumber(out, 1, 4)
233
- return out
234
- }
300
+ const quicListenerRetryKey = (): u8[] => [
301
+ 0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e,
302
+ ]
303
+ const quicListenerRetryNonce = (): u8[] => [
304
+ 0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb,
305
+ ]
306
+
307
+ /** Whether `[at, at + len)` is a window inside an array of `length` elements: `p256WindowFits`' test. */
308
+ const quicListenerWindowFits = (length: i32, at: i32, len: i32): boolean =>
309
+ at >= 0 && len >= 0 && at <= length - len
235
310
 
236
311
  /**
237
312
  * The server's answers to datagrams no connection owns, under one
@@ -243,12 +318,42 @@ const quicListenerVersionNegotiation = (dcid: u8[], scid: u8[], unused: i32): u8
243
318
  * `QUIC_CONN_STATIC_KEY_SIZE` bytes, or whose `retryTokenLifetime` is outside
244
319
  * 1 to `QUIC_LISTENER_MAX_TOKEN_LIFETIME`, or entropy of another length, make
245
320
  * a listener that drops everything.
321
+ *
322
+ * Everything an answer is computed in is made here, once: the header a
323
+ * datagram is parsed into, the HMAC and HKDF scratch, the Initial secrets
324
+ * and key slots of the INVALID_TOKEN close, and the generator's block. So
325
+ * `handleWindow` reads the datagram where it lies and keeps nothing.
246
326
  */
247
327
  export class QuicListener {
248
328
  config: QuicServerConfig
249
329
  seed: u8[]
250
330
  /** Unpredictable bytes not yet used, from `poolHead`, and the counter the next block is made from. */
251
331
  pool: u8[]
332
+ /** The counter's eight bytes, the generator's HMAC message. */
333
+ block: u8[]
334
+ /** What `handleWindow` parses a datagram's header into, in place. */
335
+ header: QuicHeader
336
+ /** Every HMAC and HKDF the listener computes runs here; its `sha256` makes tokens, resets and the generator's blocks. */
337
+ kdf: HkdfScratch
338
+ /** A full HMAC-SHA256 tag, wiped once its 16 bytes are used. */
339
+ tag: u8[]
340
+ /** A Retry token MAC's length fields: the Retry SCID's one byte, the address's four. */
341
+ lengths: u8[]
342
+ /** The Initial secrets of an INVALID_TOKEN close, and its keys, both directions. */
343
+ initialClient: u8[]
344
+ initialServer: u8[]
345
+ readSlot: QuicKeysSlot
346
+ writeSlot: QuicKeysSlot
347
+ packet: QuicPacket
348
+ /** The client's SCID and the DCID it sent to, copied out for `quicPutLongHeader`, which reads IDs from 0. */
349
+ peerCid: u8[]
350
+ ownCid: u8[]
351
+ /** An empty array: the INVALID_TOKEN close's token and reason. */
352
+ none: u8[]
353
+ /** Version 1's fixed Retry key (RFC 9001 §5.8), expanded once. */
354
+ retryKey: AesKey
355
+ /** What `handle` answers into before it copies the answer out. */
356
+ spare: QuicListenerAnswer
252
357
  counter: i64 = 0
253
358
  /** The latest time the caller gave, in milliseconds; time never runs backwards here. */
254
359
  now: i64 = 0
@@ -265,8 +370,26 @@ export class QuicListener {
265
370
  /** A listener under `config`; `entropy`'s `QUIC_LISTENER_ENTROPY_SIZE` bytes are copied, then wiped in the caller's array. */
266
371
  constructor(config: QuicServerConfig, entropy: u8[]) {
267
372
  this.config = config
268
- this.seed = quicListenerSlice(entropy, 0, QUIC_LISTENER_ENTROPY_SIZE)
269
- this.pool = []
373
+ this.seed = new Array<u8>(QUIC_LISTENER_ENTROPY_SIZE)
374
+ quicPacketCopy(this.seed, 0, entropy, 0, QUIC_LISTENER_ENTROPY_SIZE)
375
+ this.pool = new Array<u8>(QUIC_LISTENER_POOL)
376
+ this.poolHead = QUIC_LISTENER_POOL
377
+ this.block = new Array<u8>(QUIC_LISTENER_BLOCK)
378
+ this.header = new QuicHeader()
379
+ this.kdf = new HkdfScratch()
380
+ this.tag = new Array<u8>(QUIC_LISTENER_POOL)
381
+ this.lengths = new Array<u8>(5)
382
+ this.initialClient = new Array<u8>(32)
383
+ this.initialServer = new Array<u8>(32)
384
+ this.readSlot = new QuicKeysSlot()
385
+ this.writeSlot = new QuicKeysSlot()
386
+ this.packet = new QuicPacket()
387
+ this.peerCid = new Array<u8>(QUIC_MAX_CID_LENGTH)
388
+ this.ownCid = new Array<u8>(QUIC_MAX_CID_LENGTH)
389
+ this.none = []
390
+ this.spare = new QuicListenerAnswer(QUIC_LISTENER_ANSWER_SIZE)
391
+ this.retryKey = new AesKey(10, new Array<u64>(88))
392
+ aesKeyInto(quicListenerRetryKey(), this.retryKey)
270
393
  this.usable =
271
394
  toI32(entropy.length) === QUIC_LISTENER_ENTROPY_SIZE &&
272
395
  toI32(config.statelessResetKey.length) === QUIC_CONN_STATIC_KEY_SIZE &&
@@ -276,218 +399,421 @@ export class QuicListener {
276
399
  secureZero(entropy)
277
400
  }
278
401
 
279
- /** `count` unpredictable bytes: HMAC-SHA256 under the seed of a counter, a block at a time. */
280
- random(count: i32): u8[] {
281
- const out: u8[] = []
282
- while (toI32(out.length) < count) {
283
- if (this.poolHead >= toI32(this.pool.length)) {
284
- secureZero(this.pool)
285
- const block: u8[] = []
286
- quicListenerPushNumber(block, this.counter, 8)
287
- this.counter = this.counter + 1
288
- this.pool = hmacSha256(this.seed, block)
289
- this.poolHead = 0
290
- }
291
- if (this.poolHead >= 0 && this.poolHead < toI32(this.pool.length)) {
292
- out.push(this.pool[this.poolHead])
293
- this.pool[this.poolHead] = toU8(0)
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
+
426
+ /**
427
+ * One unpredictable value in 0 to 255: the next byte of HMAC-SHA256 under
428
+ * the seed of a counter, a block at a time, each byte wiped once taken.
429
+ */
430
+ randomByte(): i32 {
431
+ if (this.poolHead >= QUIC_LISTENER_POOL) {
432
+ this.refill()
433
+ }
434
+ let b: i32 = 0
435
+ if (this.poolHead >= 0 && this.poolHead < toI32(this.pool.length)) {
436
+ b = toI32(this.pool[this.poolHead])
437
+ this.pool[this.poolHead] = toU8(0)
438
+ }
439
+ this.poolHead = this.poolHead + 1
440
+ return b
441
+ }
442
+
443
+ /** Writes `count` unpredictable bytes into `out` at `at`. */
444
+ randomInto(out: u8[], at: i32, count: i32): void {
445
+ for (let k: i32 = 0; k < count; k += 1) {
446
+ const b: i32 = this.randomByte()
447
+ if (at + k >= 0 && at + k < toI32(out.length)) {
448
+ out[at + k] = toU8(b)
294
449
  }
295
- this.poolHead = this.poolHead + 1
296
450
  }
297
- return out
298
451
  }
299
452
 
300
- /** One unpredictable value in 0 to 255. */
301
- randomByte(): i32 {
302
- const one: i32 = 1
303
- const b: u8[] = this.random(one)
304
- return toI32(b.length) > 0 ? toI32(b[0]) : 0
453
+ /** The generator's next block, HMAC-SHA256 under the seed of the counter, into `pool`. */
454
+ refill(): void {
455
+ quicListenerPutNumber(this.block, 0, this.counter, QUIC_LISTENER_BLOCK)
456
+ this.counter = this.counter + 1
457
+ const mac: HmacSha256Scratch = this.kdf.sha256
458
+ {
459
+ using _scope = arena()
460
+ mac.begin(this.seed, QUIC_LISTENER_FROM, QUIC_LISTENER_ENTROPY_SIZE)
461
+ mac.update(this.block, QUIC_LISTENER_FROM, QUIC_LISTENER_BLOCK)
462
+ mac.finishInto(this.pool, QUIC_LISTENER_FROM)
463
+ }
464
+ mac.wipe()
465
+ this.poolHead = 0
305
466
  }
306
467
 
307
468
  /**
308
469
  * Decides what to answer a datagram whose first packet no connection
309
- * owns, from `address` (the client's address as the caller's socket
310
- * reports it, port included) at `now` (the caller's monotonic time in
311
- * milliseconds). See the module header for the rules; nothing a peer sends
312
- * makes it panic.
470
+ * owns, `datagram[at .. at + len)`, read where it lies, from `address`
471
+ * (the client's address as the caller's socket reports it, port included)
472
+ * at `now` (the caller's monotonic time in milliseconds), into `answer`,
473
+ * whose arrays are refilled in their own room. Answers `answer.kind`. See
474
+ * the module header for the rules; nothing a peer sends makes it panic,
475
+ * and nothing it does keeps arena memory. A window outside `datagram` is
476
+ * the caller's mistake, and panics.
313
477
  */
314
- handle(datagram: u8[], address: u8[], now: i64): QuicListenerAnswer {
315
- const answer: QuicListenerAnswer = new QuicListenerAnswer()
478
+ handleWindow(datagram: u8[], at: i32, len: i32, address: u8[], now: i64, answer: QuicListenerAnswer): i32 {
479
+ if (!quicListenerWindowFits(toI32(datagram.length), at, len)) {
480
+ panic(
481
+ `QuicListener.handleWindow: the window [${at}, ${at} + ${len}) is outside a datagram of ${toI32(datagram.length)} bytes`
482
+ )
483
+ }
484
+ answer.kind = QUIC_LISTEN_DROP
485
+ answer.retried = false
486
+ quicListenerResize(answer.reply, 0)
487
+ quicListenerResize(answer.originalDcid, 0)
488
+ quicListenerResize(answer.retryScid, 0)
316
489
  if (now > this.now) {
317
490
  this.now = now
318
491
  }
319
492
  if (!this.usable) {
320
- return answer
493
+ return answer.kind
321
494
  }
322
- const n: i32 = toI32(datagram.length)
323
- const header: QuicHeader = quicParseHeader(datagram, 0, QUIC_CONN_CID_LENGTH)
495
+ const header: QuicHeader = this.header
496
+ quicParseHeaderInto(header, datagram, at, at + len, QUIC_CONN_CID_LENGTH)
324
497
  if (header.error === QUIC_ERR_VERSION) {
325
498
  // §5.2.2: answer an unsupported version only in a datagram that could
326
499
  // start a connection, and §6.1: never a Version Negotiation packet.
327
- if (header.version !== 0 && n >= QUIC_CONN_DATAGRAM_SIZE) {
328
- answer.kind = QUIC_LISTEN_VERSION_NEGOTIATION
329
- answer.reply = quicListenerVersionNegotiation(header.dcid, header.scid, this.randomByte())
500
+ if (header.version !== 0 && len >= QUIC_CONN_DATAGRAM_SIZE) {
501
+ this.versionNegotiation(answer, datagram, header)
330
502
  }
331
- return answer
503
+ return answer.kind
332
504
  }
333
505
  if (header.error !== QUIC_PACKET_OK) {
334
- return answer
506
+ return answer.kind
335
507
  }
336
508
  if (header.type === QUIC_PACKET_SHORT) {
337
- return this.statelessReset(answer, n, header.dcid)
509
+ this.statelessReset(answer, len, datagram, header.dcidStart, header.dcidLength)
510
+ return answer.kind
338
511
  }
339
512
  // §14.1, §7.2, §5.2.2: only an Initial in a full-sized datagram, sent to
340
513
  // a DCID of at least 8 bytes, can start a connection; the rest is dropped.
341
- if (header.type !== QUIC_PACKET_INITIAL || n < QUIC_CONN_DATAGRAM_SIZE || toI32(header.dcid.length) < 8) {
342
- return answer
514
+ if (header.type !== QUIC_PACKET_INITIAL || len < QUIC_CONN_DATAGRAM_SIZE || header.dcidLength < 8) {
515
+ return answer.kind
343
516
  }
344
- const token: u8[] = header.token
345
- if (toI32(token.length) === 0 || toI32(token[0]) !== QUIC_LISTENER_TOKEN_MARKER) {
517
+ const tokenAt: i32 = header.tokenStart
518
+ if (header.tokenLength === 0 || toI32(datagram[tokenAt]) !== QUIC_LISTENER_TOKEN_MARKER) {
346
519
  if (!this.config.retry) {
347
520
  answer.kind = QUIC_LISTEN_ACCEPT
348
- return answer
521
+ return answer.kind
349
522
  }
350
- return this.retry(answer, header, address)
523
+ this.retry(answer, datagram, header, address)
524
+ return answer.kind
351
525
  }
352
- if (this.tokenValid(token, header.dcid, address)) {
526
+ if (this.tokenValid(datagram, header, address)) {
353
527
  answer.kind = QUIC_LISTEN_ACCEPT
354
528
  answer.retried = true
355
- answer.originalDcid = quicListenerSlice(
356
- token,
357
- QUIC_LISTENER_TOKEN_HEAD,
358
- toI32(token[QUIC_LISTENER_TOKEN_HEAD - 1])
529
+ quicListenerRefill(
530
+ answer.originalDcid,
531
+ datagram,
532
+ tokenAt + QUIC_LISTENER_TOKEN_HEAD,
533
+ toI32(datagram[tokenAt + QUIC_LISTENER_TOKEN_HEAD - 1])
359
534
  )
360
- answer.retryScid = header.dcid
361
- return answer
535
+ quicListenerRefill(answer.retryScid, datagram, header.dcidStart, header.dcidLength)
536
+ return answer.kind
362
537
  }
363
- return this.invalidToken(answer, datagram, header)
538
+ this.invalidToken(answer, datagram, header)
539
+ return answer.kind
364
540
  }
365
541
 
366
542
  /**
367
- * The token's MAC over its own bytes before the MAC (`head`), the ID the
368
- * client is to send to, and its address: what makes a token good for one
369
- * address and one connection only.
543
+ * `handleWindow` over the whole of `datagram`, into an answer of its own,
544
+ * made with exactly the room its reply takes: for a caller that keeps each
545
+ * answer, at the cost of one `QuicListenerAnswer` a datagram. A carrier
546
+ * makes one answer and calls `handleWindow`.
370
547
  */
371
- tokenMac(head: u8[], retryScid: u8[], address: u8[]): u8[] {
372
- const message: u8[] = []
373
- quicListenerAppend(message, head)
374
- message.push(toU8(toI32(retryScid.length)))
375
- quicListenerAppend(message, retryScid)
376
- quicListenerPushNumber(message, toI64(toI32(address.length)), 4)
377
- quicListenerAppend(message, address)
378
- const mac: u8[] = hmacSha256(this.config.retryTokenKey, message)
379
- const out: u8[] = quicListenerSlice(mac, 0, QUIC_LISTENER_TOKEN_MAC_SIZE)
380
- secureZero(mac)
381
- return out
548
+ handle(datagram: u8[], address: u8[], now: i64): QuicListenerAnswer {
549
+ const spare: QuicListenerAnswer = this.spare
550
+ this.handleWindow(datagram, QUIC_LISTENER_FROM, toI32(datagram.length), address, now, spare)
551
+ const answer: QuicListenerAnswer = new QuicListenerAnswer(toI32(spare.reply.length))
552
+ answer.kind = spare.kind
553
+ answer.retried = spare.retried
554
+ quicListenerRefill(answer.reply, spare.reply, QUIC_LISTENER_FROM, toI32(spare.reply.length))
555
+ quicListenerRefill(
556
+ answer.originalDcid,
557
+ spare.originalDcid,
558
+ QUIC_LISTENER_FROM,
559
+ toI32(spare.originalDcid.length)
560
+ )
561
+ quicListenerRefill(answer.retryScid, spare.retryScid, QUIC_LISTENER_FROM, toI32(spare.retryScid.length))
562
+ return answer
382
563
  }
383
564
 
384
- /** A Retry token for the client at `address`, whose first Initial went to `odcid`, now told to send to `retryScid`. */
385
- issueToken(odcid: u8[], retryScid: u8[], address: u8[]): u8[] {
386
- const token: u8[] = []
387
- token.push(toU8(QUIC_LISTENER_TOKEN_MARKER))
388
- quicListenerPushNumber(token, this.now, 8)
389
- token.push(toU8(toI32(odcid.length)))
390
- quicListenerAppend(token, odcid)
391
- quicListenerAppend(token, this.tokenMac(token, retryScid, address))
392
- return token
565
+ /**
566
+ * A Version Negotiation packet (RFC 9000 §17.2.1) into `answer`, answering
567
+ * a packet from the SCID `header` read to its DCID: the IDs swapped,
568
+ * version 0, and version 1 as the only one supported. A random byte fills
569
+ * the first byte's low six bits, under the 0x40 the RFC asks for so the
570
+ * packet looks like QUIC to a demultiplexer. Each ID is at most 255 bytes,
571
+ * so the packet is at most 521, inside `reply`'s room.
572
+ *
573
+ * TODO(WP34 Q1): every other packet format is `nish/net/quic-packet`'s; this
574
+ * belongs beside `quicRetryPacket` there, which this stage may not edit.
575
+ */
576
+ versionNegotiation(answer: QuicListenerAnswer, datagram: u8[], header: QuicHeader): void {
577
+ const unused: i32 = this.randomByte()
578
+ const dcidLength: i32 = header.dcidLength
579
+ const scidLength: i32 = header.scidLength
580
+ const reply: u8[] = answer.reply
581
+ quicListenerResize(reply, 11 + dcidLength + scidLength)
582
+ reply[0] = toU8((unused & 0x3f) | 0xc0)
583
+ quicListenerPutNumber(reply, 1, 0, 4)
584
+ reply[5] = toU8(scidLength)
585
+ quicPacketCopy(reply, 6, datagram, header.scidStart, scidLength)
586
+ const dcidAt: i32 = 6 + scidLength
587
+ quicListenerPutNumber(reply, dcidAt, toI64(dcidLength), 1)
588
+ quicPacketCopy(reply, dcidAt + 1, datagram, header.dcidStart, dcidLength)
589
+ quicListenerPutNumber(reply, dcidAt + 1 + dcidLength, 1, 4)
590
+ answer.kind = QUIC_LISTEN_VERSION_NEGOTIATION
393
591
  }
394
592
 
395
593
  /**
396
- * Whether `token` is one this listener issued for the client at `address`
397
- * that was told to send to `dcid`, and still within its lifetime: the
398
- * lengths add up, the MAC matches (compared in constant time), and it was
399
- * issued no later than now and less than `retryTokenLifetime` ago.
594
+ * The token MAC over `head[headAt .. headAt + headLength)`, the token's
595
+ * own bytes before its MAC, the ID the client is to send to
596
+ * (`scid[scidAt .. scidAt + scidLength)`) and its address, into `tag`:
597
+ * what makes a token good for one address and one connection only. Its
598
+ * first `QUIC_LISTENER_TOKEN_MAC_SIZE` bytes are the token's MAC; the
599
+ * caller wipes `tag` once it has used them.
400
600
  */
401
- tokenValid(token: u8[], dcid: u8[], address: u8[]): boolean {
402
- const length: i32 = toI32(token.length)
601
+ tokenMac(
602
+ head: u8[],
603
+ headAt: i32,
604
+ headLength: i32,
605
+ scid: u8[],
606
+ scidAt: i32,
607
+ scidLength: i32,
608
+ address: u8[]
609
+ ): void {
610
+ const mac: HmacSha256Scratch = this.kdf.sha256
611
+ const lengths: u8[] = this.lengths
612
+ const one: i32 = 1
613
+ const four: i32 = 4
614
+ quicListenerPutNumber(lengths, QUIC_LISTENER_FROM, toI64(scidLength), one)
615
+ quicListenerPutNumber(lengths, one, toI64(toI32(address.length)), four)
616
+ {
617
+ using _scope = arena()
618
+ mac.begin(this.config.retryTokenKey, QUIC_LISTENER_FROM, toI32(this.config.retryTokenKey.length))
619
+ mac.update(head, headAt, headLength)
620
+ mac.update(lengths, QUIC_LISTENER_FROM, one)
621
+ mac.update(scid, scidAt, scidLength)
622
+ mac.update(lengths, one, four)
623
+ mac.update(address, QUIC_LISTENER_FROM, toI32(address.length))
624
+ mac.finishInto(this.tag, QUIC_LISTENER_FROM)
625
+ }
626
+ mac.wipe()
627
+ }
628
+
629
+ /**
630
+ * Whether the token `header` read is one this listener issued for the
631
+ * client at `address` that was told to send to the DCID `header` read,
632
+ * and still within its lifetime: the lengths add up, the MAC matches
633
+ * (compared in constant time), and it was issued no later than now and
634
+ * less than `retryTokenLifetime` ago. The token is read in `datagram`.
635
+ */
636
+ tokenValid(datagram: u8[], header: QuicHeader, address: u8[]): boolean {
637
+ const tokenAt: i32 = header.tokenStart
638
+ const length: i32 = header.tokenLength
403
639
  if (length < QUIC_LISTENER_TOKEN_HEAD + QUIC_LISTENER_TOKEN_MAC_SIZE) {
404
640
  return false
405
641
  }
406
- const odcidLength: i32 = toI32(token[QUIC_LISTENER_TOKEN_HEAD - 1])
642
+ const odcidLength: i32 = toI32(datagram[tokenAt + QUIC_LISTENER_TOKEN_HEAD - 1])
407
643
  const head: i32 = QUIC_LISTENER_TOKEN_HEAD + odcidLength
408
644
  if (odcidLength > QUIC_MAX_CID_LENGTH || head + QUIC_LISTENER_TOKEN_MAC_SIZE !== length) {
409
645
  return false
410
646
  }
411
- const mac: u8[] = this.tokenMac(quicListenerSlice(token, 0, head), dcid, address)
412
- if (!timingSafeEqual(mac, quicListenerSlice(token, head, QUIC_LISTENER_TOKEN_MAC_SIZE))) {
647
+ this.tokenMac(datagram, tokenAt, head, datagram, header.dcidStart, header.dcidLength, address)
648
+ const same: boolean = timingSafeEqualAt(
649
+ this.tag,
650
+ 0,
651
+ datagram,
652
+ tokenAt + head,
653
+ QUIC_LISTENER_TOKEN_MAC_SIZE
654
+ )
655
+ secureZero(this.tag)
656
+ if (!same) {
413
657
  return false
414
658
  }
415
659
  let issued: i64 = 0
416
660
  for (let k: i32 = 1; k < QUIC_LISTENER_TOKEN_HEAD - 1; k += 1) {
417
- if (k < toI32(token.length)) {
418
- issued = (issued << 8) | toI64(token[k])
419
- }
661
+ issued = (issued << 8) | toI64(datagram[tokenAt + k])
420
662
  }
421
663
  return issued <= this.now && this.now - issued < this.config.retryTokenLifetime
422
664
  }
423
665
 
424
666
  /**
425
- * A Retry for the client's first Initial (RFC 9000 §8.1.2): a new
426
- * connection ID for the client to send to, and a token binding it, the
427
- * original DCID and the client's address to now.
667
+ * A Retry for the client's first Initial (RFC 9000 §8.1.2, §17.2.5) into
668
+ * `answer`: a new connection ID for the client to send to, and a token
669
+ * binding it, the original DCID and the client's address to now — a
670
+ * marker byte, the time, the DCID, then the MAC — under the integrity tag
671
+ * of RFC 9001 §5.8. Each part is written where it goes in `reply`.
428
672
  */
429
- retry(answer: QuicListenerAnswer, header: QuicHeader, address: u8[]): QuicListenerAnswer {
430
- const retryScid: u8[] = this.random(QUIC_CONN_CID_LENGTH)
431
- const token: u8[] = this.issueToken(header.dcid, retryScid, address)
432
- const packet: u8[] | null = quicRetryPacket(
433
- header.scid,
434
- retryScid,
435
- token,
436
- header.dcid,
437
- this.randomByte() & 15
438
- )
439
- if (packet !== null) {
673
+ retry(answer: QuicListenerAnswer, datagram: u8[], header: QuicHeader, address: u8[]): void {
674
+ const scidLength: i32 = header.scidLength
675
+ const odcidLength: i32 = header.dcidLength
676
+ const retryScidAt: i32 = 7 + scidLength
677
+ const tokenAt: i32 = retryScidAt + QUIC_CONN_CID_LENGTH
678
+ const macAt: i32 = tokenAt + QUIC_LISTENER_TOKEN_HEAD + odcidLength
679
+ const tagAt: i32 = macAt + QUIC_LISTENER_TOKEN_MAC_SIZE
680
+ const reply: u8[] = answer.reply
681
+ quicListenerResize(reply, tagAt + QUIC_RETRY_TAG_SIZE)
682
+ this.randomInto(reply, retryScidAt, QUIC_CONN_CID_LENGTH)
683
+ reply[tokenAt] = toU8(QUIC_LISTENER_TOKEN_MARKER)
684
+ quicListenerPutNumber(reply, tokenAt + 1, this.now, 8)
685
+ reply[tokenAt + QUIC_LISTENER_TOKEN_HEAD - 1] = toU8(odcidLength)
686
+ quicPacketCopy(reply, tokenAt + QUIC_LISTENER_TOKEN_HEAD, datagram, header.dcidStart, odcidLength)
687
+ this.tokenMac(reply, tokenAt, macAt - tokenAt, reply, retryScidAt, QUIC_CONN_CID_LENGTH, address)
688
+ quicPacketCopy(reply, macAt, this.tag, 0, QUIC_LISTENER_TOKEN_MAC_SIZE)
689
+ secureZero(this.tag)
690
+ reply[0] = toU8((this.randomByte() & 15) | 0xf0)
691
+ quicListenerPutNumber(reply, 1, 1, 4)
692
+ reply[5] = toU8(scidLength)
693
+ quicPacketCopy(reply, 6, datagram, header.scidStart, scidLength)
694
+ reply[retryScidAt - 1] = toU8(QUIC_CONN_CID_LENGTH)
695
+ if (this.retryTag(reply, tagAt, datagram, header.dcidStart, odcidLength)) {
440
696
  answer.kind = QUIC_LISTEN_RETRY
441
- answer.reply = packet
697
+ } else {
698
+ quicListenerResize(reply, 0)
442
699
  }
443
- return answer
700
+ }
701
+
702
+ /**
703
+ * The Retry integrity tag (RFC 9001 §5.8) over the pseudo-packet — the
704
+ * original DCID `datagram[odcidAt .. odcidAt + odcidLength)`, its length
705
+ * first, then `reply[0 .. tagAt)` — written at `reply[tagAt]`: AES-128-GCM
706
+ * under the listener's copy of version 1's fixed key, over nothing. The
707
+ * pseudo-packet and the AEAD's temporaries go with the arena block.
708
+ * Answers whether it was made.
709
+ */
710
+ retryTag(reply: u8[], tagAt: i32, datagram: u8[], odcidAt: i32, odcidLength: i32): boolean {
711
+ using _scope = arena()
712
+ const pseudo: u8[] = new Array<u8>(1 + odcidLength + tagAt)
713
+ quicListenerPutNumber(pseudo, 0, toI64(odcidLength), 1)
714
+ quicPacketCopy(pseudo, 1, datagram, odcidAt, odcidLength)
715
+ quicPacketCopy(pseudo, 1 + odcidLength, reply, 0, tagAt)
716
+ const plain: u8[] = []
717
+ const tag: u8[] | null = aesGcmSeal(this.retryKey, quicListenerRetryNonce(), pseudo, plain)
718
+ if (tag === null || toI32(tag.length) !== QUIC_RETRY_TAG_SIZE) {
719
+ return false
720
+ }
721
+ quicPacketCopy(reply, tagAt, tag, 0, QUIC_RETRY_TAG_SIZE)
722
+ return true
444
723
  }
445
724
 
446
725
  /**
447
726
  * The answer to an Initial carrying an invalid Retry token (RFC 9000
448
- * §8.1.2): an Initial with CONNECTION_CLOSE and INVALID_TOKEN, under the
449
- * Initial keys of the DCID the client sent to, from that DCID. It is sent
450
- * only when the client's Initial authenticates, so it answers a client and
451
- * not noise; it is far smaller than the datagram it answers.
727
+ * §8.1.2), into `answer`: an Initial with CONNECTION_CLOSE and
728
+ * INVALID_TOKEN, under the Initial keys of the DCID the client sent to,
729
+ * from that DCID. It is sent only when the client's Initial
730
+ * authenticates, so it answers a client and not noise; it is far smaller
731
+ * than the datagram it answers. The keys are derived into the listener's
732
+ * own slots, the Initial is opened in an arena block, and the close is
733
+ * written and sealed in `reply`.
452
734
  */
453
- invalidToken(answer: QuicListenerAnswer, datagram: u8[], header: QuicHeader): QuicListenerAnswer {
454
- const secrets: QuicInitialSecrets | null = quicInitialSecrets(header.dcid)
455
- if (secrets === null) {
456
- return answer
735
+ invalidToken(answer: QuicListenerAnswer, datagram: u8[], header: QuicHeader): void {
736
+ const secrets: boolean = quicInitialSecretsInto(
737
+ this.kdf,
738
+ datagram,
739
+ header.dcidStart,
740
+ header.dcidLength,
741
+ this.initialClient,
742
+ this.initialServer
743
+ )
744
+ if (!secrets) {
745
+ return
457
746
  }
458
- const read: QuicKeys | null = quicKeys(QUIC_AEAD_AES_128_GCM, secrets.client)
459
- if (read === null) {
460
- return answer
747
+ const read: QuicKeys | null = quicKeysInto(
748
+ this.kdf,
749
+ this.readSlot,
750
+ QUIC_AEAD_AES_128_GCM,
751
+ this.initialClient
752
+ )
753
+ if (read === null || !this.opens(read, datagram, header)) {
754
+ return
461
755
  }
462
- const opened: QuicPacket = quicOpenPacket(read, datagram, header, -1)
463
756
  // The write keys are derived only for an Initial that opened, so a forged
464
757
  // token on noise costs one derivation, not two.
465
- const write: QuicKeys | null =
466
- opened.error === QUIC_PACKET_OK ? quicKeys(QUIC_AEAD_AES_128_GCM, secrets.server) : null
758
+ const write: QuicKeys | null = quicKeysInto(
759
+ this.kdf,
760
+ this.writeSlot,
761
+ QUIC_AEAD_AES_128_GCM,
762
+ this.initialServer
763
+ )
467
764
  if (write === null) {
468
- return answer
469
- }
470
- const payload: u8[] = []
471
- const none: u8[] = []
472
- quicPushConnectionClose(payload, false, QUIC_ERROR_INVALID_TOKEN, 0, none)
473
- const first: u8[] | null = quicLongHeader(
474
- QUIC_PACKET_INITIAL,
475
- header.scid,
476
- header.dcid,
477
- none,
765
+ return
766
+ }
767
+ const peer: i32 = header.scidLength
768
+ const own: i32 = header.dcidLength
769
+ quicPacketCopy(this.peerCid, 0, datagram, header.scidStart, peer)
770
+ quicPacketCopy(this.ownCid, 0, datagram, header.dcidStart, own)
771
+ const reply: u8[] = answer.reply
772
+ quicListenerResize(reply, QUIC_LISTENER_ANSWER_SIZE)
773
+ const headerLength: i32 = quicLongHeaderSize(QUIC_PACKET_INITIAL, peer, own, 0, 0, 1, 0)
774
+ const frameEnd: i32 = quicPutConnectionClose(
775
+ reply,
776
+ headerLength,
777
+ QUIC_LISTENER_ANSWER_SIZE,
778
+ false,
779
+ QUIC_ERROR_INVALID_TOKEN,
478
780
  0,
479
- 1,
480
- toI32(payload.length)
781
+ this.none
481
782
  )
482
- if (first === null) {
483
- return answer
484
- }
485
- const packet: u8[] | null = quicSealPacket(write, first, 0, payload)
486
- if (packet !== null) {
783
+ const payloadLength: i32 = frameEnd - headerLength
784
+ const put: i32 =
785
+ headerLength > 0 && frameEnd > 0
786
+ ? quicPutLongHeader(
787
+ reply,
788
+ 0,
789
+ QUIC_PACKET_INITIAL,
790
+ this.peerCid,
791
+ peer,
792
+ this.ownCid,
793
+ own,
794
+ this.none,
795
+ 0,
796
+ 1,
797
+ payloadLength
798
+ )
799
+ : -1
800
+ const end: i32 =
801
+ put === headerLength ? quicSealInPlace(write, reply, 0, headerLength, payloadLength, 0) : -1
802
+ if (end > 0) {
803
+ quicListenerResize(reply, end)
487
804
  answer.kind = QUIC_LISTEN_INVALID_TOKEN
488
- answer.reply = packet
805
+ } else {
806
+ quicListenerResize(reply, 0)
489
807
  }
490
- return answer
808
+ }
809
+
810
+ /** Whether the Initial `header` read in `datagram` opens under `read`; what opening it allocates goes with the arena block. */
811
+ opens(read: QuicKeys, datagram: u8[], header: QuicHeader): boolean {
812
+ using _scope = arena()
813
+ const packet: QuicPacket = this.packet
814
+ const clear: u8[] = quicUnprotectHeader(read, datagram, header, -1, packet)
815
+ const payload: u8[] | null = quicDecryptPayload(read, datagram, header, clear, packet)
816
+ return payload !== null && packet.error === QUIC_PACKET_OK
491
817
  }
492
818
 
493
819
  /** Takes one stateless reset from the rate limit's budget, topping it up first. Answers whether one was there. */
@@ -512,34 +838,42 @@ export class QuicListener {
512
838
  }
513
839
 
514
840
  /**
515
- * A stateless reset answering an `n`-byte datagram sent to `dcid` (RFC
516
- * 9000 §10.3): a short-header first byte, unpredictable bytes, and the
517
- * token for `dcid`. It is one byte shorter than a datagram of up to 43
518
- * bytes and between 43 bytes and one byte short of the datagram (at most
519
- * 1200) otherwise, so it is always shorter; a datagram too short for that
520
- * to leave 21 bytes, or one past the rate limit, gets nothing.
841
+ * A stateless reset into `answer`, answering an `n`-byte datagram sent to
842
+ * the ID `datagram[dcidAt .. dcidAt + dcidLength)` (RFC 9000 §10.3): a
843
+ * short-header first byte, unpredictable bytes, and the token for that
844
+ * ID, computed in place by `quicConnResetTokenInto`. It is one byte
845
+ * shorter than a datagram of up to 43 bytes and between 43 bytes and one
846
+ * byte short of the datagram (at most 1200) otherwise, so it is always
847
+ * shorter; a datagram too short for that to leave 21 bytes, or one past
848
+ * the rate limit, gets nothing.
521
849
  */
522
- statelessReset(answer: QuicListenerAnswer, n: i32, dcid: u8[]): QuicListenerAnswer {
850
+ statelessReset(answer: QuicListenerAnswer, n: i32, datagram: u8[], dcidAt: i32, dcidLength: i32): void {
523
851
  if (n <= QUIC_LISTENER_MIN_RESET || !this.takeReset()) {
524
- return answer
852
+ return
525
853
  }
526
854
  let size: i32 = n - 1
527
855
  if (n > QUIC_LISTENER_SHORT_TRIGGER) {
528
856
  const most: i32 = n - 1 < QUIC_CONN_DATAGRAM_SIZE ? n - 1 : QUIC_CONN_DATAGRAM_SIZE
529
857
  const span: i32 = most - QUIC_LISTENER_SHORT_TRIGGER + 1
530
- const two: i32 = 2
531
- const pick: u8[] = this.random(two)
532
- const r: i32 = toI32(pick.length) > 1 ? (toI32(pick[0]) << 8) | toI32(pick[1]) : 0
858
+ const high: i32 = this.randomByte()
859
+ const r: i32 = (high << 8) | this.randomByte()
533
860
  size = QUIC_LISTENER_SHORT_TRIGGER + (span > 0 ? r % span : 0)
534
861
  }
535
- const reply: u8[] = this.random(size - QUIC_RESET_TOKEN_SIZE)
536
- if (toI32(reply.length) > 0) {
537
- reply[0] = toU8((toI32(reply[0]) & 0x3f) | 0x40)
538
- }
539
- quicListenerAppend(reply, quicStatelessResetToken(this.config.statelessResetKey, dcid))
862
+ const reply: u8[] = answer.reply
863
+ const tokenAt: i32 = size - QUIC_RESET_TOKEN_SIZE
864
+ quicListenerResize(reply, size)
865
+ this.randomInto(reply, QUIC_LISTENER_FROM, tokenAt)
866
+ reply[0] = toU8((toI32(reply[0]) & 0x3f) | 0x40)
867
+ quicConnResetTokenInto(
868
+ this.kdf.sha256,
869
+ this.config.statelessResetKey,
870
+ datagram,
871
+ dcidAt,
872
+ dcidLength,
873
+ reply,
874
+ tokenAt
875
+ )
540
876
  answer.kind = QUIC_LISTEN_STATELESS_RESET
541
- answer.reply = reply
542
- return answer
543
877
  }
544
878
  }
545
879
 
@@ -567,6 +901,24 @@ export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | nu
567
901
  export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
568
902
  now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
569
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
+
570
922
  /** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
571
923
  export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
572
924
 
@@ -582,8 +934,10 @@ export class QuicFlight {
582
934
  * Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
583
935
  * datagram after datagram, for one GSO send, and answers its length in
584
936
  * bytes (0 for none); `flight` says how many datagrams it holds and the
585
- * segment size. Each datagram goes only when the pacer has credit for a full
586
- * 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
587
941
  * `quicListenerTakePaced` does, so the flight is the pacer's whole credit
588
942
  * and no more. It stops at the first datagram shorter than
589
943
  * `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
@@ -601,6 +955,9 @@ export const quicListenerTakeFlight = (
601
955
  flight.count = 0
602
956
  flight.segment = 0
603
957
  let length: i32 = 0
958
+ if (conn.recovery.pacerDelay(now, quicListenerQuantum(conn)) > 0) {
959
+ return length
960
+ }
604
961
  while (
605
962
  flight.count < QUIC_LISTENER_FLIGHT_MAX &&
606
963
  length + QUIC_CONN_DATAGRAM_SIZE <= cap &&