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