@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,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 }