@threadedstack/wmd-web 0.0.0-stage → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. package/wmdpack.wasm +0 -0
@@ -0,0 +1,867 @@
1
+ // P-B10: the guest's network in the browser: a small user-space TCP/IP stack that
2
+ // terminates the guest's Ethernet frames (virtio-net over the net channel) and carries its
3
+ // TCP connections and DNS lookups over the relay (relay-client.js), as the engine's
4
+ // gvisor-tap-vsock netstack does natively (internal/engine/netstack.go).
5
+ //
6
+ // What it answers, on the engine's addresses (192.168.127.0/24, gateway .1, guest .2):
7
+ // ARP for the gateway only
8
+ // DHCP one static lease: the guest's address, the gateway as router and DNS server
9
+ // DNS UDP to gateway:53: A queries go to the relay (which resolves and filters them);
10
+ // AAAA and other types get an empty answer (the guest has no IPv6 route);
11
+ // gateway.wmd.internal is the gateway
12
+ // TCP every connection the guest opens is a relay stream; the SYN-ACK is sent only
13
+ // once the relay has connected, so a refused connection is a RST (ECONNREFUSED)
14
+ // ICMP echo to the gateway; anything else is answered "administratively prohibited"
15
+ // UDP other than DHCP/DNS: ICMP unreachable
16
+ //
17
+ // TCP here is the passive end of each connection. The link (the channel rings) is
18
+ // reliable and ordered, and vm.wasm waits rather than drops when the guest has no receive
19
+ // buffer (chan_pump.c), so there is no congestion control; the guest's advertised window
20
+ // and the ring's room bound what is in flight. Retransmission on a timeout, three
21
+ // duplicate ACKs and zero-window probes are still there in case the guest drops a segment.
22
+ //
23
+ // W56: on an in-page switch (switch.js, peer.js) the guest has neighbours: ARP is answered for
24
+ // each guest on the switch too, their names (name, name.wmd.internal) resolve to their addresses
25
+ // before any relay lookup, a connection to one of them is a stream to that guest's network Worker
26
+ // (peer.js) instead of a relay stream, and a connection a neighbour opens to this guest comes in
27
+ // through peerOpen(): here the stack is the active end (it sends the SYN). Without a relay (a
28
+ // switch only) other addresses and names fail at once.
29
+ //
30
+ // Truncation: a stream the relay ends cleanly (the remote's FIN) ends with a FIN; a stream
31
+ // that fails (the remote reset, the relay dropped, a limit) ends with a RST, never a FIN,
32
+ // so the guest's program sees an error, not a short file that looks complete.
33
+
34
+ import {
35
+ ETH_ARP, ETH_IPV4, IP_ICMP, IP_TCP, IP_UDP, TCP_ACK, TCP_FIN, TCP_PSH, TCP_RST, TCP_SYN, arpReply, icmpEchoReply, icmpUnreachable,
36
+ ipToU32, macBytes, macStr, parseArp, parseEth, parseIp, parseTcp, parseUdp, seqAdd, seqDiff, seqLe, seqLt, tcpFrame, u32ToIp, udpFrame,
37
+ } from './packet.js'
38
+ import { DHCP, QTYPE, RC, dhcpReply, dnsResponse, parseDhcp, parseDnsQuery } from './dhcp-dns.js'
39
+ import { E, E_NAMES, MAX_DATA, WINDOW } from './proto.js'
40
+
41
+ export const NET_DEFAULTS = {
42
+ gateway: '192.168.127.1',
43
+ ip: '192.168.127.2',
44
+ mask: '255.255.255.0',
45
+ gatewayMac: '5a:94:ef:e4:0c:dd', // internal/engine/netstack.go gatewayMAC
46
+ mtu: 1500,
47
+ }
48
+
49
+ const OUR_MSS = 1460
50
+ const OUR_WS = 3 // window scale we offer: windows up to 512 KiB
51
+ const RCV_BUF = WINDOW // bytes from the guest held for the relay at most, per connection
52
+ const RTO_MIN = 1000
53
+ const RTO_MAX = 30_000
54
+ const CONNECT_TIMEOUT_MS = 30_000
55
+ const DNS_TIMEOUT_MS = 10_000
56
+ const HALF_OPEN_IDLE_MS = 10 * 60_000
57
+ const SYNACK_GIVE_UP_MS = 60_000 // Linux: tcp_synack_retries (5), about 63 s
58
+ const GUEST_CLOSED_IDLE_MS = 120_000 // the guest's FIN is in, the remote says nothing: its kernel forgot it (FIN_WAIT2, 60 s)
59
+ const MAX_PENDING = 32 // handshakes not finished at once (more get a RST), so abandoned ones cannot take every relay slot
60
+ const INTERNAL_ZONE = 'wmd.internal'
61
+
62
+ const P_CONNECTING = 1 // waiting for the relay's CONNECTED
63
+ const P_SYNACK = 2 // SYN-ACK sent, waiting for the guest's ACK
64
+ const P_OPEN = 3
65
+ const P_SYNSENT = 4 // W56: a peer's connection to the guest: our SYN sent, waiting for its SYN-ACK
66
+ const PEER_TTL = 60 // seconds: DNS answers for guests on the switch
67
+
68
+ /** The transport when there is no relay (a guest on a switch only): every connect and lookup fails at once. */
69
+ class NoRelay {
70
+ constructor() {
71
+ this.up = false
72
+ this.connecting = false
73
+ this.stack = null
74
+ }
75
+ connect(id) {
76
+ queueMicrotask(() => this.stack?.onConnected(id, E.UNREACHABLE, 0, 'no relay: only the guests on this network are reachable'))
77
+ }
78
+ data() {}
79
+ end() {}
80
+ abort() {}
81
+ credit() {}
82
+ dns() {}
83
+ }
84
+
85
+ class Conn {
86
+ constructor(key, id, s, ip) {
87
+ this.key = key
88
+ this.id = id
89
+ this.gip = ip.src
90
+ this.rip = ip.dst
91
+ this.gport = s.sport
92
+ this.rport = s.dport
93
+ this.phase = P_CONNECTING
94
+ this.irs = s.seq
95
+ this.rcvNxt = seqAdd(s.seq, 1)
96
+ this.guestWs = s.wscale >= 0 ? s.wscale : -1
97
+ this.ourWs = this.guestWs >= 0 ? OUR_WS : 0
98
+ this.mss = Math.min(s.mss || 536, OUR_MSS)
99
+ this.iss = (Math.random() * 0x100000000) >>> 0
100
+ this.sndUna = this.iss
101
+ this.sndNxt = this.iss
102
+ this.sndWnd = s.win // until the handshake ends: unscaled, as RFC 7323 says for SYNs
103
+ this.sendQ = [] // bytes from the relay from sndUna on (after the SYN)
104
+ this.sendQBytes = 0
105
+ this.toRelay = [] // bytes from the guest waiting for relay credit
106
+ this.toRelayBytes = 0
107
+ this.relayCredit = 0
108
+ this.guestFin = false // the guest's FIN was received
109
+ this.relayEndSent = false
110
+ this.remoteEnd = false // the relay said END
111
+ this.finSent = false
112
+ this.finSeq = 0
113
+ this.finAcked = false
114
+ this.rto = RTO_MIN
115
+ this.rtoAt = 0
116
+ this.dupAcks = 0
117
+ this.needAck = false
118
+ this.lastWndSent = 0
119
+ this.openedAt = 0
120
+ this.synAckAt = 0
121
+ this.lastActive = 0
122
+ this.lastRemote = 0 // the last data or end from the relay
123
+ this.bytesIn = 0 // guest -> relay
124
+ this.bytesOut = 0 // relay -> guest
125
+ }
126
+ inflight() {
127
+ return seqDiff(this.sndNxt, this.sndUna)
128
+ }
129
+ }
130
+
131
+ export class NetStack {
132
+ /**
133
+ * @param {object} o
134
+ * @param {(frame: Uint8Array) => void} o.send a frame for the guest
135
+ * @param {object} [o.relay] {connect(id, host, port), data(id, u8), end(id), abort(id), credit(id, n), dns(id, name, qtype), up}; none: no internet
136
+ * @param {object} [o.peers] W56: the guests on an in-page switch (peer.js PeerNet)
137
+ * @param {() => number} [o.now]
138
+ * @param {() => number} [o.txRoom] bytes the guest's ring can take now (net-worker.js); data waits when 0
139
+ * @param {number} [o.maxConns] connections at once (more get a RST)
140
+ * @param {(line: string) => void} [o.log]
141
+ */
142
+ constructor(o) {
143
+ const c = { ...NET_DEFAULTS, ...(o.config || {}) }
144
+ this.cfg = c
145
+ this.gw = ipToU32(c.gateway)
146
+ this.ip = ipToU32(c.ip)
147
+ this.mask = ipToU32(c.mask)
148
+ this.gwMac = macBytes(c.gatewayMac)
149
+ this.guestMac = null
150
+ this.sendFrame = o.send
151
+ this.relay = o.relay || new NoRelay()
152
+ if (!o.relay) this.relay.stack = this
153
+ this.noRelay = !o.relay
154
+ this.peers = o.peers || null // W56: the switch's guests (peer.js)
155
+ this.now = o.now || (() => performance.now())
156
+ this.maxConns = o.maxConns ?? 256
157
+ this.log = o.log || (() => {})
158
+ this.conns = new Map() // key -> Conn
159
+ this.byId = new Map() // relay stream id -> Conn
160
+ this.dns = new Map() // relay query id -> {q, p, sport, at}
161
+ this.nextId = 1
162
+ this.txRoom = o.txRoom || (() => Infinity)
163
+ this.wantsSend = new Set()
164
+ this.acks = new Set()
165
+ this.stats = {
166
+ framesIn: 0, framesOut: 0, bytesIn: 0, bytesOut: 0, arp: 0, dhcp: 0, dnsQueries: 0, dnsAnswered: 0, dnsFailed: 0,
167
+ tcpOpened: 0, tcpRefused: 0, tcpReset: 0, tcpFinished: 0, retransmits: 0, dupAckRetransmits: 0, probes: 0,
168
+ dropped: 0, icmp: 0, refusedBy: {}, connectMs: [], payloadIn: 0, payloadOut: 0,
169
+ }
170
+ this.events = []
171
+ }
172
+
173
+ #event(e) {
174
+ this.events.push({ at: Math.round(this.now()), ...e })
175
+ if (this.events.length > 200) this.events.shift()
176
+ }
177
+
178
+ #out(f) {
179
+ this.stats.framesOut++
180
+ this.stats.bytesOut += f.length
181
+ this.sendFrame(f)
182
+ }
183
+
184
+ /** A frame from the guest. */
185
+ input(f) {
186
+ this.stats.framesIn++
187
+ this.stats.bytesIn += f.length
188
+ const eth = parseEth(f)
189
+ if (!eth) return this.#drop('short frame')
190
+ if (!this.guestMac && !(eth.src[0] & 1)) this.guestMac = eth.src.slice()
191
+ if (eth.type === ETH_ARP) return this.#arp(eth)
192
+ if (eth.type !== ETH_IPV4) return // IPv6 and others: no route
193
+ const ip = parseIp(eth.payload)
194
+ if (!ip) return this.#drop('bad IPv4')
195
+ if (ip.fragment) return this.#drop('IPv4 fragment')
196
+ const mac = eth.src
197
+ if (ip.proto === IP_UDP) return this.#udp(mac, ip)
198
+ if (ip.proto === IP_TCP) return this.#tcp(mac, ip)
199
+ if (ip.proto === IP_ICMP) return this.#icmp(mac, ip)
200
+ this.#drop(`IP protocol ${ip.proto}`)
201
+ }
202
+
203
+ #drop(why) {
204
+ this.stats.dropped++
205
+ if (this.stats.dropped <= 20) this.log(`net: dropped a frame: ${why}`)
206
+ }
207
+
208
+ #arp(eth) {
209
+ const a = parseArp(eth.payload)
210
+ if (!a || a.op !== 1) return
211
+ // the gateway, and (W56) each other guest on the switch: their frames come here too
212
+ if (a.tpa !== this.gw && !(this.peers && a.tpa !== this.ip && this.peers.has(a.tpa))) return
213
+ this.stats.arp++
214
+ this.#out(arpReply(this.gwMac, a.tpa, a.sha, a.spa))
215
+ }
216
+
217
+ #icmp(mac, ip) {
218
+ this.stats.icmp++
219
+ const p = ip.payload
220
+ if (p.length < 8 || p[0] !== 8) return
221
+ if (ip.dst === this.gw) return this.#out(icmpEchoReply(this.gwMac, mac, this.gw, ip.src, p))
222
+ this.#out(icmpUnreachable(this.gwMac, mac, this.gw, ip.src, 13, quoteOf(ip))) // only TCP and DNS leave the guest
223
+ }
224
+
225
+ #udp(mac, ip) {
226
+ const u = parseUdp(ip.payload)
227
+ if (!u) return this.#drop('bad UDP')
228
+ if (u.dport === 67) return this.#dhcp(mac, u)
229
+ if (u.dport === 53 && ip.dst === this.gw) return this.#dnsQuery(mac, ip, u)
230
+ if ((ip.dst & 0xf0000000) >>> 0 === 0xe0000000 || ip.dst === 0xffffffff || ip.dst === ((this.ip | ~this.mask) >>> 0)) return // multicast/broadcast
231
+ this.#out(icmpUnreachable(this.gwMac, mac, this.gw, ip.src, ip.dst === this.gw ? 3 : 13, quoteOf(ip)))
232
+ }
233
+
234
+ #dhcp(mac, u) {
235
+ const m = parseDhcp(u.payload)
236
+ if (!m) return
237
+ this.stats.dhcp++
238
+ let type
239
+ if (m.type === DHCP.DISCOVER) type = DHCP.OFFER
240
+ else if (m.type === DHCP.REQUEST || m.type === DHCP.INFORM) type = DHCP.ACK
241
+ else return
242
+ this.guestMac = m.chaddr.slice()
243
+ const reply = dhcpReply(m, type, { ip: this.ip, gw: this.gw, mask: this.mask })
244
+ this.#event({ ev: 'dhcp', type: m.type, mac: macStr(m.chaddr) })
245
+ this.#out(udpFrame(this.gwMac, m.chaddr, this.gw, 0xffffffff, 67, 68, reply))
246
+ }
247
+
248
+ #dnsQuery(mac, ip, u) {
249
+ const q = parseDnsQuery(u.payload)
250
+ if (!q) return this.#drop('bad DNS query')
251
+ this.stats.dnsQueries++
252
+ const reply = (rcode, answers = [], ttl = 60) => {
253
+ if (rcode === RC.NOERROR) this.stats.dnsAnswered++
254
+ else this.stats.dnsFailed++
255
+ this.#out(udpFrame(this.gwMac, mac, this.gw, ip.src, 53, u.sport, dnsResponse(u.payload, q, rcode, answers, ttl)))
256
+ }
257
+ if (q.opcode !== 0 || q.qdcount !== 1) return reply(RC.NOTIMP)
258
+ const name = q.name.toLowerCase().replace(/\.$/, '')
259
+ if (q.qclass !== 1) return reply(RC.REFUSED)
260
+ if (name === `gateway.${INTERNAL_ZONE}`) return reply(RC.NOERROR, q.qtype === QTYPE.A ? [u32b(this.gw)] : [])
261
+ if (this.peers) {
262
+ // W56: a guest on the switch, by its name or name.wmd.internal
263
+ const short = name.endsWith(`.${INTERNAL_ZONE}`) ? name.slice(0, -INTERNAL_ZONE.length - 1) : name
264
+ const a = this.peers.lookup(short)
265
+ if (a !== undefined) {
266
+ this.stats.dnsPeer = (this.stats.dnsPeer || 0) + 1
267
+ return reply(RC.NOERROR, q.qtype === QTYPE.A ? [u32b(a)] : [], PEER_TTL)
268
+ }
269
+ }
270
+ if (name === INTERNAL_ZONE || name.endsWith(`.${INTERNAL_ZONE}`)) return reply(RC.NXDOMAIN)
271
+ if (q.qtype !== QTYPE.A) return reply(RC.NOERROR) // no IPv6 route; other types: nothing
272
+ if (this.noRelay) return reply(RC.SERVFAIL)
273
+ if (!this.relay.up && !this.relay.connecting) this.relay.wake?.()
274
+ const id = this.nextId++
275
+ this.dns.set(id, { reply, name, at: this.now() })
276
+ this.relay.dns(id, name, QTYPE.A)
277
+ }
278
+
279
+ /** The relay's answer to DNS query id. rcode: RC.*, addrs: 4-byte arrays. */
280
+ onDnsReply(id, rcode, ttl, addrs, reason) {
281
+ const d = this.dns.get(id)
282
+ if (!d) return
283
+ this.dns.delete(id)
284
+ if (rcode !== RC.NOERROR) this.#event({ ev: 'dns', name: d.name, rcode, reason })
285
+ d.reply(rcode, rcode === RC.NOERROR ? addrs.filter(a => a.length === 4) : [], Math.min(Math.max(ttl, 1), 300))
286
+ }
287
+
288
+ // ---- TCP ----
289
+
290
+ #key(ip, s) {
291
+ return `${ip.src}:${s.sport}>${ip.dst}:${s.dport}`
292
+ }
293
+
294
+ #seg(c, flags, seq, data, opts) {
295
+ const win = c.phase === P_SYNACK && (flags & TCP_SYN) ? Math.min(this.#rcvWnd(c), 0xffff) : this.#rcvWnd(c) >> c.ourWs
296
+ if (!(flags & TCP_SYN)) c.lastWndSent = this.#rcvWnd(c)
297
+ this.#out(tcpFrame(this.gwMac, this.guestMac || this.gwMac, c.rip, c.gip, c.rport, c.gport, seq, c.rcvNxt, flags, win, data, opts))
298
+ }
299
+
300
+ #rcvWnd(c) {
301
+ const w = Math.max(0, RCV_BUF - c.toRelayBytes)
302
+ return c.ourWs ? w : Math.min(w, 0xffff)
303
+ }
304
+
305
+ #rst(mac, ip, s) {
306
+ // RFC 793: a reset for a segment with no connection
307
+ const len = s.payload.length + (s.flags & TCP_SYN ? 1 : 0) + (s.flags & TCP_FIN ? 1 : 0)
308
+ const flags = s.flags & TCP_ACK ? TCP_RST : TCP_RST | TCP_ACK
309
+ const seq = s.flags & TCP_ACK ? s.ack : 0
310
+ this.#out(tcpFrame(this.gwMac, mac, ip.dst, ip.src, s.dport, s.sport, seq, seqAdd(s.seq, len), flags, 0))
311
+ }
312
+
313
+ #tcp(mac, ip) {
314
+ const s = parseTcp(ip.payload)
315
+ if (!s) return this.#drop('bad TCP')
316
+ if (s.flags & TCP_RST) {
317
+ const c = this.conns.get(this.#key(ip, s))
318
+ if (c?.phase === P_SYNSENT) {
319
+ // nothing listens on the port: the peer's connect() gets ECONNREFUSED
320
+ if (!(s.flags & TCP_ACK) || s.ack !== seqAdd(c.iss, 1)) return
321
+ this.#forget(c)
322
+ this.stats.tcpRefused++
323
+ c.via.accepted(c.id, E.REFUSED, 'connection refused')
324
+ } else if (c) this.#close(c, 'guest reset', true)
325
+ return
326
+ }
327
+ let c = this.conns.get(this.#key(ip, s))
328
+ if (s.flags & TCP_SYN && !(s.flags & TCP_ACK)) {
329
+ if (c) {
330
+ if (s.seq === c.irs && !c.inbound) {
331
+ // our SYN-ACK was lost: send it again, but do not move the retransmit timer or the give-up time
332
+ if (c.phase === P_SYNACK && !this.#giveUpSynAck(c)) this.#seg(c, TCP_SYN | TCP_ACK, c.iss, null, { mss: OUR_MSS, wscale: c.ourWs })
333
+ return
334
+ }
335
+ this.#close(c, 'a new connection on the same ports', true) // a new connection on the same 4-tuple
336
+ }
337
+ return this.#syn(mac, ip, s)
338
+ }
339
+ if (!c) return this.#rst(mac, ip, s)
340
+ c.lastActive = this.now()
341
+ if (c.phase === P_CONNECTING) return // the guest retransmits its SYN until we answer
342
+ if (c.phase === P_SYNSENT) return this.#synAckIn(c, s)
343
+ if (!(s.flags & TCP_ACK)) return
344
+ this.#ack(c, s)
345
+ if (!this.conns.has(c.key) || c.phase !== P_OPEN) return
346
+ this.#data(c, s)
347
+ if (this.conns.has(c.key)) this.#maybeDone(c)
348
+ }
349
+
350
+ #syn(mac, ip, s) {
351
+ const dst = ip.dst
352
+ const local = ((dst & this.mask) >>> 0) === ((this.gw & this.mask) >>> 0)
353
+ const why =
354
+ dst === this.gw ? 'the gateway has no TCP services' :
355
+ dst === this.ip || (dst >>> 24) === 127 || dst === 0 || dst >>> 28 >= 14 || dst === ((this.ip | ~this.mask) >>> 0) ? 'not a remote address' :
356
+ local && !this.peers ? 'nothing else is on the guest network' :
357
+ local && !this.peers.has(dst) ? 'no guest on the network has that address' :
358
+ this.conns.size >= this.maxConns ? `more than ${this.maxConns} connections at once` :
359
+ this.#pending() >= MAX_PENDING && !this.#evictOldestSynAck() ? `more than ${MAX_PENDING} connections being opened at once` : ''
360
+ if (why) {
361
+ this.stats.tcpRefused++
362
+ this.stats.refusedBy.local = (this.stats.refusedBy.local || 0) + 1
363
+ this.#event({ ev: 'refused', to: `${u32ToIp(dst)}:${s.dport}`, why })
364
+ return this.#rst(mac, ip, s)
365
+ }
366
+ if (!this.guestMac) this.guestMac = mac.slice()
367
+ const id = this.nextId++
368
+ const c = new Conn(this.#key(ip, s), id, s, ip)
369
+ c.via = local ? this.peers : this.relay
370
+ c.openedAt = c.lastActive = this.now()
371
+ this.conns.set(c.key, c)
372
+ this.byId.set(id, c)
373
+ if (local) this.peers.connect(id, dst, s.dport, s.sport)
374
+ else this.relay.connect(id, u32ToIp(dst), s.dport)
375
+ }
376
+
377
+ /** The stack still has stream id (a transport asks before it acts on a late answer). */
378
+ hasConn(id) {
379
+ return this.byId.has(id)
380
+ }
381
+
382
+ /**
383
+ * W56: a guest on the switch (at rip, from its port rport) opens a connection to this guest's
384
+ * port gport: SYN the guest; via.accepted(id, code) once it answers. Returns the stream id, or
385
+ * null when it is refused at once.
386
+ */
387
+ peerOpen(rip, rport, gport, via) {
388
+ const key = `${this.ip}:${gport}>${rip}:${rport}`
389
+ const why =
390
+ !this.guestMac ? 'the guest has no network yet' :
391
+ this.conns.has(key) ? 'a connection on the same ports is open' :
392
+ this.conns.size >= this.maxConns ? `more than ${this.maxConns} connections at once` : ''
393
+ if (why) {
394
+ this.#event({ ev: 'refused-in', from: `${u32ToIp(rip)}:${rport}`, to: gport, why })
395
+ return null
396
+ }
397
+ const id = this.nextId++
398
+ const c = new Conn(key, id, { sport: gport, dport: rport, seq: 0, wscale: -1, mss: OUR_MSS, win: 0 }, { src: this.ip, dst: rip })
399
+ c.via = via
400
+ c.phase = P_SYNSENT
401
+ c.inbound = true
402
+ c.ourWs = OUR_WS
403
+ c.sndNxt = seqAdd(c.iss, 1)
404
+ c.openedAt = c.lastActive = c.synAckAt = this.now()
405
+ c.relayCredit = WINDOW // the opener's grant: it takes WINDOW bytes before it says more
406
+ this.conns.set(key, c)
407
+ this.byId.set(id, c)
408
+ this.stats.peerIn = (this.stats.peerIn || 0) + 1
409
+ this.#synOut(c)
410
+ return id
411
+ }
412
+
413
+ #synOut(c) {
414
+ this.#out(tcpFrame(this.gwMac, this.guestMac, c.rip, c.gip, c.rport, c.gport, c.iss, 0, TCP_SYN, Math.min(RCV_BUF, 0xffff), null, { mss: OUR_MSS, wscale: OUR_WS }))
415
+ c.rtoAt = this.now() + c.rto
416
+ }
417
+
418
+ /** W56: the guest's answer to our SYN. */
419
+ #synAckIn(c, s) {
420
+ if (!(s.flags & TCP_SYN) || !(s.flags & TCP_ACK) || s.ack !== seqAdd(c.iss, 1)) return
421
+ c.irs = s.seq
422
+ c.rcvNxt = seqAdd(s.seq, 1)
423
+ c.guestWs = s.wscale >= 0 ? s.wscale : -1
424
+ c.ourWs = c.guestWs >= 0 ? OUR_WS : 0 // window scaling only when both offered it
425
+ c.mss = Math.min(s.mss || 536, OUR_MSS)
426
+ c.sndUna = c.sndNxt = seqAdd(c.iss, 1)
427
+ c.sndWnd = s.win // a SYN's window is not scaled
428
+ c.phase = P_OPEN
429
+ c.rto = RTO_MIN
430
+ c.rtoAt = 0
431
+ this.stats.tcpOpened++
432
+ this.#seg(c, TCP_ACK, c.sndNxt, null)
433
+ c.via.accepted(c.id, E.OK)
434
+ this.wantsSend.add(c)
435
+ this.#trySend(c)
436
+ }
437
+
438
+ #pending() {
439
+ let n = 0
440
+ for (const c of this.conns.values()) if (c.phase !== P_OPEN) n++
441
+ return n
442
+ }
443
+
444
+ /** The pending cap is full: the oldest handshake that is not open yet gives way to the new SYN, so a flood of half-open handshakes cannot lock out real connects. False if none can go. */
445
+ #evictOldestSynAck() {
446
+ let old = null
447
+ for (const c of this.conns.values()) if (c.phase !== P_OPEN && (!old || c.openedAt < old.openedAt)) old = c
448
+ if (!old) return false
449
+ this.#abandon(old, 'evicted for a newer SYN')
450
+ return true
451
+ }
452
+
453
+ #abandon(c, why) {
454
+ this.stats.handshakesAbandoned = (this.stats.handshakesAbandoned || 0) + 1
455
+ this.#event({ ev: 'abandoned', to: `${u32ToIp(c.rip)}:${c.rport}`, why })
456
+ this.#forget(c)
457
+ c.via.abort(c.id)
458
+ this.#out(tcpFrame(this.gwMac, this.guestMac || this.gwMac, c.rip, c.gip, c.rport, c.gport, c.sndNxt, c.rcvNxt, TCP_RST | TCP_ACK, 0))
459
+ }
460
+
461
+ /** A handshake the guest never finishes is given up after SYNACK_GIVE_UP_MS (counted from the first SYN-ACK, whatever the guest re-sends). True if it was given up. */
462
+ #giveUpSynAck(c) {
463
+ if (c.phase !== P_SYNACK || this.now() - c.synAckAt <= SYNACK_GIVE_UP_MS) return false
464
+ this.#abandon(c, 'no ACK for the SYN-ACK')
465
+ return true
466
+ }
467
+
468
+ #synAck(c) {
469
+ this.#seg(c, TCP_SYN | TCP_ACK, c.iss, null, { mss: OUR_MSS, wscale: c.ourWs })
470
+ c.rtoAt = this.now() + c.rto
471
+ }
472
+
473
+ /** The relay's answer to a connect. */
474
+ onConnected(id, code, credit, detail) {
475
+ const c = this.byId.get(id)
476
+ if (!c || c.phase !== P_CONNECTING) {
477
+ if (!c && code === E.OK) this.relay.abort(id) // a connect that came back after we gave up on it
478
+ return
479
+ }
480
+ if (code !== E.OK) {
481
+ this.stats.tcpRefused++
482
+ const name = E_NAMES[code] || `code ${code}`
483
+ this.stats.refusedBy[name] = (this.stats.refusedBy[name] || 0) + 1
484
+ this.#event({ ev: 'refused', to: `${u32ToIp(c.rip)}:${c.rport}`, why: name, detail })
485
+ this.log(`net: connect to ${u32ToIp(c.rip)}:${c.rport} refused by the relay: ${name}${detail ? ` (${detail})` : ''}`)
486
+ this.#forget(c)
487
+ // RST|ACK acknowledging the SYN: the guest's connect() fails with ECONNREFUSED
488
+ this.#out(tcpFrame(this.gwMac, this.guestMac, c.rip, c.gip, c.rport, c.gport, 0, c.rcvNxt, TCP_RST | TCP_ACK, 0))
489
+ return
490
+ }
491
+ this.stats.tcpOpened++
492
+ const ms = this.now() - c.openedAt
493
+ this.stats.connectMs.push(Math.round(ms * 10) / 10)
494
+ if (this.stats.connectMs.length > 100) this.stats.connectMs.shift()
495
+ c.relayCredit = credit
496
+ c.phase = P_SYNACK
497
+ c.synAckAt = this.now()
498
+ c.sndNxt = seqAdd(c.iss, 1)
499
+ this.#synAck(c)
500
+ }
501
+
502
+ #ack(c, s) {
503
+ const ack = s.ack
504
+ if (c.phase === P_SYNACK) {
505
+ if (ack !== seqAdd(c.iss, 1)) return // not for our SYN-ACK
506
+ c.phase = P_OPEN
507
+ c.sndUna = ack
508
+ c.rto = RTO_MIN
509
+ c.rtoAt = 0
510
+ c.sndWnd = s.win << Math.max(c.guestWs, 0)
511
+ this.wantsSend.add(c)
512
+ this.#trySend(c)
513
+ return
514
+ }
515
+ const wnd = s.win << Math.max(c.guestWs, 0)
516
+ if (seqLt(c.sndUna, ack) && seqLe(ack, c.sndNxt)) {
517
+ let n = seqDiff(ack, c.sndUna)
518
+ c.sndUna = ack
519
+ if (c.finSent && ack === seqAdd(c.finSeq, 1)) {
520
+ c.finAcked = true
521
+ n--
522
+ }
523
+ if (n > 0) {
524
+ this.#takeAcked(c, n)
525
+ c.via.credit(c.id, n) // the relay may send what the guest has taken
526
+ }
527
+ c.dupAcks = 0
528
+ c.rto = RTO_MIN
529
+ c.rtoAt = c.inflight() > 0 ? this.now() + c.rto : 0
530
+ c.sndWnd = wnd
531
+ this.wantsSend.add(c)
532
+ this.#trySend(c)
533
+ return
534
+ }
535
+ if (ack === c.sndUna && !s.payload.length && !(s.flags & TCP_FIN)) {
536
+ const opened = wnd > c.sndWnd
537
+ c.sndWnd = wnd
538
+ if (opened) {
539
+ this.wantsSend.add(c)
540
+ this.#trySend(c)
541
+ } else if (c.inflight() > 0 && ++c.dupAcks === 3) {
542
+ this.stats.dupAckRetransmits++
543
+ c.sndNxt = c.sndUna // go back: the segment after the ACK was lost
544
+ if (c.finSent && !c.finAcked) c.finSent = false
545
+ this.#trySend(c)
546
+ }
547
+ }
548
+ }
549
+
550
+ #takeAcked(c, n) {
551
+ c.sendQBytes -= n
552
+ while (n > 0 && c.sendQ.length) {
553
+ const h = c.sendQ[0]
554
+ if (h.length <= n) {
555
+ n -= h.length
556
+ c.sendQ.shift()
557
+ } else {
558
+ c.sendQ[0] = h.subarray(n)
559
+ n = 0
560
+ }
561
+ }
562
+ }
563
+
564
+ #data(c, s) {
565
+ const len = s.payload.length
566
+ const fin = !!(s.flags & TCP_FIN)
567
+ if (!len && !fin) {
568
+ // an old sequence number with no data is the guest's zero-window probe (or a keepalive):
569
+ // answer with our window, as RFC 9293 3.10.7.4 says for any unacceptable segment
570
+ if (seqLt(s.seq, c.rcvNxt)) {
571
+ c.needAck = true
572
+ this.acks.add(c)
573
+ }
574
+ return
575
+ }
576
+ let data = s.payload
577
+ let seq = s.seq
578
+ if (c.guestFin) {
579
+ c.needAck = true // a retransmission after its FIN
580
+ this.acks.add(c)
581
+ return
582
+ }
583
+ if (seqLt(seq, c.rcvNxt)) {
584
+ const skip = seqDiff(c.rcvNxt, seq)
585
+ if (skip >= len + (fin ? 1 : 0)) {
586
+ c.needAck = true
587
+ this.acks.add(c)
588
+ return // all old
589
+ }
590
+ data = data.subarray(Math.min(skip, len))
591
+ seq = c.rcvNxt
592
+ }
593
+ if (seq !== c.rcvNxt) {
594
+ c.needAck = true // a gap: ask again for what we expect
595
+ this.acks.add(c)
596
+ return
597
+ }
598
+ const room = this.#rcvWnd(c)
599
+ const take = Math.min(data.length, room)
600
+ if (take) {
601
+ const chunk = data.slice(0, take)
602
+ c.toRelay.push(chunk)
603
+ c.toRelayBytes += take
604
+ c.bytesIn += take
605
+ this.stats.payloadIn += take
606
+ c.rcvNxt = seqAdd(c.rcvNxt, take)
607
+ }
608
+ if (fin && take === data.length) {
609
+ c.rcvNxt = seqAdd(c.rcvNxt, 1)
610
+ c.guestFin = true
611
+ }
612
+ c.needAck = true
613
+ this.acks.add(c) // flushAcks() sends what came in this batch to the relay, coalesced, then the ACK
614
+ }
615
+
616
+ /** Send held guest bytes to the relay as credit allows, in messages of up to MAX_DATA (one per segment cost a WebSocket message each). */
617
+ #flushToRelay(c) {
618
+ while (c.toRelay.length && c.relayCredit > 0) {
619
+ const n = Math.min(c.toRelayBytes, c.relayCredit, MAX_DATA)
620
+ let part
621
+ if (c.toRelay[0].length >= n) {
622
+ const h = c.toRelay[0]
623
+ part = h.subarray(0, n)
624
+ if (h.length === n) c.toRelay.shift()
625
+ else c.toRelay[0] = h.subarray(n)
626
+ } else {
627
+ part = new Uint8Array(n)
628
+ let o = 0
629
+ while (o < n) {
630
+ const h = c.toRelay[0]
631
+ const k = Math.min(h.length, n - o)
632
+ part.set(h.subarray(0, k), o)
633
+ o += k
634
+ if (k === h.length) c.toRelay.shift()
635
+ else c.toRelay[0] = h.subarray(k)
636
+ }
637
+ }
638
+ c.toRelayBytes -= n
639
+ c.relayCredit -= n
640
+ c.via.data(c.id, part)
641
+ }
642
+ if (c.guestFin && !c.toRelay.length && !c.relayEndSent) {
643
+ c.relayEndSent = true
644
+ c.via.end(c.id)
645
+ }
646
+ }
647
+
648
+ onRelayCredit(id, n) {
649
+ const c = this.byId.get(id)
650
+ if (!c) return
651
+ const before = this.#rcvWnd(c)
652
+ c.relayCredit += n
653
+ this.#flushToRelay(c)
654
+ // the window opened since the guest last heard (by two segments, or from below one): tell it
655
+ const now = this.#rcvWnd(c)
656
+ if (c.phase === P_OPEN && before < now && (now - c.lastWndSent >= 2 * c.mss || (c.lastWndSent < c.mss && now >= c.mss))) {
657
+ c.needAck = true
658
+ this.acks.add(c)
659
+ }
660
+ this.#maybeDone(c)
661
+ }
662
+
663
+ onRelayData(id, u8) {
664
+ const c = this.byId.get(id)
665
+ if (!c) return
666
+ if (c.remoteEnd) return
667
+ c.lastRemote = this.now()
668
+ c.sendQ.push(u8)
669
+ c.sendQBytes += u8.length
670
+ c.bytesOut += u8.length
671
+ this.stats.payloadOut += u8.length
672
+ this.wantsSend.add(c)
673
+ this.#trySend(c)
674
+ }
675
+
676
+ onRelayEnd(id) {
677
+ const c = this.byId.get(id)
678
+ if (!c) return
679
+ c.remoteEnd = true
680
+ c.lastRemote = this.now()
681
+ this.wantsSend.add(c)
682
+ this.#trySend(c)
683
+ }
684
+
685
+ onRelayAbort(id, code, reason) {
686
+ const c = this.byId.get(id)
687
+ if (!c) return
688
+ if (c.phase === P_CONNECTING) return this.onConnected(id, code || E.GONE, 0, reason)
689
+ this.#event({ ev: 'abort', to: `${u32ToIp(c.rip)}:${c.rport}`, why: E_NAMES[code] || code, reason })
690
+ this.#close(c, `relay: ${E_NAMES[code] || code} ${reason || ''}`.trim(), false)
691
+ }
692
+
693
+ /** The relay connection is gone: every stream fails (RST), every lookup fails. */
694
+ onRelayDown(why) {
695
+ for (const c of [...this.conns.values()]) {
696
+ if (c.via !== this.relay) continue // W56: a stream to a guest on the switch does not use the relay
697
+ if (c.phase === P_CONNECTING) this.onConnected(c.id, E.GONE, 0, why)
698
+ else this.#close(c, `relay down: ${why}`, false)
699
+ }
700
+ for (const id of [...this.dns.keys()]) this.onDnsReply(id, RC.SERVFAIL, 0, [], why)
701
+ }
702
+
703
+ /** Send what the guest's window and the ring allow. */
704
+ #trySend(c) {
705
+ if (c.phase !== P_OPEN || !this.conns.has(c.key)) return
706
+ for (;;) {
707
+ const off = c.inflight()
708
+ const avail = c.sendQBytes - off
709
+ if (avail > 0) {
710
+ const wnd = c.sndWnd - off
711
+ const room = this.txRoom()
712
+ if (wnd <= 0 || room <= 0) break
713
+ const n = Math.min(avail, wnd, c.mss, room)
714
+ const data = peek(c.sendQ, off, n)
715
+ this.#seg(c, TCP_ACK | (n === avail ? TCP_PSH : 0), c.sndNxt, data)
716
+ c.needAck = false
717
+ c.sndNxt = seqAdd(c.sndNxt, n)
718
+ if (!c.rtoAt) c.rtoAt = this.now() + c.rto
719
+ continue
720
+ }
721
+ if (c.remoteEnd && !c.finSent && avail === 0) {
722
+ c.finSent = true
723
+ c.finSeq = c.sndNxt
724
+ this.#seg(c, TCP_FIN | TCP_ACK, c.sndNxt, null)
725
+ c.needAck = false
726
+ c.sndNxt = seqAdd(c.sndNxt, 1)
727
+ if (!c.rtoAt) c.rtoAt = this.now() + c.rto
728
+ }
729
+ break
730
+ }
731
+ if (c.sendQBytes - c.inflight() <= 0 && (!c.remoteEnd || c.finSent)) this.wantsSend.delete(c)
732
+ }
733
+
734
+ /** More room in the guest's ring: send what waited. */
735
+ pump() {
736
+ for (const c of [...this.wantsSend]) {
737
+ if (this.txRoom() <= 0) break
738
+ this.#trySend(c)
739
+ }
740
+ }
741
+
742
+ #maybeDone(c) {
743
+ if (c.guestFin && c.finAcked && c.relayEndSent) {
744
+ this.stats.tcpFinished++
745
+ this.#forget(c)
746
+ c.via.closed?.(c.id) // a peer's stream ends with no abort: drop its record
747
+ }
748
+ }
749
+
750
+ /** End a connection; tell the relay (abort) unless it told us; RST the guest unless it reset. */
751
+ #close(c, why, fromGuest) {
752
+ if (!this.conns.has(c.key)) return
753
+ this.#forget(c)
754
+ if (fromGuest) {
755
+ c.via.abort(c.id)
756
+ return
757
+ }
758
+ this.stats.tcpReset++
759
+ this.#event({ ev: 'reset', to: `${u32ToIp(c.rip)}:${c.rport}`, why, sent: c.bytesOut, recv: c.bytesIn })
760
+ this.#out(tcpFrame(this.gwMac, this.guestMac, c.rip, c.gip, c.rport, c.gport, c.sndNxt, c.rcvNxt, TCP_RST | TCP_ACK, 0))
761
+ }
762
+
763
+ #forget(c) {
764
+ this.conns.delete(c.key)
765
+ this.byId.delete(c.id)
766
+ this.wantsSend.delete(c)
767
+ this.acks.delete(c)
768
+ }
769
+
770
+ /** After a batch of input: one ACK per connection that needs one. */
771
+ flushAcks() {
772
+ for (const c of this.acks) {
773
+ if (this.conns.has(c.key)) this.#flushToRelay(c)
774
+ if (c.needAck && this.conns.has(c.key) && c.phase === P_OPEN) this.#seg(c, TCP_ACK, c.sndNxt, null)
775
+ c.needAck = false
776
+ if (this.conns.has(c.key)) this.#maybeDone(c)
777
+ }
778
+ this.acks.clear()
779
+ }
780
+
781
+ /** Timers: retransmission, zero-window probes, connect and DNS timeouts. Call every ~100 ms. */
782
+ tick() {
783
+ const t = this.now()
784
+ for (const c of [...this.conns.values()]) {
785
+ if (c.phase === P_CONNECTING) {
786
+ if (t - c.openedAt > CONNECT_TIMEOUT_MS) {
787
+ c.via.abort(c.id)
788
+ this.onConnected(c.id, E.TIMEOUT, 0, 'no answer from the relay')
789
+ }
790
+ continue
791
+ }
792
+ if (c.phase === P_SYNSENT) {
793
+ if (t - c.synAckAt > SYNACK_GIVE_UP_MS) {
794
+ this.#forget(c)
795
+ c.via.accepted(c.id, E.TIMEOUT, 'the guest did not answer')
796
+ } else if (c.rtoAt && t >= c.rtoAt) {
797
+ c.rto = Math.min(c.rto * 2, RTO_MAX)
798
+ this.stats.retransmits++
799
+ this.#synOut(c)
800
+ }
801
+ continue
802
+ }
803
+ if (this.#giveUpSynAck(c)) continue
804
+ if (c.rtoAt && t >= c.rtoAt) {
805
+ c.rto = Math.min(c.rto * 2, RTO_MAX)
806
+ c.rtoAt = t + c.rto
807
+ if (c.phase === P_SYNACK) {
808
+ if (this.#giveUpSynAck(c)) continue
809
+ this.stats.retransmits++
810
+ this.#seg(c, TCP_SYN | TCP_ACK, c.iss, null, { mss: OUR_MSS, wscale: c.ourWs })
811
+ continue
812
+ }
813
+ if (c.inflight() > 0) {
814
+ this.stats.retransmits++
815
+ c.sndNxt = c.sndUna
816
+ if (c.finSent && !c.finAcked) c.finSent = false
817
+ this.#trySend(c)
818
+ } else if (c.sendQBytes > 0 && c.sndWnd === 0) {
819
+ this.stats.probes++
820
+ this.#seg(c, TCP_ACK, seqAdd(c.sndUna, -1 >>> 0), null) // a window probe: an old sequence number makes the guest ACK with its window
821
+ } else c.rtoAt = 0
822
+ } else if (!c.rtoAt && c.sendQBytes > c.inflight() && c.sndWnd - c.inflight() <= 0) {
823
+ c.rtoAt = t + c.rto // the persist timer
824
+ }
825
+ if (c.remoteEnd && c.finAcked && !c.guestFin && t - c.lastActive > HALF_OPEN_IDLE_MS) this.#close(c, 'half-open and idle', false)
826
+ else if (c.guestFin && !c.remoteEnd && t - Math.max(c.lastActive, c.lastRemote) > GUEST_CLOSED_IDLE_MS) {
827
+ // the guest closed and the remote has been silent since: nobody will end it; tell the relay
828
+ c.via.abort(c.id)
829
+ this.#close(c, 'closed by the guest, the remote silent', false)
830
+ }
831
+ }
832
+ for (const [id, d] of this.dns) if (t - d.at > DNS_TIMEOUT_MS) this.onDnsReply(id, RC.SERVFAIL, 0, [], 'timed out')
833
+ }
834
+
835
+ snapshot() {
836
+ return { ...this.stats, conns: this.conns.size, pendingDns: this.dns.size, events: this.events.slice(-60) }
837
+ }
838
+ }
839
+
840
+ /** The IP header and 8 bytes of an IP packet, for an ICMP error. */
841
+ function quoteOf(ip) {
842
+ const q = new Uint8Array(ip.header.length + Math.min(8, ip.payload.length))
843
+ q.set(ip.header)
844
+ q.set(ip.payload.subarray(0, 8), ip.header.length)
845
+ return q
846
+ }
847
+
848
+ function u32b(v) {
849
+ return Uint8Array.of(v >>> 24, (v >>> 16) & 255, (v >>> 8) & 255, v & 255)
850
+ }
851
+
852
+ /** n bytes at offset off of a chunk list (a view when it is in one chunk). */
853
+ function peek(chunks, off, n) {
854
+ let i = 0
855
+ while (off >= chunks[i].length) off -= chunks[i++].length
856
+ if (chunks[i].length - off >= n) return chunks[i].subarray(off, off + n)
857
+ const out = new Uint8Array(n)
858
+ let o = 0
859
+ while (o < n) {
860
+ const c = chunks[i++]
861
+ const k = Math.min(c.length - off, n - o)
862
+ out.set(c.subarray(off, off + k), o)
863
+ o += k
864
+ off = 0
865
+ }
866
+ return out
867
+ }