@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.
- package/INSTALL.md +43 -25
- package/bin/nish +0 -0
- package/package.json +2 -2
- package/runtime/nish.d.ts +55 -5
- package/runtime/nish.h +231 -20
- package/runtime/nish.mjs +16 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +7 -6
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +213 -15
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +16 -3
- package/runtime/runtime.c +89 -23
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +225 -20
- package/std/README.md +86 -55
- package/std/bytes.ts +163 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +245 -53
- package/std/crypto/p256.ts +1376 -42
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +9 -9
- package/std/json-tokenizer.ts +1296 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1163 -0
- package/std/json.ts +1412 -70
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +417 -55
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +231 -45
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-conn-params.ts +86 -16
- package/std/net/quic-listener.ts +576 -219
- package/std/net/quic-packet.ts +443 -58
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +434 -28
- package/std/net/quic.ts +922 -206
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +204 -57
- package/std/net/tls.ts +1132 -305
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/text.ts +77 -0
- package/std/threads.ts +438 -14
package/std/net/quic-listener.ts
CHANGED
|
@@ -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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* if (
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `
|
|
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 {
|
|
99
|
-
import {
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
171
|
-
* `QUIC_LISTEN_*`, and `reply` the datagram to send back, empty when there
|
|
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.
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
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
|
-
/**
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
/**
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
/**
|
|
209
|
-
const
|
|
210
|
-
for (let k: i32 =
|
|
211
|
-
|
|
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
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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):
|
|
222
|
-
*
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 =
|
|
269
|
-
this.
|
|
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
|
-
/**
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
/**
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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,
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
323
|
-
|
|
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 &&
|
|
328
|
-
answer
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
345
|
-
if (
|
|
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
|
-
|
|
523
|
+
this.retry(answer, datagram, header, address)
|
|
524
|
+
return answer.kind
|
|
351
525
|
}
|
|
352
|
-
if (this.tokenValid(
|
|
526
|
+
if (this.tokenValid(datagram, header, address)) {
|
|
353
527
|
answer.kind = QUIC_LISTEN_ACCEPT
|
|
354
528
|
answer.retried = true
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
361
|
-
return answer
|
|
535
|
+
quicListenerRefill(answer.retryScid, datagram, header.dcidStart, header.dcidLength)
|
|
536
|
+
return answer.kind
|
|
362
537
|
}
|
|
363
|
-
|
|
538
|
+
this.invalidToken(answer, datagram, header)
|
|
539
|
+
return answer.kind
|
|
364
540
|
}
|
|
365
541
|
|
|
366
542
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
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
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
/**
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
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
|
-
|
|
402
|
-
|
|
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(
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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)
|
|
426
|
-
* connection ID for the client to send to, and a token
|
|
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[]):
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
)
|
|
439
|
-
|
|
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
|
-
|
|
697
|
+
} else {
|
|
698
|
+
quicListenerResize(reply, 0)
|
|
442
699
|
}
|
|
443
|
-
|
|
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)
|
|
449
|
-
* Initial keys of the DCID the client sent to,
|
|
450
|
-
* only when the client's Initial
|
|
451
|
-
* not noise; it is far smaller
|
|
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):
|
|
454
|
-
const secrets:
|
|
455
|
-
|
|
456
|
-
|
|
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 =
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
|
469
|
-
}
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
toI32(payload.length)
|
|
781
|
+
this.none
|
|
481
782
|
)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
805
|
+
} else {
|
|
806
|
+
quicListenerResize(reply, 0)
|
|
489
807
|
}
|
|
490
|
-
|
|
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
|
|
516
|
-
* 9000 §10.3): a
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
531
|
-
const
|
|
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[] =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
586
|
-
*
|
|
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 &&
|