@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/proto.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// P-B10: the relay protocol, shared by the browser's network Worker (net-worker.js) and the
|
|
2
|
+
// relay server (web/relay/relay.ts).
|
|
3
|
+
//
|
|
4
|
+
// One WebSocket per guest machine carries every TCP stream of that guest and its DNS
|
|
5
|
+
// lookups. Binary messages only (the relay's first message is a JSON text "hello"):
|
|
6
|
+
//
|
|
7
|
+
// type u8 | id u32 BE | payload
|
|
8
|
+
//
|
|
9
|
+
// Client -> relay
|
|
10
|
+
// CONNECT port u16 BE | host utf8 open stream `id` to host:port (an IP literal; a name
|
|
11
|
+
// is resolved by the relay and checked after resolution)
|
|
12
|
+
// DATA bytes at most the credit the relay granted for the stream
|
|
13
|
+
// END - half-close (the guest sent FIN)
|
|
14
|
+
// ABORT - reset (the guest sent RST, or the stream is gone)
|
|
15
|
+
// CREDIT n u32 BE the relay may send n more bytes on the stream
|
|
16
|
+
// DNS qtype u16 BE | name utf8 look up `name` (qtype 1 = A, 28 = AAAA); `id` is the query's
|
|
17
|
+
//
|
|
18
|
+
// Relay -> client
|
|
19
|
+
// CONNECTED code u8 | credit u32 BE | detail utf8 code 0: open, the client may send `credit` bytes;
|
|
20
|
+
// else refused (detail says why)
|
|
21
|
+
// DATA bytes at most the credit the client granted (WINDOW at first)
|
|
22
|
+
// END - the remote closed cleanly (FIN): no more data
|
|
23
|
+
// ABORT code u8 | reason utf8 the stream failed (remote reset, relay limit, ...):
|
|
24
|
+
// NOT a clean end; data may be missing
|
|
25
|
+
// CREDIT n u32 BE the client may send n more bytes
|
|
26
|
+
// DNSREPLY rcode u8 | ttl u32 BE | count u8 | addresses (4 or 16 bytes each) | reason utf8
|
|
27
|
+
//
|
|
28
|
+
// Flow control: each side may have at most the credit the other granted in flight per
|
|
29
|
+
// stream; a stream starts with WINDOW bytes of credit for the relay (the client grants it
|
|
30
|
+
// by sending CONNECT) and the credit in CONNECTED for the client. Credit comes back as the
|
|
31
|
+
// receiver passes the bytes on.
|
|
32
|
+
|
|
33
|
+
export const PROTOCOL = 'wmd-relay.v1'
|
|
34
|
+
export const WINDOW = 256 << 10
|
|
35
|
+
export const MAX_DATA = 64 << 10 // largest DATA payload either side sends
|
|
36
|
+
|
|
37
|
+
export const T = {
|
|
38
|
+
CONNECT: 0x01, DATA: 0x02, END: 0x03, ABORT: 0x04, CREDIT: 0x05, DNS: 0x06,
|
|
39
|
+
CONNECTED: 0x81, RDATA: 0x82, REND: 0x83, RABORT: 0x84, RCREDIT: 0x85, DNSREPLY: 0x86,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why a connect or a stream failed (CONNECTED code, ABORT code). */
|
|
43
|
+
export const E = {
|
|
44
|
+
OK: 0,
|
|
45
|
+
BLOCKED: 1, // the address is private, loopback, link-local, metadata, reserved or the relay's own
|
|
46
|
+
NOT_ALLOWED: 2, // not on the relay's allowlist
|
|
47
|
+
REFUSED: 3, // the remote refused (ECONNREFUSED)
|
|
48
|
+
TIMEOUT: 4,
|
|
49
|
+
DNS: 5, // the name did not resolve
|
|
50
|
+
LIMIT: 6, // a relay limit (connections, rate, bytes)
|
|
51
|
+
BAD: 7, // malformed request
|
|
52
|
+
UNREACHABLE: 8,
|
|
53
|
+
PORT: 9, // the port is blocked by policy
|
|
54
|
+
RESET: 10, // the remote reset or failed mid-stream
|
|
55
|
+
GONE: 11, // the session is closing
|
|
56
|
+
}
|
|
57
|
+
export const E_NAMES = Object.fromEntries(Object.entries(E).map(([k, v]) => [v, k.toLowerCase()]))
|
|
58
|
+
|
|
59
|
+
/** DNS rcodes used in DNSREPLY (RFC 1035). */
|
|
60
|
+
export const RCODE = { NOERROR: 0, SERVFAIL: 2, NXDOMAIN: 3, REFUSED: 5 }
|
|
61
|
+
|
|
62
|
+
const enc = new TextEncoder()
|
|
63
|
+
const dec = new TextDecoder()
|
|
64
|
+
|
|
65
|
+
export function msg(type, id, payload) {
|
|
66
|
+
const n = payload ? payload.length : 0
|
|
67
|
+
const b = new Uint8Array(5 + n)
|
|
68
|
+
b[0] = type
|
|
69
|
+
new DataView(b.buffer).setUint32(1, id >>> 0)
|
|
70
|
+
if (n) b.set(payload, 5)
|
|
71
|
+
return b
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function connectMsg(id, host, port) {
|
|
75
|
+
const h = enc.encode(host)
|
|
76
|
+
const p = new Uint8Array(2 + h.length)
|
|
77
|
+
p[0] = port >> 8
|
|
78
|
+
p[1] = port & 255
|
|
79
|
+
p.set(h, 2)
|
|
80
|
+
return msg(T.CONNECT, id, p)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function u32Msg(type, id, n) {
|
|
84
|
+
const p = new Uint8Array(4)
|
|
85
|
+
new DataView(p.buffer).setUint32(0, n >>> 0)
|
|
86
|
+
return msg(type, id, p)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function dnsMsg(id, name, qtype) {
|
|
90
|
+
const h = enc.encode(name)
|
|
91
|
+
const p = new Uint8Array(2 + h.length)
|
|
92
|
+
p[0] = qtype >> 8
|
|
93
|
+
p[1] = qtype & 255
|
|
94
|
+
p.set(h, 2)
|
|
95
|
+
return msg(T.DNS, id, p)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function connectedMsg(id, code, credit, detail = '') {
|
|
99
|
+
const d = enc.encode(detail)
|
|
100
|
+
const p = new Uint8Array(5 + d.length)
|
|
101
|
+
p[0] = code
|
|
102
|
+
new DataView(p.buffer).setUint32(1, credit >>> 0)
|
|
103
|
+
p.set(d, 5)
|
|
104
|
+
return msg(T.CONNECTED, id, p)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function abortMsg(id, code, reason = '') {
|
|
108
|
+
const d = enc.encode(reason)
|
|
109
|
+
const p = new Uint8Array(1 + d.length)
|
|
110
|
+
p[0] = code
|
|
111
|
+
p.set(d, 1)
|
|
112
|
+
return msg(T.RABORT, id, p)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** addrs: arrays of 4 (A) or 16 (AAAA) bytes. */
|
|
116
|
+
export function dnsReplyMsg(id, rcode, ttl, addrs = [], reason = '') {
|
|
117
|
+
const d = enc.encode(reason)
|
|
118
|
+
const alen = addrs.reduce((s, a) => s + a.length, 0)
|
|
119
|
+
const p = new Uint8Array(6 + alen + d.length)
|
|
120
|
+
p[0] = rcode
|
|
121
|
+
new DataView(p.buffer).setUint32(1, ttl >>> 0)
|
|
122
|
+
p[5] = addrs.length
|
|
123
|
+
let o = 6
|
|
124
|
+
for (const a of addrs) {
|
|
125
|
+
p.set(a, o)
|
|
126
|
+
o += a.length
|
|
127
|
+
}
|
|
128
|
+
p.set(d, o)
|
|
129
|
+
return msg(T.DNSREPLY, id, p)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Parse one message; returns null when it is too short. */
|
|
133
|
+
export function parse(u8) {
|
|
134
|
+
if (u8.length < 5) return null
|
|
135
|
+
const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength)
|
|
136
|
+
const type = u8[0]
|
|
137
|
+
const id = dv.getUint32(1)
|
|
138
|
+
const p = u8.subarray(5)
|
|
139
|
+
const m = { type, id, payload: p }
|
|
140
|
+
switch (type) {
|
|
141
|
+
case T.CONNECT:
|
|
142
|
+
if (p.length < 3) return null
|
|
143
|
+
m.port = (p[0] << 8) | p[1]
|
|
144
|
+
m.host = dec.decode(p.subarray(2))
|
|
145
|
+
break
|
|
146
|
+
case T.DNS:
|
|
147
|
+
if (p.length < 3) return null
|
|
148
|
+
m.qtype = (p[0] << 8) | p[1]
|
|
149
|
+
m.name = dec.decode(p.subarray(2))
|
|
150
|
+
break
|
|
151
|
+
case T.CREDIT:
|
|
152
|
+
case T.RCREDIT:
|
|
153
|
+
if (p.length !== 4) return null
|
|
154
|
+
m.n = dv.getUint32(5)
|
|
155
|
+
break
|
|
156
|
+
case T.CONNECTED:
|
|
157
|
+
if (p.length < 5) return null
|
|
158
|
+
m.code = p[0]
|
|
159
|
+
m.credit = dv.getUint32(6)
|
|
160
|
+
m.detail = dec.decode(p.subarray(5))
|
|
161
|
+
break
|
|
162
|
+
case T.RABORT:
|
|
163
|
+
if (p.length < 1) return null
|
|
164
|
+
m.code = p[0]
|
|
165
|
+
m.reason = dec.decode(p.subarray(1))
|
|
166
|
+
break
|
|
167
|
+
case T.DNSREPLY: {
|
|
168
|
+
if (p.length < 6) return null
|
|
169
|
+
m.rcode = p[0]
|
|
170
|
+
m.ttl = dv.getUint32(6)
|
|
171
|
+
m.count = p[5]
|
|
172
|
+
m.rest = p.subarray(6) // the addresses (their size depends on the query), then the reason
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return m
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The addresses of a DNSREPLY for a query of qtype (1: 4 bytes each, 28: 16). */
|
|
180
|
+
export function dnsReplyAddrs(m, qtype) {
|
|
181
|
+
const size = qtype === 28 ? 16 : 4
|
|
182
|
+
const addrs = []
|
|
183
|
+
for (let i = 0; i < m.count; i++) {
|
|
184
|
+
const a = m.rest.subarray(i * size, (i + 1) * size)
|
|
185
|
+
if (a.length !== size) return { addrs, reason: 'short reply' }
|
|
186
|
+
addrs.push(a)
|
|
187
|
+
}
|
|
188
|
+
return { addrs, reason: dec.decode(m.rest.subarray(m.count * size)) }
|
|
189
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// P-B10: the browser side of the relay protocol (proto.js): one WebSocket to the relay
|
|
2
|
+
// server for the whole guest, opened when the guest first needs it and again after a drop
|
|
3
|
+
// (with a backoff), never in a loop on its own. The stack (stack.js) calls connect / data /
|
|
4
|
+
// end / abort / credit / dns; the client calls the stack back (onConnected, onRelayData,
|
|
5
|
+
// onRelayEnd, onRelayAbort, onRelayCredit, onDnsReply, onRelayDown).
|
|
6
|
+
//
|
|
7
|
+
// A drop of the WebSocket fails every stream (onRelayDown: the stack resets them; the
|
|
8
|
+
// relay forgets them too), so nothing resumes half-way and nothing ends as if complete.
|
|
9
|
+
|
|
10
|
+
import { E, PROTOCOL, RCODE, T, connectMsg, dnsMsg, dnsReplyAddrs, msg, parse, u32Msg } from './proto.js'
|
|
11
|
+
|
|
12
|
+
const BACKOFF_MIN_MS = 250
|
|
13
|
+
const BACKOFF_MAX_MS = 8000
|
|
14
|
+
const OPEN_TIMEOUT_MS = 10_000
|
|
15
|
+
const QUEUE_MAX_MS = 8000 // a connect or lookup waits this long for the relay to come back
|
|
16
|
+
|
|
17
|
+
export class RelayClient {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} o
|
|
20
|
+
* @param {string} o.url ws(s)://host:port/relay
|
|
21
|
+
* @param {string} [o.token] the session token (sent as a WebSocket subprotocol "t.<token>")
|
|
22
|
+
* @param {string} [o.tokenUrl] fetch a fresh token from here before each open (JSON {token}),
|
|
23
|
+
* with ?instance=<this guest's id>: the relay keeps this guest's caps
|
|
24
|
+
* across its tokens and reconnects
|
|
25
|
+
* @param {string} [o.instance] this guest's id (default: a random one per client)
|
|
26
|
+
* @param {typeof WebSocket} [o.WebSocket]
|
|
27
|
+
* @param {(line: string) => void} [o.log]
|
|
28
|
+
*/
|
|
29
|
+
constructor(o) {
|
|
30
|
+
this.url = o.url
|
|
31
|
+
this.token = o.token || ''
|
|
32
|
+
this.tokenUrl = o.tokenUrl || ''
|
|
33
|
+
this.instance = o.instance || Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('')
|
|
34
|
+
this.WS = o.WebSocket || globalThis.WebSocket
|
|
35
|
+
this.log = o.log || (() => {})
|
|
36
|
+
this.now = o.now || (() => performance.now())
|
|
37
|
+
this.stack = null
|
|
38
|
+
this.ws = null
|
|
39
|
+
this.up = false
|
|
40
|
+
this.connecting = false
|
|
41
|
+
this.queue = [] // messages waiting for the socket
|
|
42
|
+
this.backoff = 0
|
|
43
|
+
this.nextAt = 0
|
|
44
|
+
this.timer = null
|
|
45
|
+
this.hello = null
|
|
46
|
+
this.stats = { opens: 0, openFailures: 0, drops: 0, sent: 0, received: 0, lastClose: null, lastError: null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
attach(stack) {
|
|
50
|
+
this.stack = stack
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#send(m) {
|
|
54
|
+
if (this.up) {
|
|
55
|
+
this.stats.sent += m.length
|
|
56
|
+
this.ws.send(m)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
this.queue.push({ m, at: this.now() })
|
|
60
|
+
this.wake()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Open the socket if it is not open (now, or when the backoff allows). */
|
|
64
|
+
wake() {
|
|
65
|
+
if (this.up || this.connecting || this.timer) return
|
|
66
|
+
const wait = this.nextAt - this.now()
|
|
67
|
+
if (wait > 0) {
|
|
68
|
+
this.timer = setTimeout(() => {
|
|
69
|
+
this.timer = null
|
|
70
|
+
this.#open()
|
|
71
|
+
}, wait)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
this.#open()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #open() {
|
|
78
|
+
if (this.up || this.connecting) return
|
|
79
|
+
this.connecting = true
|
|
80
|
+
let token = this.token
|
|
81
|
+
try {
|
|
82
|
+
if (this.tokenUrl) {
|
|
83
|
+
const u = new URL(this.tokenUrl, globalThis.location?.href)
|
|
84
|
+
u.searchParams.set('instance', this.instance)
|
|
85
|
+
const r = await fetch(u, { cache: 'no-store' })
|
|
86
|
+
if (!r.ok) throw new Error(`token: GET ${this.tokenUrl}: ${r.status}`)
|
|
87
|
+
token = (await r.json()).token
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return this.#failed(e.message)
|
|
91
|
+
}
|
|
92
|
+
let ws
|
|
93
|
+
try {
|
|
94
|
+
ws = new this.WS(this.url, token ? [PROTOCOL, `t.${token}`] : [PROTOCOL])
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return this.#failed(e.message)
|
|
97
|
+
}
|
|
98
|
+
ws.binaryType = 'arraybuffer'
|
|
99
|
+
this.ws = ws
|
|
100
|
+
const openTimer = setTimeout(() => ws.readyState !== 1 && ws.close(), OPEN_TIMEOUT_MS)
|
|
101
|
+
ws.onopen = () => {
|
|
102
|
+
clearTimeout(openTimer)
|
|
103
|
+
if (this.ws !== ws) return
|
|
104
|
+
this.connecting = false
|
|
105
|
+
this.up = true
|
|
106
|
+
this.backoff = 0
|
|
107
|
+
this.stats.opens++
|
|
108
|
+
const q = this.queue
|
|
109
|
+
this.queue = []
|
|
110
|
+
for (const x of q) this.#send(x.m)
|
|
111
|
+
}
|
|
112
|
+
ws.onmessage = e => {
|
|
113
|
+
if (this.ws !== ws) return
|
|
114
|
+
if (typeof e.data === 'string') {
|
|
115
|
+
try {
|
|
116
|
+
this.hello = JSON.parse(e.data)
|
|
117
|
+
} catch {}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const u8 = new Uint8Array(e.data)
|
|
121
|
+
this.stats.received += u8.length
|
|
122
|
+
this.#dispatch(u8)
|
|
123
|
+
}
|
|
124
|
+
ws.onerror = () => {
|
|
125
|
+
this.stats.lastError = 'websocket error'
|
|
126
|
+
}
|
|
127
|
+
ws.onclose = e => {
|
|
128
|
+
clearTimeout(openTimer)
|
|
129
|
+
if (this.ws !== ws) return
|
|
130
|
+
const wasUp = this.up
|
|
131
|
+
this.ws = null
|
|
132
|
+
this.up = false
|
|
133
|
+
this.stats.lastClose = { code: e.code, reason: e.reason, at: Math.round(this.now()) }
|
|
134
|
+
if (wasUp) {
|
|
135
|
+
this.stats.drops++
|
|
136
|
+
this.backoff = BACKOFF_MIN_MS
|
|
137
|
+
this.nextAt = this.now() + this.backoff
|
|
138
|
+
this.log(`net: the relay connection closed (${e.code}${e.reason ? ` ${e.reason}` : ''}): every connection is reset`)
|
|
139
|
+
this.stack?.onRelayDown(`relay connection closed (${e.code}${e.reason ? ` ${e.reason}` : ''})`)
|
|
140
|
+
} else this.#failed(`could not connect to the relay (${e.code}${e.reason ? ` ${e.reason}` : ''})`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#failed(why) {
|
|
145
|
+
this.connecting = false
|
|
146
|
+
this.ws = null
|
|
147
|
+
this.stats.openFailures++
|
|
148
|
+
this.stats.lastError = why
|
|
149
|
+
this.backoff = Math.min(Math.max(this.backoff * 2, BACKOFF_MIN_MS), BACKOFF_MAX_MS)
|
|
150
|
+
this.nextAt = this.now() + this.backoff
|
|
151
|
+
this.log(`net: ${why}; next try in ${this.backoff} ms`)
|
|
152
|
+
// what waited for this open keeps waiting (the relay may be restarting) for up to
|
|
153
|
+
// QUEUE_MAX_MS; older connects are refused (RST) and older lookups get SERVFAIL
|
|
154
|
+
const t = this.now()
|
|
155
|
+
const keep = []
|
|
156
|
+
for (const x of this.queue) {
|
|
157
|
+
if (t - x.at < QUEUE_MAX_MS) keep.push(x)
|
|
158
|
+
else {
|
|
159
|
+
const id = new DataView(x.m.buffer, x.m.byteOffset).getUint32(1)
|
|
160
|
+
if (x.m[0] === T.CONNECT) this.stack?.onConnected(id, E.GONE, 0, why)
|
|
161
|
+
else if (x.m[0] === T.DNS) this.stack?.onDnsReply(id, RCODE.SERVFAIL, 0, [], why)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.queue = keep
|
|
165
|
+
if (keep.length) this.wake()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#dispatch(u8) {
|
|
169
|
+
const m = parse(u8)
|
|
170
|
+
const s = this.stack
|
|
171
|
+
if (!m || !s) return
|
|
172
|
+
switch (m.type) {
|
|
173
|
+
case T.CONNECTED:
|
|
174
|
+
s.onConnected(m.id, m.code, m.credit, m.detail)
|
|
175
|
+
break
|
|
176
|
+
case T.RDATA:
|
|
177
|
+
if (m.payload.length) s.onRelayData(m.id, m.payload.slice())
|
|
178
|
+
break
|
|
179
|
+
case T.REND:
|
|
180
|
+
s.onRelayEnd(m.id)
|
|
181
|
+
break
|
|
182
|
+
case T.RABORT:
|
|
183
|
+
s.onRelayAbort(m.id, m.code, m.reason)
|
|
184
|
+
break
|
|
185
|
+
case T.RCREDIT:
|
|
186
|
+
s.onRelayCredit(m.id, m.n)
|
|
187
|
+
break
|
|
188
|
+
case T.DNSREPLY: {
|
|
189
|
+
const { addrs, reason } = dnsReplyAddrs(m, 1)
|
|
190
|
+
s.onDnsReply(m.id, m.rcode, m.ttl, addrs, reason)
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- the stack's interface ----
|
|
197
|
+
connect(id, host, port) {
|
|
198
|
+
this.#send(connectMsg(id, host, port))
|
|
199
|
+
}
|
|
200
|
+
data(id, u8) {
|
|
201
|
+
if (this.up) this.#send(msg(T.DATA, id, u8))
|
|
202
|
+
}
|
|
203
|
+
end(id) {
|
|
204
|
+
if (this.up) this.#send(msg(T.END, id))
|
|
205
|
+
}
|
|
206
|
+
abort(id) {
|
|
207
|
+
if (this.up) this.#send(msg(T.ABORT, id))
|
|
208
|
+
}
|
|
209
|
+
credit(id, n) {
|
|
210
|
+
if (this.up && n > 0) this.#send(u32Msg(T.CREDIT, id, n))
|
|
211
|
+
}
|
|
212
|
+
dns(id, name, qtype) {
|
|
213
|
+
this.#send(dnsMsg(id, name, qtype))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Test hook: drop the WebSocket as a network failure would. */
|
|
217
|
+
drop() {
|
|
218
|
+
this.ws?.close(4000, 'dropped by the page (test hook)')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
snapshot() {
|
|
222
|
+
return { url: this.url, up: this.up, connecting: this.connecting, hello: this.hello, ...this.stats }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { E, RCODE }
|