@threadedstack/wmd-web 0.0.0-stage → 0.1.13
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/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
package/src/net/stack.js
ADDED
|
@@ -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
|
+
}
|