@amritk/nish-x86_64-linux 0.16.0 → 0.17.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 +30 -7
- 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-wasm.c +1 -0
- package/runtime/runtime.c +46 -8
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +1 -3
- package/std/README.md +166 -35
- 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 +148 -27
- 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/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 +905 -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 +392 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +653 -0
- package/std/net/quic-packet.ts +1474 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1603 -0
- package/std/net/quic.ts +3214 -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 +410 -0
- package/std/net/tls.ts +1368 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
|
@@ -0,0 +1,653 @@
|
|
|
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 } from "nish/net/quic-listener";
|
|
11
|
+
*
|
|
12
|
+
* const listener = new QuicListener(config, entropy); // entropy: QUIC_LISTENER_ENTROPY_SIZE random bytes
|
|
13
|
+
* // For a datagram whose first packet no connection owns (`ownsConnectionId`):
|
|
14
|
+
* const answer: QuicListenerAnswer = listener.handle(datagram, fromAddress, now);
|
|
15
|
+
* if (answer.kind === QUIC_LISTEN_ACCEPT) {
|
|
16
|
+
* const conn = new QuicConnection(config, connectionEntropy);
|
|
17
|
+
* if (answer.retried) { conn.acceptRetry(answer.originalDcid, answer.retryScid); }
|
|
18
|
+
* conn.receive(datagram, now);
|
|
19
|
+
* } else if (toI32(answer.reply.length) > 0) {
|
|
20
|
+
* … send answer.reply back to fromAddress …
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* **Routing is the caller's.** The listener sees only datagrams the caller
|
|
24
|
+
* could not hand to a live connection, and it must see only those: a stateless
|
|
25
|
+
* reset carries the token for the packet's connection ID, and handing the
|
|
26
|
+
* listener a packet for a connection that is still alive would give that
|
|
27
|
+
* token, which ends the connection, to whoever sent it (RFC 9000 §21.11).
|
|
28
|
+
*
|
|
29
|
+
* **Retry tokens** (§8.1.4) are this server's own format: a marker byte, the
|
|
30
|
+
* time the token was issued, and the client's original Destination
|
|
31
|
+
* Connection ID, then 16 bytes of HMAC-SHA256 under the configuration's
|
|
32
|
+
* `retryTokenKey` over all of that, the Retry's Source Connection ID (which
|
|
33
|
+
* the client's next Initial must be sent to) and the client's address as the
|
|
34
|
+
* caller gives it, port included. So a token is good only from the address
|
|
35
|
+
* it was sent to, only for the connection it was issued for, and only for
|
|
36
|
+
* `retryTokenLifetime` milliseconds. A token that carries the marker and fails
|
|
37
|
+
* any of that is an invalid Retry token, which closes the attempt with
|
|
38
|
+
* INVALID_TOKEN (§8.1.2); a token without the marker, which some other
|
|
39
|
+
* server issued, is treated as no token at all (§8.1.3).
|
|
40
|
+
*
|
|
41
|
+
* **Stateless resets** (§10.3) end in the token `quicStatelessResetToken`
|
|
42
|
+
* derives from the configuration's static key and the packet's connection
|
|
43
|
+
* ID, so a server that restarted with the same key can end the connections
|
|
44
|
+
* it lost. RFC 9000 §10.3 and §10.3.3 bound them, and each bound is held
|
|
45
|
+
* here. A reset is sent only for a short header, and is always shorter than
|
|
46
|
+
* the datagram that caused it, so two endpoints resetting each other shrink
|
|
47
|
+
* to nothing; one byte shorter for a datagram of 43 bytes or fewer, and
|
|
48
|
+
* never under 21 bytes, so a datagram of 21 or fewer gets none. That also
|
|
49
|
+
* bounds amplification at less than one to one. They are rate limited:
|
|
50
|
+
* `QUIC_LISTENER_RESET_BURST` at once, then one per
|
|
51
|
+
* `QUIC_LISTENER_RESET_INTERVAL` milliseconds. Each reset's other bytes are
|
|
52
|
+
* unpredictable, from the listener's own generator.
|
|
53
|
+
*
|
|
54
|
+
* **Pacing** (RFC 9002 §7.7) is the carrier's send loop, so it is here
|
|
55
|
+
* beside the rest of what the carrier calls: `quicListenerTakePaced` takes a
|
|
56
|
+
* connection's next datagram only when its pacer has the credit for it, and
|
|
57
|
+
* `quicListenerPaceTime` says when it will. The pacer earns 5/4 of the
|
|
58
|
+
* congestion window per smoothed RTT and lets a burst of at most the initial
|
|
59
|
+
* window go at once, so a flight leaves spread over the round trip instead
|
|
60
|
+
* of in one burst that overruns a queue on the path.
|
|
61
|
+
*
|
|
62
|
+
* let out: u8[] | null = quicListenerTakePaced(conn, now);
|
|
63
|
+
* while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
|
|
64
|
+
* … and wake at the earlier of conn.deadline() and quicListenerPaceTime(conn, now)
|
|
65
|
+
*
|
|
66
|
+
* **GSO and GRO** (`nish:net`'s `UDP_SEGMENT` and `UDP_GRO`). A carrier that
|
|
67
|
+
* sends with segmentation offload takes a whole flight at once:
|
|
68
|
+
* `quicListenerTakeFlight` writes as many datagrams as the pacer has credit
|
|
69
|
+
* for back to back into one buffer, every one `QUIC_CONN_DATAGRAM_SIZE`
|
|
70
|
+
* bytes but the last, which is what one `udpSendTo` with that segment size
|
|
71
|
+
* cuts back into datagrams; each is written in place, so a flight allocates
|
|
72
|
+
* nothing. The flight is paced as a whole: it is as long as the pacer's
|
|
73
|
+
* credit, and charged to it datagram by datagram, so a burst is never more
|
|
74
|
+
* than the initial window however the carrier sends it. On the receiving
|
|
75
|
+
* side, `quicListenerReceiveSegments` hands a connection the datagrams of
|
|
76
|
+
* one GRO receive, segment by segment, in place.
|
|
77
|
+
*
|
|
78
|
+
* const flight = new QuicFlight();
|
|
79
|
+
* const n: i32 = quicListenerTakeFlight(conn, now, buf, 0, toI32(buf.length), flight);
|
|
80
|
+
* if (n > 0) { udpSendTo(fd, buf, 0, n, peer, flight.count > 1 ? flight.segment : 0, 0); }
|
|
81
|
+
*
|
|
82
|
+
* **Sans-IO and deterministic.** The caller gives the time, the client's
|
|
83
|
+
* address and, once, `QUIC_LISTENER_ENTROPY_SIZE` random bytes: every
|
|
84
|
+
* unpredictable byte the listener sends (a Retry's connection ID and unused
|
|
85
|
+
* bits, Version Negotiation's unused bits, a reset's padding and length) is
|
|
86
|
+
* HMAC-SHA256 under that seed of a counter. So a recorded exchange replays
|
|
87
|
+
* byte for byte.
|
|
88
|
+
*
|
|
89
|
+
* **Secrets.** The seed lives in the listener and the static keys in the
|
|
90
|
+
* caller's `QuicServerConfig`, for the server's lifetime, as plain bytes,
|
|
91
|
+
* since a `Secret` may not be a field (NL2430); QUIC-5 in
|
|
92
|
+
* `docs/security/quic.md` records it, with #430.
|
|
93
|
+
*
|
|
94
|
+
* Written from RFC 9000, in this module's own structure; nothing here is
|
|
95
|
+
* ported from another implementation. Private names carry the
|
|
96
|
+
* `quicListener` prefix (`docs/wp26-stdlib.md` §3e).
|
|
97
|
+
*/
|
|
98
|
+
import { timingSafeEqual } from "nish/crypto/ct"
|
|
99
|
+
import { hmacSha256 } from "nish/crypto/hmac"
|
|
100
|
+
import {
|
|
101
|
+
QUIC_AEAD_AES_128_GCM,
|
|
102
|
+
QUIC_ERR_VERSION,
|
|
103
|
+
QUIC_MAX_CID_LENGTH,
|
|
104
|
+
QUIC_PACKET_INITIAL,
|
|
105
|
+
QUIC_PACKET_OK,
|
|
106
|
+
QUIC_PACKET_SHORT,
|
|
107
|
+
QuicHeader,
|
|
108
|
+
QuicInitialSecrets,
|
|
109
|
+
QuicKeys,
|
|
110
|
+
QuicPacket,
|
|
111
|
+
quicInitialSecrets,
|
|
112
|
+
quicKeys,
|
|
113
|
+
quicLongHeader,
|
|
114
|
+
quicOpenPacket,
|
|
115
|
+
quicParseHeader,
|
|
116
|
+
quicRetryPacket,
|
|
117
|
+
quicSealPacket,
|
|
118
|
+
} from "nish/net/quic-packet"
|
|
119
|
+
import { QUIC_ERROR_INVALID_TOKEN, QUIC_RESET_TOKEN_SIZE, quicPushConnectionClose } from "nish/net/quic-frame"
|
|
120
|
+
import {
|
|
121
|
+
QUIC_CONN_CID_LENGTH,
|
|
122
|
+
QUIC_CONN_DATAGRAM_SIZE,
|
|
123
|
+
QUIC_CONN_STATIC_KEY_SIZE,
|
|
124
|
+
QuicConnection,
|
|
125
|
+
QuicServerConfig,
|
|
126
|
+
quicStatelessResetToken,
|
|
127
|
+
} from "nish/net/quic"
|
|
128
|
+
|
|
129
|
+
// ---- What `handle` answers ----------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** Nothing to send and no connection to make: the datagram is dropped. */
|
|
132
|
+
export const QUIC_LISTEN_DROP: i32 = 0
|
|
133
|
+
/** Make a connection and hand it the datagram; after a Retry, `acceptRetry` it first. */
|
|
134
|
+
export const QUIC_LISTEN_ACCEPT: i32 = 1
|
|
135
|
+
/** Send `reply`, a Version Negotiation packet (RFC 9000 §17.2.1). */
|
|
136
|
+
export const QUIC_LISTEN_VERSION_NEGOTIATION: i32 = 2
|
|
137
|
+
/** Send `reply`, a Retry packet with an address-validation token (§17.2.5). */
|
|
138
|
+
export const QUIC_LISTEN_RETRY: i32 = 3
|
|
139
|
+
/** Send `reply`, an Initial carrying CONNECTION_CLOSE with INVALID_TOKEN (§8.1.2). */
|
|
140
|
+
export const QUIC_LISTEN_INVALID_TOKEN: i32 = 4
|
|
141
|
+
/** Send `reply`, a stateless reset (§10.3). */
|
|
142
|
+
export const QUIC_LISTEN_STATELESS_RESET: i32 = 5
|
|
143
|
+
|
|
144
|
+
// ---- Sizes and limits -----------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/** How many random bytes the constructor takes: the seed of every unpredictable byte the listener sends. */
|
|
147
|
+
export const QUIC_LISTENER_ENTROPY_SIZE: i32 = 32
|
|
148
|
+
/** The most stateless resets the listener sends at once, before the rate limit applies. */
|
|
149
|
+
export const QUIC_LISTENER_RESET_BURST: i32 = 16
|
|
150
|
+
/** After the burst, one stateless reset per this many milliseconds. */
|
|
151
|
+
export const QUIC_LISTENER_RESET_INTERVAL: i64 = 100
|
|
152
|
+
/**
|
|
153
|
+
* The smallest stateless reset (RFC 9000 §10.3): a first byte, at least 38
|
|
154
|
+
* unpredictable bits after the fixed ones, and the 16-byte token.
|
|
155
|
+
*/
|
|
156
|
+
export const QUIC_LISTENER_MIN_RESET: i32 = 21
|
|
157
|
+
/** The longest a Retry token may be accepted for, in milliseconds: a client returns one within a round trip. */
|
|
158
|
+
export const QUIC_LISTENER_MAX_TOKEN_LIFETIME: i64 = 60000
|
|
159
|
+
|
|
160
|
+
/** The first byte of every Retry token this listener issues. */
|
|
161
|
+
const QUIC_LISTENER_TOKEN_MARKER: i32 = 0x52
|
|
162
|
+
/** A Retry token's MAC length: HMAC-SHA256 cut to 128 bits. */
|
|
163
|
+
const QUIC_LISTENER_TOKEN_MAC_SIZE: i32 = 16
|
|
164
|
+
/** A Retry token's fixed part before the original DCID: the marker, eight bytes of time, the DCID's length. */
|
|
165
|
+
const QUIC_LISTENER_TOKEN_HEAD: i32 = 10
|
|
166
|
+
/** Up to this datagram length a reset is exactly one byte shorter (RFC 9000 §10.3). */
|
|
167
|
+
const QUIC_LISTENER_SHORT_TRIGGER: i32 = 43
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* What `QuicListener.handle` decided about one datagram: `kind` is a
|
|
171
|
+
* `QUIC_LISTEN_*`, and `reply` the datagram to send back, empty when there is
|
|
172
|
+
* none. For `QUIC_LISTEN_ACCEPT` after a Retry, `retried` is set and
|
|
173
|
+
* `originalDcid` and `retryScid` are what `QuicConnection.acceptRetry` takes.
|
|
174
|
+
*/
|
|
175
|
+
export class QuicListenerAnswer {
|
|
176
|
+
reply: u8[]
|
|
177
|
+
originalDcid: u8[]
|
|
178
|
+
retryScid: u8[]
|
|
179
|
+
kind: i32 = 0
|
|
180
|
+
retried: boolean = false
|
|
181
|
+
|
|
182
|
+
constructor() {
|
|
183
|
+
this.reply = []
|
|
184
|
+
this.originalDcid = []
|
|
185
|
+
this.retryScid = []
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The bytes `bytes[from .. from + length)`, copied into an array of exactly that size. */
|
|
190
|
+
const quicListenerSlice = (bytes: u8[], from: i32, length: i32): u8[] => {
|
|
191
|
+
const out: u8[] = new Array<u8>(length)
|
|
192
|
+
const n: i32 = toI32(out.length)
|
|
193
|
+
for (let k: i32 = 0; k < n; k += 1) {
|
|
194
|
+
if (from + k >= 0 && from + k < toI32(bytes.length)) {
|
|
195
|
+
out[k] = bytes[from + k]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Appends every byte of `bytes` to `out`. */
|
|
202
|
+
const quicListenerAppend = (out: u8[], bytes: u8[]): void => {
|
|
203
|
+
for (const b of bytes) {
|
|
204
|
+
out.push(b)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Appends `value`'s low `size` bytes to `out`, big-endian. */
|
|
209
|
+
const quicListenerPushNumber = (out: u8[], value: i64, size: i32): void => {
|
|
210
|
+
for (let k: i32 = size - 1; k >= 0; k -= 1) {
|
|
211
|
+
out.push(toU8(toI32((value >> (toI64(k) * 8)) & 255)))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A Version Negotiation packet (RFC 9000 §17.2.1) answering a packet from
|
|
217
|
+
* `scid` to `dcid`: the IDs swapped, version 0, and version 1 as the only one
|
|
218
|
+
* supported. `unused` fills the first byte's low six bits, under the 0x40 the
|
|
219
|
+
* RFC asks for so the packet looks like QUIC to a demultiplexer.
|
|
220
|
+
*
|
|
221
|
+
* TODO(WP34 Q1): every other packet format is `nish/net/quic-packet`'s; this
|
|
222
|
+
* belongs beside `quicRetryPacket` there, which this stage may not edit.
|
|
223
|
+
*/
|
|
224
|
+
const quicListenerVersionNegotiation = (dcid: u8[], scid: u8[], unused: i32): u8[] => {
|
|
225
|
+
const out: u8[] = []
|
|
226
|
+
out.push(toU8((unused & 0x3f) | 0xc0))
|
|
227
|
+
quicListenerPushNumber(out, 0, 4)
|
|
228
|
+
out.push(toU8(toI32(scid.length)))
|
|
229
|
+
quicListenerAppend(out, scid)
|
|
230
|
+
out.push(toU8(toI32(dcid.length)))
|
|
231
|
+
quicListenerAppend(out, dcid)
|
|
232
|
+
quicListenerPushNumber(out, 1, 4)
|
|
233
|
+
return out
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The server's answers to datagrams no connection owns, under one
|
|
238
|
+
* `QuicServerConfig`. Make one when the server starts, with the same
|
|
239
|
+
* configuration its connections use, and hand it every such datagram with
|
|
240
|
+
* the address it came from and the time.
|
|
241
|
+
*
|
|
242
|
+
* A configuration whose `statelessResetKey` or `retryTokenKey` is not
|
|
243
|
+
* `QUIC_CONN_STATIC_KEY_SIZE` bytes, or whose `retryTokenLifetime` is outside
|
|
244
|
+
* 1 to `QUIC_LISTENER_MAX_TOKEN_LIFETIME`, or entropy of another length, make
|
|
245
|
+
* a listener that drops everything.
|
|
246
|
+
*/
|
|
247
|
+
export class QuicListener {
|
|
248
|
+
config: QuicServerConfig
|
|
249
|
+
seed: u8[]
|
|
250
|
+
/** Unpredictable bytes not yet used, from `poolHead`, and the counter the next block is made from. */
|
|
251
|
+
pool: u8[]
|
|
252
|
+
counter: i64 = 0
|
|
253
|
+
/** The latest time the caller gave, in milliseconds; time never runs backwards here. */
|
|
254
|
+
now: i64 = 0
|
|
255
|
+
/** When the reset budget was last topped up (-1: never yet), and the stateless resets that may go now. */
|
|
256
|
+
resetRefilled: i64 = -1
|
|
257
|
+
resetBudget: i32 = 0
|
|
258
|
+
poolHead: i32 = 0
|
|
259
|
+
/** Stateless resets sent, and ones the rate limit held back. */
|
|
260
|
+
resetsSent: i32 = 0
|
|
261
|
+
resetsLimited: i32 = 0
|
|
262
|
+
/** Whether the configuration and entropy were usable. */
|
|
263
|
+
usable: boolean = false
|
|
264
|
+
|
|
265
|
+
/** A listener under `config`; `entropy`'s `QUIC_LISTENER_ENTROPY_SIZE` bytes are copied, then wiped in the caller's array. */
|
|
266
|
+
constructor(config: QuicServerConfig, entropy: u8[]) {
|
|
267
|
+
this.config = config
|
|
268
|
+
this.seed = quicListenerSlice(entropy, 0, QUIC_LISTENER_ENTROPY_SIZE)
|
|
269
|
+
this.pool = []
|
|
270
|
+
this.usable =
|
|
271
|
+
toI32(entropy.length) === QUIC_LISTENER_ENTROPY_SIZE &&
|
|
272
|
+
toI32(config.statelessResetKey.length) === QUIC_CONN_STATIC_KEY_SIZE &&
|
|
273
|
+
toI32(config.retryTokenKey.length) === QUIC_CONN_STATIC_KEY_SIZE &&
|
|
274
|
+
config.retryTokenLifetime >= 1 &&
|
|
275
|
+
config.retryTokenLifetime <= QUIC_LISTENER_MAX_TOKEN_LIFETIME
|
|
276
|
+
secureZero(entropy)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** `count` unpredictable bytes: HMAC-SHA256 under the seed of a counter, a block at a time. */
|
|
280
|
+
random(count: i32): u8[] {
|
|
281
|
+
const out: u8[] = []
|
|
282
|
+
while (toI32(out.length) < count) {
|
|
283
|
+
if (this.poolHead >= toI32(this.pool.length)) {
|
|
284
|
+
secureZero(this.pool)
|
|
285
|
+
const block: u8[] = []
|
|
286
|
+
quicListenerPushNumber(block, this.counter, 8)
|
|
287
|
+
this.counter = this.counter + 1
|
|
288
|
+
this.pool = hmacSha256(this.seed, block)
|
|
289
|
+
this.poolHead = 0
|
|
290
|
+
}
|
|
291
|
+
if (this.poolHead >= 0 && this.poolHead < toI32(this.pool.length)) {
|
|
292
|
+
out.push(this.pool[this.poolHead])
|
|
293
|
+
this.pool[this.poolHead] = toU8(0)
|
|
294
|
+
}
|
|
295
|
+
this.poolHead = this.poolHead + 1
|
|
296
|
+
}
|
|
297
|
+
return out
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** One unpredictable value in 0 to 255. */
|
|
301
|
+
randomByte(): i32 {
|
|
302
|
+
const one: i32 = 1
|
|
303
|
+
const b: u8[] = this.random(one)
|
|
304
|
+
return toI32(b.length) > 0 ? toI32(b[0]) : 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Decides what to answer a datagram whose first packet no connection
|
|
309
|
+
* owns, from `address` (the client's address as the caller's socket
|
|
310
|
+
* reports it, port included) at `now` (the caller's monotonic time in
|
|
311
|
+
* milliseconds). See the module header for the rules; nothing a peer sends
|
|
312
|
+
* makes it panic.
|
|
313
|
+
*/
|
|
314
|
+
handle(datagram: u8[], address: u8[], now: i64): QuicListenerAnswer {
|
|
315
|
+
const answer: QuicListenerAnswer = new QuicListenerAnswer()
|
|
316
|
+
if (now > this.now) {
|
|
317
|
+
this.now = now
|
|
318
|
+
}
|
|
319
|
+
if (!this.usable) {
|
|
320
|
+
return answer
|
|
321
|
+
}
|
|
322
|
+
const n: i32 = toI32(datagram.length)
|
|
323
|
+
const header: QuicHeader = quicParseHeader(datagram, 0, QUIC_CONN_CID_LENGTH)
|
|
324
|
+
if (header.error === QUIC_ERR_VERSION) {
|
|
325
|
+
// §5.2.2: answer an unsupported version only in a datagram that could
|
|
326
|
+
// start a connection, and §6.1: never a Version Negotiation packet.
|
|
327
|
+
if (header.version !== 0 && n >= QUIC_CONN_DATAGRAM_SIZE) {
|
|
328
|
+
answer.kind = QUIC_LISTEN_VERSION_NEGOTIATION
|
|
329
|
+
answer.reply = quicListenerVersionNegotiation(header.dcid, header.scid, this.randomByte())
|
|
330
|
+
}
|
|
331
|
+
return answer
|
|
332
|
+
}
|
|
333
|
+
if (header.error !== QUIC_PACKET_OK) {
|
|
334
|
+
return answer
|
|
335
|
+
}
|
|
336
|
+
if (header.type === QUIC_PACKET_SHORT) {
|
|
337
|
+
return this.statelessReset(answer, n, header.dcid)
|
|
338
|
+
}
|
|
339
|
+
// §14.1, §7.2, §5.2.2: only an Initial in a full-sized datagram, sent to
|
|
340
|
+
// a DCID of at least 8 bytes, can start a connection; the rest is dropped.
|
|
341
|
+
if (header.type !== QUIC_PACKET_INITIAL || n < QUIC_CONN_DATAGRAM_SIZE || toI32(header.dcid.length) < 8) {
|
|
342
|
+
return answer
|
|
343
|
+
}
|
|
344
|
+
const token: u8[] = header.token
|
|
345
|
+
if (toI32(token.length) === 0 || toI32(token[0]) !== QUIC_LISTENER_TOKEN_MARKER) {
|
|
346
|
+
if (!this.config.retry) {
|
|
347
|
+
answer.kind = QUIC_LISTEN_ACCEPT
|
|
348
|
+
return answer
|
|
349
|
+
}
|
|
350
|
+
return this.retry(answer, header, address)
|
|
351
|
+
}
|
|
352
|
+
if (this.tokenValid(token, header.dcid, address)) {
|
|
353
|
+
answer.kind = QUIC_LISTEN_ACCEPT
|
|
354
|
+
answer.retried = true
|
|
355
|
+
answer.originalDcid = quicListenerSlice(
|
|
356
|
+
token,
|
|
357
|
+
QUIC_LISTENER_TOKEN_HEAD,
|
|
358
|
+
toI32(token[QUIC_LISTENER_TOKEN_HEAD - 1])
|
|
359
|
+
)
|
|
360
|
+
answer.retryScid = header.dcid
|
|
361
|
+
return answer
|
|
362
|
+
}
|
|
363
|
+
return this.invalidToken(answer, datagram, header)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The token's MAC over its own bytes before the MAC (`head`), the ID the
|
|
368
|
+
* client is to send to, and its address: what makes a token good for one
|
|
369
|
+
* address and one connection only.
|
|
370
|
+
*/
|
|
371
|
+
tokenMac(head: u8[], retryScid: u8[], address: u8[]): u8[] {
|
|
372
|
+
const message: u8[] = []
|
|
373
|
+
quicListenerAppend(message, head)
|
|
374
|
+
message.push(toU8(toI32(retryScid.length)))
|
|
375
|
+
quicListenerAppend(message, retryScid)
|
|
376
|
+
quicListenerPushNumber(message, toI64(toI32(address.length)), 4)
|
|
377
|
+
quicListenerAppend(message, address)
|
|
378
|
+
const mac: u8[] = hmacSha256(this.config.retryTokenKey, message)
|
|
379
|
+
const out: u8[] = quicListenerSlice(mac, 0, QUIC_LISTENER_TOKEN_MAC_SIZE)
|
|
380
|
+
secureZero(mac)
|
|
381
|
+
return out
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** A Retry token for the client at `address`, whose first Initial went to `odcid`, now told to send to `retryScid`. */
|
|
385
|
+
issueToken(odcid: u8[], retryScid: u8[], address: u8[]): u8[] {
|
|
386
|
+
const token: u8[] = []
|
|
387
|
+
token.push(toU8(QUIC_LISTENER_TOKEN_MARKER))
|
|
388
|
+
quicListenerPushNumber(token, this.now, 8)
|
|
389
|
+
token.push(toU8(toI32(odcid.length)))
|
|
390
|
+
quicListenerAppend(token, odcid)
|
|
391
|
+
quicListenerAppend(token, this.tokenMac(token, retryScid, address))
|
|
392
|
+
return token
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Whether `token` is one this listener issued for the client at `address`
|
|
397
|
+
* that was told to send to `dcid`, and still within its lifetime: the
|
|
398
|
+
* lengths add up, the MAC matches (compared in constant time), and it was
|
|
399
|
+
* issued no later than now and less than `retryTokenLifetime` ago.
|
|
400
|
+
*/
|
|
401
|
+
tokenValid(token: u8[], dcid: u8[], address: u8[]): boolean {
|
|
402
|
+
const length: i32 = toI32(token.length)
|
|
403
|
+
if (length < QUIC_LISTENER_TOKEN_HEAD + QUIC_LISTENER_TOKEN_MAC_SIZE) {
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
const odcidLength: i32 = toI32(token[QUIC_LISTENER_TOKEN_HEAD - 1])
|
|
407
|
+
const head: i32 = QUIC_LISTENER_TOKEN_HEAD + odcidLength
|
|
408
|
+
if (odcidLength > QUIC_MAX_CID_LENGTH || head + QUIC_LISTENER_TOKEN_MAC_SIZE !== length) {
|
|
409
|
+
return false
|
|
410
|
+
}
|
|
411
|
+
const mac: u8[] = this.tokenMac(quicListenerSlice(token, 0, head), dcid, address)
|
|
412
|
+
if (!timingSafeEqual(mac, quicListenerSlice(token, head, QUIC_LISTENER_TOKEN_MAC_SIZE))) {
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
let issued: i64 = 0
|
|
416
|
+
for (let k: i32 = 1; k < QUIC_LISTENER_TOKEN_HEAD - 1; k += 1) {
|
|
417
|
+
if (k < toI32(token.length)) {
|
|
418
|
+
issued = (issued << 8) | toI64(token[k])
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return issued <= this.now && this.now - issued < this.config.retryTokenLifetime
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A Retry for the client's first Initial (RFC 9000 §8.1.2): a new
|
|
426
|
+
* connection ID for the client to send to, and a token binding it, the
|
|
427
|
+
* original DCID and the client's address to now.
|
|
428
|
+
*/
|
|
429
|
+
retry(answer: QuicListenerAnswer, header: QuicHeader, address: u8[]): QuicListenerAnswer {
|
|
430
|
+
const retryScid: u8[] = this.random(QUIC_CONN_CID_LENGTH)
|
|
431
|
+
const token: u8[] = this.issueToken(header.dcid, retryScid, address)
|
|
432
|
+
const packet: u8[] | null = quicRetryPacket(
|
|
433
|
+
header.scid,
|
|
434
|
+
retryScid,
|
|
435
|
+
token,
|
|
436
|
+
header.dcid,
|
|
437
|
+
this.randomByte() & 15
|
|
438
|
+
)
|
|
439
|
+
if (packet !== null) {
|
|
440
|
+
answer.kind = QUIC_LISTEN_RETRY
|
|
441
|
+
answer.reply = packet
|
|
442
|
+
}
|
|
443
|
+
return answer
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The answer to an Initial carrying an invalid Retry token (RFC 9000
|
|
448
|
+
* §8.1.2): an Initial with CONNECTION_CLOSE and INVALID_TOKEN, under the
|
|
449
|
+
* Initial keys of the DCID the client sent to, from that DCID. It is sent
|
|
450
|
+
* only when the client's Initial authenticates, so it answers a client and
|
|
451
|
+
* not noise; it is far smaller than the datagram it answers.
|
|
452
|
+
*/
|
|
453
|
+
invalidToken(answer: QuicListenerAnswer, datagram: u8[], header: QuicHeader): QuicListenerAnswer {
|
|
454
|
+
const secrets: QuicInitialSecrets | null = quicInitialSecrets(header.dcid)
|
|
455
|
+
if (secrets === null) {
|
|
456
|
+
return answer
|
|
457
|
+
}
|
|
458
|
+
const read: QuicKeys | null = quicKeys(QUIC_AEAD_AES_128_GCM, secrets.client)
|
|
459
|
+
if (read === null) {
|
|
460
|
+
return answer
|
|
461
|
+
}
|
|
462
|
+
const opened: QuicPacket = quicOpenPacket(read, datagram, header, -1)
|
|
463
|
+
// The write keys are derived only for an Initial that opened, so a forged
|
|
464
|
+
// token on noise costs one derivation, not two.
|
|
465
|
+
const write: QuicKeys | null =
|
|
466
|
+
opened.error === QUIC_PACKET_OK ? quicKeys(QUIC_AEAD_AES_128_GCM, secrets.server) : null
|
|
467
|
+
if (write === null) {
|
|
468
|
+
return answer
|
|
469
|
+
}
|
|
470
|
+
const payload: u8[] = []
|
|
471
|
+
const none: u8[] = []
|
|
472
|
+
quicPushConnectionClose(payload, false, QUIC_ERROR_INVALID_TOKEN, 0, none)
|
|
473
|
+
const first: u8[] | null = quicLongHeader(
|
|
474
|
+
QUIC_PACKET_INITIAL,
|
|
475
|
+
header.scid,
|
|
476
|
+
header.dcid,
|
|
477
|
+
none,
|
|
478
|
+
0,
|
|
479
|
+
1,
|
|
480
|
+
toI32(payload.length)
|
|
481
|
+
)
|
|
482
|
+
if (first === null) {
|
|
483
|
+
return answer
|
|
484
|
+
}
|
|
485
|
+
const packet: u8[] | null = quicSealPacket(write, first, 0, payload)
|
|
486
|
+
if (packet !== null) {
|
|
487
|
+
answer.kind = QUIC_LISTEN_INVALID_TOKEN
|
|
488
|
+
answer.reply = packet
|
|
489
|
+
}
|
|
490
|
+
return answer
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Takes one stateless reset from the rate limit's budget, topping it up first. Answers whether one was there. */
|
|
494
|
+
takeReset(): boolean {
|
|
495
|
+
if (this.resetRefilled < 0) {
|
|
496
|
+
this.resetRefilled = this.now
|
|
497
|
+
this.resetBudget = QUIC_LISTENER_RESET_BURST
|
|
498
|
+
}
|
|
499
|
+
const earned: i64 = (this.now - this.resetRefilled) / QUIC_LISTENER_RESET_INTERVAL
|
|
500
|
+
if (earned > 0) {
|
|
501
|
+
const room: i64 = toI64(QUIC_LISTENER_RESET_BURST - this.resetBudget)
|
|
502
|
+
this.resetBudget = this.resetBudget + toI32(earned < room ? earned : room)
|
|
503
|
+
this.resetRefilled = this.resetRefilled + earned * QUIC_LISTENER_RESET_INTERVAL
|
|
504
|
+
}
|
|
505
|
+
if (this.resetBudget <= 0) {
|
|
506
|
+
this.resetsLimited = this.resetsLimited + 1
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
this.resetBudget = this.resetBudget - 1
|
|
510
|
+
this.resetsSent = this.resetsSent + 1
|
|
511
|
+
return true
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* A stateless reset answering an `n`-byte datagram sent to `dcid` (RFC
|
|
516
|
+
* 9000 §10.3): a short-header first byte, unpredictable bytes, and the
|
|
517
|
+
* token for `dcid`. It is one byte shorter than a datagram of up to 43
|
|
518
|
+
* bytes and between 43 bytes and one byte short of the datagram (at most
|
|
519
|
+
* 1200) otherwise, so it is always shorter; a datagram too short for that
|
|
520
|
+
* to leave 21 bytes, or one past the rate limit, gets nothing.
|
|
521
|
+
*/
|
|
522
|
+
statelessReset(answer: QuicListenerAnswer, n: i32, dcid: u8[]): QuicListenerAnswer {
|
|
523
|
+
if (n <= QUIC_LISTENER_MIN_RESET || !this.takeReset()) {
|
|
524
|
+
return answer
|
|
525
|
+
}
|
|
526
|
+
let size: i32 = n - 1
|
|
527
|
+
if (n > QUIC_LISTENER_SHORT_TRIGGER) {
|
|
528
|
+
const most: i32 = n - 1 < QUIC_CONN_DATAGRAM_SIZE ? n - 1 : QUIC_CONN_DATAGRAM_SIZE
|
|
529
|
+
const span: i32 = most - QUIC_LISTENER_SHORT_TRIGGER + 1
|
|
530
|
+
const two: i32 = 2
|
|
531
|
+
const pick: u8[] = this.random(two)
|
|
532
|
+
const r: i32 = toI32(pick.length) > 1 ? (toI32(pick[0]) << 8) | toI32(pick[1]) : 0
|
|
533
|
+
size = QUIC_LISTENER_SHORT_TRIGGER + (span > 0 ? r % span : 0)
|
|
534
|
+
}
|
|
535
|
+
const reply: u8[] = this.random(size - QUIC_RESET_TOKEN_SIZE)
|
|
536
|
+
if (toI32(reply.length) > 0) {
|
|
537
|
+
reply[0] = toU8((toI32(reply[0]) & 0x3f) | 0x40)
|
|
538
|
+
}
|
|
539
|
+
quicListenerAppend(reply, quicStatelessResetToken(this.config.statelessResetKey, dcid))
|
|
540
|
+
answer.kind = QUIC_LISTEN_STATELESS_RESET
|
|
541
|
+
answer.reply = reply
|
|
542
|
+
return answer
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The next datagram `conn` sends at `now`, as `takeDatagram` answers it,
|
|
548
|
+
* but only when the connection's pacer has the credit for a full datagram
|
|
549
|
+
* (RFC 9002 §7.7); `null` when it has not, or when nothing is due. The
|
|
550
|
+
* datagram is charged to the pacer at its real size.
|
|
551
|
+
*/
|
|
552
|
+
export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | null => {
|
|
553
|
+
if (conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE) > 0) {
|
|
554
|
+
return null
|
|
555
|
+
}
|
|
556
|
+
const out: u8[] | null = conn.takeDatagram(now)
|
|
557
|
+
if (out !== null) {
|
|
558
|
+
conn.recovery.onPaced(now, toI32(out.length))
|
|
559
|
+
}
|
|
560
|
+
return out
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* When `quicListenerTakePaced` next lets `conn` send a full datagram, in the
|
|
565
|
+
* caller's milliseconds: `now` when it may already.
|
|
566
|
+
*/
|
|
567
|
+
export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
|
|
568
|
+
now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
|
|
569
|
+
|
|
570
|
+
/** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
|
|
571
|
+
export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
|
|
572
|
+
|
|
573
|
+
/** What `quicListenerTakeFlight` wrote: how many datagrams, and the segment size to send them with. */
|
|
574
|
+
export class QuicFlight {
|
|
575
|
+
/** The datagrams in the flight. */
|
|
576
|
+
count: i32 = 0
|
|
577
|
+
/** Every datagram's size but the last's, which may be shorter: the `UDP_SEGMENT` to send with. */
|
|
578
|
+
segment: i32 = 0
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
|
|
583
|
+
* datagram after datagram, for one GSO send, and answers its length in
|
|
584
|
+
* bytes (0 for none); `flight` says how many datagrams it holds and the
|
|
585
|
+
* segment size. Each datagram goes only when the pacer has credit for a full
|
|
586
|
+
* one (RFC 9002 §7.7) and is charged at its real size, as
|
|
587
|
+
* `quicListenerTakePaced` does, so the flight is the pacer's whole credit
|
|
588
|
+
* and no more. It stops at the first datagram shorter than
|
|
589
|
+
* `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
|
|
590
|
+
* at `QUIC_LISTENER_FLIGHT_MAX` datagrams, and when `cap` has no room for
|
|
591
|
+
* another.
|
|
592
|
+
*/
|
|
593
|
+
export const quicListenerTakeFlight = (
|
|
594
|
+
conn: QuicConnection,
|
|
595
|
+
now: i64,
|
|
596
|
+
buf: u8[],
|
|
597
|
+
at: i32,
|
|
598
|
+
cap: i32,
|
|
599
|
+
flight: QuicFlight
|
|
600
|
+
): i32 => {
|
|
601
|
+
flight.count = 0
|
|
602
|
+
flight.segment = 0
|
|
603
|
+
let length: i32 = 0
|
|
604
|
+
while (
|
|
605
|
+
flight.count < QUIC_LISTENER_FLIGHT_MAX &&
|
|
606
|
+
length + QUIC_CONN_DATAGRAM_SIZE <= cap &&
|
|
607
|
+
conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE) <= 0
|
|
608
|
+
) {
|
|
609
|
+
const n: i32 = conn.takeDatagramInto(buf, at + length, now)
|
|
610
|
+
if (n <= 0) {
|
|
611
|
+
return length
|
|
612
|
+
}
|
|
613
|
+
conn.recovery.onPaced(now, n)
|
|
614
|
+
if (flight.count === 0) {
|
|
615
|
+
flight.segment = n
|
|
616
|
+
}
|
|
617
|
+
flight.count = flight.count + 1
|
|
618
|
+
length = length + n
|
|
619
|
+
if (n < QUIC_CONN_DATAGRAM_SIZE) {
|
|
620
|
+
return length
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return length
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Hands `conn` the datagrams of one receive, `buf[off .. off + len)`, at
|
|
628
|
+
* `now`: one datagram when `segment` is 0, else a GRO receive cut every
|
|
629
|
+
* `segment` bytes (the last may be shorter). Each is read in place. Answers
|
|
630
|
+
* how many it handed over.
|
|
631
|
+
*/
|
|
632
|
+
export const quicListenerReceiveSegments = (
|
|
633
|
+
conn: QuicConnection,
|
|
634
|
+
buf: u8[],
|
|
635
|
+
off: i32,
|
|
636
|
+
len: i32,
|
|
637
|
+
segment: i32,
|
|
638
|
+
now: i64
|
|
639
|
+
): i32 => {
|
|
640
|
+
if (segment <= 0) {
|
|
641
|
+
conn.receiveWindow(buf, off, len, now)
|
|
642
|
+
return 1
|
|
643
|
+
}
|
|
644
|
+
let count: i32 = 0
|
|
645
|
+
let at: i32 = 0
|
|
646
|
+
while (at < len) {
|
|
647
|
+
const n: i32 = len - at < segment ? len - at : segment
|
|
648
|
+
conn.receiveWindow(buf, off + at, n, now)
|
|
649
|
+
at = at + n
|
|
650
|
+
count += 1
|
|
651
|
+
}
|
|
652
|
+
return count
|
|
653
|
+
}
|