@amritk/nish-x86_64-linux 0.18.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 +215 -18
- 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 +4 -4
- package/runtime/runtime-wasm.c +5 -3
- package/runtime/runtime.c +69 -13
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +223 -22
- package/std/README.md +81 -51
- 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 +932 -61
- 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 +1120 -34
- 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 +211 -31
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +88 -7
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- 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 +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/threads.ts +438 -14
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/prefork` — run a server on every core: a supervisor process that
|
|
3
|
+
* starts one worker process per core, restarts one that dies, and stops them
|
|
4
|
+
* all on SIGTERM or SIGINT (`docs/wp42-multicore-server.md` §2.3).
|
|
5
|
+
*
|
|
6
|
+
* import { preforkWorkers } from "nish/net/prefork";
|
|
7
|
+
*
|
|
8
|
+
* export const main = (): number => {
|
|
9
|
+
* const worker: i32 = preforkWorkers(process.argv);
|
|
10
|
+
* if (worker < 0) { return worker === -1 ? 0 : 1; } // the supervisor, done
|
|
11
|
+
* const listener = tcpListenFlags("0.0.0.0", 8080, 1024, 1); // SO_REUSEPORT
|
|
12
|
+
* // ... the single-threaded readiness loop, unchanged ...
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* **One program, two roles.** `preforkWorkers(process.argv)` answers in two kinds of
|
|
16
|
+
* process. In a worker it answers the worker's index, `0` to `N - 1`, at once,
|
|
17
|
+
* and the program goes on to bind its listener and serve. In the process the
|
|
18
|
+
* user started it supervises, and answers only once every worker has exited:
|
|
19
|
+
* `-1` after a shutdown, `-2` when the workers could not be started or the
|
|
20
|
+
* supervisor could not listen for signals. Each worker binds its own listener
|
|
21
|
+
* with `tcpListenFlags`' flag 1 (`SO_REUSEPORT`), so on Linux the kernel
|
|
22
|
+
* spreads new connections across them and no state is shared:
|
|
23
|
+
* a worker is the single-threaded server it was before, `Arena.used()` flat
|
|
24
|
+
* per worker.
|
|
25
|
+
*
|
|
26
|
+
* **How a worker is told.** The supervisor runs the program again with
|
|
27
|
+
* `--nish-prefork-worker=<index>` appended to its arguments, through `spawn`;
|
|
28
|
+
* on Linux it runs `/proc/self/exe`, so a program started through `PATH` or a
|
|
29
|
+
* relative path finds itself. `preforkArgs(process.argv)` answers the
|
|
30
|
+
* arguments without that flag, for a worker that reads its own; the module
|
|
31
|
+
* takes `process.argv` from its caller because only a program with a `main`
|
|
32
|
+
* has one. The environment is inherited whole, and every descriptor the
|
|
33
|
+
* supervisor holds is close-on-exec, so a worker starts with none of them
|
|
34
|
+
* (`docs/security/runtime.md`, RT-17, RT-19).
|
|
35
|
+
*
|
|
36
|
+
* **How many.** `NISH_WORKERS` when it is a positive whole number, else
|
|
37
|
+
* `PreforkConfig.workers` when that is positive, else
|
|
38
|
+
* `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
|
|
39
|
+
* `SO_REUSEPORT` port without spreading connections across it. The count has
|
|
40
|
+
* no upper bound, so `NISH_WORKERS` is the operator's to set, not a client's.
|
|
41
|
+
*
|
|
42
|
+
* **Restarts.** A worker that exits, by a signal or with any code, is started
|
|
43
|
+
* again at the same index while the supervisor is not stopping. One that dies
|
|
44
|
+
* within `quickExitMs` of starting waits before its restart: `backoffMs` the
|
|
45
|
+
* first time, twice the last wait after that, up to `backoffCapMs`; one that
|
|
46
|
+
* ran longer starts again at once and clears its wait, so a crash loop costs a
|
|
47
|
+
* spawn every `backoffCapMs`, not a core. A restart `spawn` refuses waits on
|
|
48
|
+
* the same backoff and is tried again; only the first start of a worker that
|
|
49
|
+
* fails ends the supervisor, with `PREFORK_SPAWN_FAILED`.
|
|
50
|
+
*
|
|
51
|
+
* **Shutdown.** The supervisor listens through `signalFd()`, made before the
|
|
52
|
+
* first spawn so no signal falls between. On SIGTERM or SIGINT it stops
|
|
53
|
+
* restarting, sends every worker SIGTERM, reaps them for `graceMs`, sends what
|
|
54
|
+
* is left SIGKILL and reaps that. A second signal during the grace period sends
|
|
55
|
+
* SIGKILL at once. The workers get the default dispositions back from `exec`,
|
|
56
|
+
* so a worker that never asked for `signalFd()` ends on the SIGTERM. A
|
|
57
|
+
* supervisor that dies any other way (SIGKILL, the OOM killer, or the default
|
|
58
|
+
* action of SIGHUP or SIGQUIT, which `signalFd()` does not catch) leaves its
|
|
59
|
+
* workers running, reparented to init.
|
|
60
|
+
*
|
|
61
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
62
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
63
|
+
*/
|
|
64
|
+
import { netClose, pollAdd, pollCreate, pollWait } from "nish:net"
|
|
65
|
+
import {
|
|
66
|
+
availableParallelism,
|
|
67
|
+
getenv,
|
|
68
|
+
kill,
|
|
69
|
+
monotonicNanos,
|
|
70
|
+
platform,
|
|
71
|
+
readSignal,
|
|
72
|
+
signalFd,
|
|
73
|
+
spawn,
|
|
74
|
+
waitPid,
|
|
75
|
+
} from "nish:process"
|
|
76
|
+
|
|
77
|
+
/** The argument a worker is started with, its index after the `=`. */
|
|
78
|
+
export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
|
|
79
|
+
|
|
80
|
+
/** What `preforkWith` answers in the supervisor once its workers are gone. */
|
|
81
|
+
export const PREFORK_STOPPED: i32 = -1
|
|
82
|
+
/**
|
|
83
|
+
* What `preforkWith` answers when a worker could not be started, or when the
|
|
84
|
+
* supervisor could not listen for SIGTERM and so starts none.
|
|
85
|
+
*/
|
|
86
|
+
export const PREFORK_SPAWN_FAILED: i32 = -2
|
|
87
|
+
|
|
88
|
+
/** The supervisor's numbers; every one is checked by `preforkWith`. */
|
|
89
|
+
export class PreforkConfig {
|
|
90
|
+
/** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
|
|
91
|
+
workers: i32 = 0
|
|
92
|
+
/** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
|
|
93
|
+
tickMs: i32 = 100
|
|
94
|
+
/** The first wait before restarting a worker that died young. */
|
|
95
|
+
backoffMs: i32 = 100
|
|
96
|
+
/** The longest wait before a restart. */
|
|
97
|
+
backoffCapMs: i32 = 5000
|
|
98
|
+
/** A worker that exits within this many milliseconds of starting died young. */
|
|
99
|
+
quickExitMs: i32 = 1000
|
|
100
|
+
/** How long the workers get to exit after SIGTERM before SIGKILL. */
|
|
101
|
+
graceMs: i32 = 5000
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when `s` is one or more ASCII digits and nothing else. */
|
|
105
|
+
const preforkDigits = (s: string): boolean => {
|
|
106
|
+
if (s.length === 0 || s.length > 9) {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
for (let i: i32 = 0; i < toI32(s.length); i++) {
|
|
110
|
+
const c: i32 = toI32(s.charCodeAt(i))
|
|
111
|
+
if (c < 48 || c > 57) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* How many workers to start: `env` (`NISH_WORKERS`, `null` when unset) when it
|
|
120
|
+
* is a positive whole number, else `requested` when positive, else the
|
|
121
|
+
* platform's default.
|
|
122
|
+
*/
|
|
123
|
+
export const preforkWorkerCount = (requested: i32, env: string | null): i32 => {
|
|
124
|
+
if (env !== null && preforkDigits(env)) {
|
|
125
|
+
const n: i32 = parseInt(env)
|
|
126
|
+
if (n > 0) {
|
|
127
|
+
return n
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (requested > 0) {
|
|
131
|
+
return requested
|
|
132
|
+
}
|
|
133
|
+
if (platform !== "linux") {
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
136
|
+
const cores: i32 = availableParallelism()
|
|
137
|
+
return cores > 0 ? cores : 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The worker index `args` carries in its last element, or -1 when none. */
|
|
141
|
+
export const preforkIndex = (args: string[]): i32 => {
|
|
142
|
+
if (args.length < 2) {
|
|
143
|
+
return -1
|
|
144
|
+
}
|
|
145
|
+
const last: string = args[args.length - 1]
|
|
146
|
+
if (!last.startsWith(PREFORK_WORKER_FLAG)) {
|
|
147
|
+
return -1
|
|
148
|
+
}
|
|
149
|
+
const digits: string = last.substring(PREFORK_WORKER_FLAG.length)
|
|
150
|
+
return preforkDigits(digits) ? parseInt(digits) : -1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `args` (`process.argv`) without the worker flag, the same in every process. */
|
|
154
|
+
export const preforkArgs = (args: string[]): string[] => {
|
|
155
|
+
const keep: i32 = preforkIndex(args) >= 0 ? toI32(args.length) - 1 : toI32(args.length)
|
|
156
|
+
const out: string[] = []
|
|
157
|
+
for (let i: i32 = 0; i < toI32(args.length); i++) {
|
|
158
|
+
if (i < keep) {
|
|
159
|
+
out.push(args[i])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Milliseconds on the monotonic clock. */
|
|
166
|
+
const preforkNowMs = (): i64 => monotonicNanos() / 1000000
|
|
167
|
+
|
|
168
|
+
/** The signals the supervisor stops its workers with. */
|
|
169
|
+
const PREFORK_SIGTERM: i32 = 15
|
|
170
|
+
const PREFORK_SIGKILL: i32 = 9
|
|
171
|
+
|
|
172
|
+
/** One worker's slot in the supervisor. */
|
|
173
|
+
class PreforkSlot {
|
|
174
|
+
/** When the running worker started, or when the waiting one may start. */
|
|
175
|
+
at: i64 = 0
|
|
176
|
+
/** The running worker's pid, 0 while it waits to be started again. */
|
|
177
|
+
pid: i32 = 0
|
|
178
|
+
/** The wait before the last restart, 0 after a worker that ran long. */
|
|
179
|
+
delay: i32 = 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The supervisor's state while it runs. */
|
|
183
|
+
class PreforkSupervisor {
|
|
184
|
+
config: PreforkConfig
|
|
185
|
+
program: string
|
|
186
|
+
args: string[]
|
|
187
|
+
slots: PreforkSlot[]
|
|
188
|
+
running: i32 = 0
|
|
189
|
+
|
|
190
|
+
constructor(config: PreforkConfig, args: string[], count: i32) {
|
|
191
|
+
this.config = config
|
|
192
|
+
this.args = preforkArgs(args)
|
|
193
|
+
const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
|
|
194
|
+
this.program = self !== null ? self : this.args[0]
|
|
195
|
+
this.slots = []
|
|
196
|
+
for (let i: i32 = 0; i < count; i++) {
|
|
197
|
+
this.slots.push(new PreforkSlot())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Start worker `index`; false when `spawn` refused. */
|
|
202
|
+
start(index: i32): boolean {
|
|
203
|
+
const argv: string[] = [this.program]
|
|
204
|
+
for (let i: i32 = 1; i < toI32(this.args.length); i++) {
|
|
205
|
+
argv.push(this.args[i])
|
|
206
|
+
}
|
|
207
|
+
argv.push(`${PREFORK_WORKER_FLAG}${index}`)
|
|
208
|
+
const pid: i32 = spawn(argv)
|
|
209
|
+
if (pid <= 0) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
const slot: PreforkSlot = this.slots[index]
|
|
213
|
+
slot.pid = pid
|
|
214
|
+
slot.at = preforkNowMs()
|
|
215
|
+
this.running = this.running + 1
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The slot `pid` ran in, or -1. */
|
|
220
|
+
find(pid: i32): i32 {
|
|
221
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
222
|
+
if (this.slots[i].pid === pid) {
|
|
223
|
+
return i
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return -1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Give `slot` the time it may start again: at once after a worker that ran
|
|
231
|
+
* at least `quickExitMs`, else, or when `failed` (`spawn` refused it), after
|
|
232
|
+
* a wait that starts at `backoffMs` and doubles up to `backoffCapMs`.
|
|
233
|
+
*/
|
|
234
|
+
backoff(slot: PreforkSlot, now: i64, failed: boolean): void {
|
|
235
|
+
if (failed || now - slot.at < toI64(this.config.quickExitMs)) {
|
|
236
|
+
const cap: i32 = this.config.backoffCapMs
|
|
237
|
+
if (slot.delay === 0) {
|
|
238
|
+
slot.delay = this.config.backoffMs
|
|
239
|
+
} else {
|
|
240
|
+
// Compared before doubling, so a cap near the i32 limit cannot overflow.
|
|
241
|
+
slot.delay = slot.delay > cap / 2 ? cap : slot.delay * 2
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
slot.delay = 0
|
|
245
|
+
}
|
|
246
|
+
slot.at = now + toI64(slot.delay)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reap every worker that has exited. Unless `stopping`, each one's slot is
|
|
251
|
+
* given the time it may start again. A child that is not a worker (one the
|
|
252
|
+
* program spawned itself before calling `preforkWith`) is reaped too, and
|
|
253
|
+
* its status is discarded. Answers false when `waitPid` fails, which means
|
|
254
|
+
* no child is left to wait for.
|
|
255
|
+
*/
|
|
256
|
+
reap(stopping: boolean): boolean {
|
|
257
|
+
while (this.running > 0) {
|
|
258
|
+
const done: i32[] = waitPid(-1, 1)
|
|
259
|
+
if (done[0] === -4) {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if (done[0] <= 0) {
|
|
263
|
+
return done[0] === 0
|
|
264
|
+
}
|
|
265
|
+
const index: i32 = this.find(done[0])
|
|
266
|
+
if (index < 0) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
const slot: PreforkSlot = this.slots[index]
|
|
270
|
+
this.running = this.running - 1
|
|
271
|
+
slot.pid = 0
|
|
272
|
+
if (!stopping) {
|
|
273
|
+
this.backoff(slot, preforkNowMs(), false)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Start every waiting worker whose time has come. When `first`, answer false
|
|
281
|
+
* at the first `spawn` that fails; otherwise a failed start waits on the
|
|
282
|
+
* same backoff as a worker that exited at once, and the answer is true.
|
|
283
|
+
*/
|
|
284
|
+
restart(first: boolean): boolean {
|
|
285
|
+
const now: i64 = preforkNowMs()
|
|
286
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
287
|
+
const slot: PreforkSlot = this.slots[i]
|
|
288
|
+
if (slot.pid === 0 && slot.at <= now && !this.start(i)) {
|
|
289
|
+
if (first) {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
console.error(`nish/net/prefork: cannot restart worker ${i}`)
|
|
293
|
+
this.backoff(slot, now, true)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Send `sig` to every running worker. */
|
|
300
|
+
signal(sig: i32): void {
|
|
301
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
302
|
+
if (this.slots[i].pid > 0) {
|
|
303
|
+
kill(this.slots[i].pid, sig)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* SIGTERM, reap until `graceMs` or a second signal, SIGKILL, reap until no
|
|
310
|
+
* worker is left. Two signals that arrive before the supervisor reads the
|
|
311
|
+
* first count as the second here too. Every reap goes through `reap`, so a
|
|
312
|
+
* child that is not a worker is skipped in both phases, and the SIGKILL
|
|
313
|
+
* phase ends only when the last worker is reaped or no child is left.
|
|
314
|
+
*/
|
|
315
|
+
stop(loop: i32, signals: i32, ready: i32[]): void {
|
|
316
|
+
this.signal(PREFORK_SIGTERM)
|
|
317
|
+
const deadline: i64 = preforkNowMs() + toI64(this.config.graceMs)
|
|
318
|
+
this.reap(true)
|
|
319
|
+
let forced = false
|
|
320
|
+
while (this.running > 0 && !forced && preforkNowMs() < deadline) {
|
|
321
|
+
const n: i32 = pollWait(loop, ready, this.config.tickMs)
|
|
322
|
+
if (n > 0) {
|
|
323
|
+
readSignal(signals)
|
|
324
|
+
forced = true
|
|
325
|
+
}
|
|
326
|
+
this.reap(true)
|
|
327
|
+
}
|
|
328
|
+
if (this.running > 0) {
|
|
329
|
+
this.signal(PREFORK_SIGKILL)
|
|
330
|
+
while (this.reap(true) && this.running > 0) {
|
|
331
|
+
if (pollWait(loop, ready, this.config.tickMs) > 0) {
|
|
332
|
+
readSignal(signals)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The first field of `config` the supervisor cannot run with, or "" when none. */
|
|
340
|
+
const preforkInvalid = (config: PreforkConfig): string => {
|
|
341
|
+
if (config.workers < 0) {
|
|
342
|
+
return "workers below 0"
|
|
343
|
+
}
|
|
344
|
+
if (config.tickMs <= 0) {
|
|
345
|
+
return "tickMs not above 0"
|
|
346
|
+
}
|
|
347
|
+
if (config.backoffMs <= 0 || config.backoffCapMs < config.backoffMs) {
|
|
348
|
+
return "backoffMs not above 0 or above backoffCapMs"
|
|
349
|
+
}
|
|
350
|
+
if (config.quickExitMs < 0 || config.graceMs < 0) {
|
|
351
|
+
return "quickExitMs or graceMs below 0"
|
|
352
|
+
}
|
|
353
|
+
return ""
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* In a worker, its index. In the supervisor, run the workers until SIGTERM or
|
|
358
|
+
* SIGINT and answer `PREFORK_STOPPED`, or `PREFORK_SPAWN_FAILED` when a worker's
|
|
359
|
+
* first start failed (those already started are stopped first) or no signal
|
|
360
|
+
* listener could be made (no worker is started). Panics on a config
|
|
361
|
+
* `preforkInvalid` names a field of.
|
|
362
|
+
*/
|
|
363
|
+
export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
|
|
364
|
+
const invalid: string = preforkInvalid(config)
|
|
365
|
+
if (invalid !== "") {
|
|
366
|
+
panic(`nish/net/prefork: ${invalid}`)
|
|
367
|
+
}
|
|
368
|
+
const index: i32 = preforkIndex(args)
|
|
369
|
+
if (index >= 0) {
|
|
370
|
+
return index
|
|
371
|
+
}
|
|
372
|
+
const signals: i32 = signalFd()
|
|
373
|
+
const loop: i32 = pollCreate()
|
|
374
|
+
if (signals < 0 || loop < 0 || pollAdd(loop, signals, 1, 0) < 0) {
|
|
375
|
+
// `signals` is the process's one signal pipe, which every later
|
|
376
|
+
// `signalFd()` answers, so only the loop is closed.
|
|
377
|
+
console.error("nish/net/prefork: cannot listen for SIGTERM")
|
|
378
|
+
if (loop >= 0) {
|
|
379
|
+
netClose(loop)
|
|
380
|
+
}
|
|
381
|
+
return PREFORK_SPAWN_FAILED
|
|
382
|
+
}
|
|
383
|
+
const supervisor = new PreforkSupervisor(
|
|
384
|
+
config,
|
|
385
|
+
args,
|
|
386
|
+
preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
|
|
387
|
+
)
|
|
388
|
+
const ready: i32[] = new Array<i32>(2)
|
|
389
|
+
let result: i32 = PREFORK_STOPPED
|
|
390
|
+
if (!supervisor.restart(true)) {
|
|
391
|
+
console.error(`nish/net/prefork: cannot start ${supervisor.program}`)
|
|
392
|
+
result = PREFORK_SPAWN_FAILED
|
|
393
|
+
} else {
|
|
394
|
+
let running = true
|
|
395
|
+
while (running) {
|
|
396
|
+
const n: i32 = pollWait(loop, ready, config.tickMs)
|
|
397
|
+
if (n > 0) {
|
|
398
|
+
readSignal(signals)
|
|
399
|
+
running = false
|
|
400
|
+
} else {
|
|
401
|
+
supervisor.reap(false)
|
|
402
|
+
supervisor.restart(false)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
supervisor.stop(loop, signals, ready)
|
|
407
|
+
netClose(loop)
|
|
408
|
+
return result
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** `preforkWith` with the default `PreforkConfig`. */
|
|
412
|
+
export const preforkWorkers = (args: string[]): i32 => preforkWith(args, new PreforkConfig())
|
package/std/net/quic-listener.ts
CHANGED
|
@@ -71,6 +71,10 @@
|
|
|
71
71
|
* while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
|
|
72
72
|
* … and wake at the earlier of conn.deadline() and quicListenerPaceTime(conn, now)
|
|
73
73
|
*
|
|
74
|
+
* A carrier that sends whole flights with `quicListenerTakeFlight`, below,
|
|
75
|
+
* wakes at `quicListenerFlightTime` instead, as `Http3Server` does: a flight
|
|
76
|
+
* waits for more credit than one datagram, so the pace time can come first.
|
|
77
|
+
*
|
|
74
78
|
* **GSO and GRO** (`nish:net`'s `UDP_SEGMENT` and `UDP_GRO`). A carrier that
|
|
75
79
|
* sends with segmentation offload takes a whole flight at once:
|
|
76
80
|
* `quicListenerTakeFlight` writes as many datagrams as the pacer has credit
|
|
@@ -79,7 +83,10 @@
|
|
|
79
83
|
* cuts back into datagrams; each is written in place, so a flight allocates
|
|
80
84
|
* nothing. The flight is paced as a whole: it is as long as the pacer's
|
|
81
85
|
* credit, and charged to it datagram by datagram, so a burst is never more
|
|
82
|
-
* than the initial window however the carrier sends it.
|
|
86
|
+
* than the initial window however the carrier sends it. It starts only on a
|
|
87
|
+
* millisecond's earning, up to four datagrams, so a fast connection's pacer,
|
|
88
|
+
* which earns by the microsecond, does not let it out a datagram per send;
|
|
89
|
+
* `quicListenerFlightTime` says when. On the receiving
|
|
83
90
|
* side, `quicListenerReceiveSegments` hands a connection the datagrams of
|
|
84
91
|
* one GRO receive, segment by segment, in place.
|
|
85
92
|
*
|
|
@@ -95,9 +102,10 @@
|
|
|
95
102
|
* byte for byte.
|
|
96
103
|
*
|
|
97
104
|
* **Secrets.** The seed lives in the listener and the static keys in the
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* `
|
|
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.
|
|
101
109
|
*
|
|
102
110
|
* Written from RFC 9000, in this module's own structure; nothing here is
|
|
103
111
|
* ported from another implementation. Private names carry the
|
|
@@ -212,12 +220,38 @@ export class QuicListenerAnswer {
|
|
|
212
220
|
retryScid: u8[]
|
|
213
221
|
kind: i32 = 0
|
|
214
222
|
retried: boolean = false
|
|
223
|
+
room: i32 = 0
|
|
215
224
|
|
|
216
225
|
constructor(room: i32) {
|
|
217
|
-
this.
|
|
226
|
+
this.room = room > 0 ? room : 0
|
|
227
|
+
this.reply = quicListenerRoom(this.room)
|
|
218
228
|
this.originalDcid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
|
|
219
229
|
this.retryScid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
|
|
220
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
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
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)
|
|
221
255
|
}
|
|
222
256
|
|
|
223
257
|
/** An empty array with room for `size` bytes, so pushing up to that many allocates nothing. */
|
|
@@ -365,6 +399,30 @@ export class QuicListener {
|
|
|
365
399
|
secureZero(entropy)
|
|
366
400
|
}
|
|
367
401
|
|
|
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
|
+
|
|
368
426
|
/**
|
|
369
427
|
* One unpredictable value in 0 to 255: the next byte of HMAC-SHA256 under
|
|
370
428
|
* the seed of a counter, a block at a time, each byte wiped once taken.
|
|
@@ -843,6 +901,24 @@ export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | nu
|
|
|
843
901
|
export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
|
|
844
902
|
now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
|
|
845
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
|
+
|
|
846
922
|
/** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
|
|
847
923
|
export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
|
|
848
924
|
|
|
@@ -858,8 +934,10 @@ export class QuicFlight {
|
|
|
858
934
|
* Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
|
|
859
935
|
* datagram after datagram, for one GSO send, and answers its length in
|
|
860
936
|
* bytes (0 for none); `flight` says how many datagrams it holds and the
|
|
861
|
-
* segment size.
|
|
862
|
-
*
|
|
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
|
|
863
941
|
* `quicListenerTakePaced` does, so the flight is the pacer's whole credit
|
|
864
942
|
* and no more. It stops at the first datagram shorter than
|
|
865
943
|
* `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
|
|
@@ -877,6 +955,9 @@ export const quicListenerTakeFlight = (
|
|
|
877
955
|
flight.count = 0
|
|
878
956
|
flight.segment = 0
|
|
879
957
|
let length: i32 = 0
|
|
958
|
+
if (conn.recovery.pacerDelay(now, quicListenerQuantum(conn)) > 0) {
|
|
959
|
+
return length
|
|
960
|
+
}
|
|
880
961
|
while (
|
|
881
962
|
flight.count < QUIC_LISTENER_FLIGHT_MAX &&
|
|
882
963
|
length + QUIC_CONN_DATAGRAM_SIZE <= cap &&
|