@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,172 @@
1
+ // P-B10: the guest network's Worker. It runs the TCP/IP stack (stack.js) and the relay's
2
+ // WebSocket (relay-client.js), off the page and off the channel service.
3
+ //
4
+ // The channel service (chan-service.js, NetSession in chan-services.js) owns the net
5
+ // channel's rings and talks to this Worker over a MessagePort:
6
+ // service -> here {t:'in', buf} bytes the guest's virtio-net wrote: 4-byte BE length + frame, as a stream
7
+ // {t:'wrote', n} n of the bytes we sent were written into the guest's ring
8
+ // here -> service {t:'out', buf} frames for the guest, length-prefixed the same way
9
+ // {t:'took', n} n input bytes were processed (the service reads more only then)
10
+ // The page: {t:'init', port, relay?: {url, protocol?, token?, tokenUrl?, key?}, instance?, names?, switchPort?, config?, maxConns?}, {t:'stats'},
11
+ // (W59: protocol 'wisp' uses wisp-client.js; instance is the machine's id, names its placeholder
12
+ // map [[name, ip]], kept by the page across restores; {t:'names', id} asks for the whole map)
13
+ // (W56: switchPort is the guest's port on an in-page switch, switch.js; with no relay only
14
+ // the guests on the switch are reachable)
15
+ // {t:'drop-relay'} (test hook: drop the WebSocket).
16
+ // Messages out to the page: {t:'stats', stats}, {t:'log', line}, W59: {t:'name', name, ip} (a new placeholder),
17
+ // {t:'status', status} (vm.net.onStatus), {t:'names', id, names}.
18
+
19
+ import { NetStack } from './stack.js'
20
+ import { RelayClient } from './relay-client.js'
21
+ import { WispClient } from './wisp-client.js'
22
+ import { PeerNet } from './peer.js'
23
+
24
+ const RING_INFLIGHT = 1 << 20 // bytes sent to the service not yet in the guest's ring, at most
25
+ const MAX_FRAME = 65536
26
+
27
+ let stack = null
28
+ let relay = null
29
+ let peers = null
30
+ let port = null
31
+ let inflight = 0 // bytes posted to the service, not yet written into the ring
32
+ let pending = [] // output frames of this turn
33
+ let pendingBytes = 0
34
+ let flushQueued = false
35
+ // the input stream's partial frame
36
+ let hdr = new Uint8Array(4)
37
+ let hdrN = 0
38
+ let frame = null
39
+ let frameN = 0
40
+
41
+ const log = line => self.postMessage({ t: 'log', line })
42
+
43
+ self.onmessage = e => {
44
+ const m = e.data
45
+ if (m.t === 'init') init(m)
46
+ else if (m.t === 'stats') self.postMessage({ t: 'stats', id: m.id, stats: snapshot() })
47
+ else if (m.t === 'drop-relay') relay?.drop()
48
+ else if (m.t === 'names') self.postMessage({ t: 'names', id: m.id, names: relay?.nameList?.() ?? [] })
49
+ }
50
+
51
+ function snapshot() {
52
+ return stack ? { stack: stack.snapshot(), relay: relay?.snapshot() ?? null, peers: peers?.snapshot() ?? null, ringInflight: inflight } : null
53
+ }
54
+
55
+ function init(m) {
56
+ port = m.port
57
+ if (m.relay?.protocol === 'wisp') {
58
+ relay = new WispClient({
59
+ url: m.relay.url, token: m.relay.token, tokenUrl: m.relay.tokenUrl, key: m.relay.key, instance: m.instance, names: m.names, log,
60
+ onName: (name, ip) => self.postMessage({ t: 'name', name, ip }),
61
+ onStatus: status => self.postMessage({ t: 'status', status }),
62
+ })
63
+ } else if (m.relay) relay = new RelayClient({ url: m.relay.url, token: m.relay.token, tokenUrl: m.relay.tokenUrl, instance: m.instance, log })
64
+ if (m.switchPort) peers = new PeerNet({ port: m.switchPort, log })
65
+ stack = new NetStack({ send: queueOut, relay, peers, config: m.config, maxConns: m.maxConns, log, txRoom: () => RING_INFLIGHT - inflight - pendingBytes })
66
+ relay?.attach(stack)
67
+ peers?.attach(stack)
68
+ wrapCallbacks()
69
+ port.onmessage = ev => {
70
+ const x = ev.data
71
+ if (x.t === 'in') {
72
+ const u8 = new Uint8Array(x.buf)
73
+ feed(u8)
74
+ stack.flushAcks()
75
+ port.postMessage({ t: 'took', n: u8.length })
76
+ } else if (x.t === 'wrote') {
77
+ inflight -= x.n
78
+ stack.pump()
79
+ }
80
+ flushOut()
81
+ }
82
+ setInterval(() => {
83
+ stack.tick()
84
+ flushOut()
85
+ }, 100)
86
+ // stats for the page while the guest runs
87
+ let last = ''
88
+ setInterval(() => {
89
+ const s = JSON.stringify(snapshot())
90
+ if (s !== last) self.postMessage({ t: 'stats', stats: JSON.parse(s) })
91
+ last = s
92
+ }, 1000)
93
+ self.postMessage({ t: 'ready' })
94
+ }
95
+
96
+ /** After relay callbacks (they run outside our port handler), send what they produced. */
97
+ function wrapCallbacks() {
98
+ for (const k of ['onRelayDown', 'onConnected', 'onRelayData', 'onRelayEnd', 'onRelayAbort', 'onRelayCredit', 'onDnsReply', 'peerOpen']) {
99
+ const g = stack[k].bind(stack)
100
+ stack[k] = (...a) => {
101
+ const r = g(...a)
102
+ scheduleFlush()
103
+ return r
104
+ }
105
+ }
106
+ }
107
+
108
+ function scheduleFlush() {
109
+ if (flushQueued) return
110
+ flushQueued = true
111
+ queueMicrotask(() => {
112
+ flushQueued = false
113
+ flushOut()
114
+ })
115
+ }
116
+
117
+ function feed(u8) {
118
+ let i = 0
119
+ while (i < u8.length) {
120
+ if (!frame) {
121
+ const k = Math.min(4 - hdrN, u8.length - i)
122
+ hdr.set(u8.subarray(i, i + k), hdrN)
123
+ hdrN += k
124
+ i += k
125
+ if (hdrN < 4) break
126
+ hdrN = 0
127
+ const len = ((hdr[0] << 24) | (hdr[1] << 16) | (hdr[2] << 8) | hdr[3]) >>> 0
128
+ if (len === 0) continue // keepalive
129
+ if (len > MAX_FRAME) {
130
+ log(`net: a frame of ${len} bytes from the guest; the stream is out of step, dropping the input`)
131
+ return
132
+ }
133
+ frame = new Uint8Array(len)
134
+ frameN = 0
135
+ continue
136
+ }
137
+ const k = Math.min(frame.length - frameN, u8.length - i)
138
+ frame.set(u8.subarray(i, i + k), frameN)
139
+ frameN += k
140
+ i += k
141
+ if (frameN === frame.length) {
142
+ const f = frame
143
+ frame = null
144
+ stack.input(f)
145
+ }
146
+ }
147
+ }
148
+
149
+ function queueOut(f) {
150
+ pending.push(f)
151
+ pendingBytes += 4 + f.length
152
+ }
153
+
154
+ function flushOut() {
155
+ stack.flushAcks()
156
+ if (!pending.length) return
157
+ const buf = new Uint8Array(pendingBytes)
158
+ let o = 0
159
+ for (const f of pending) {
160
+ const n = f.length
161
+ buf[o] = n >>> 24
162
+ buf[o + 1] = (n >>> 16) & 255
163
+ buf[o + 2] = (n >>> 8) & 255
164
+ buf[o + 3] = n & 255
165
+ buf.set(f, o + 4)
166
+ o += 4 + n
167
+ }
168
+ inflight += pendingBytes
169
+ pending = []
170
+ pendingBytes = 0
171
+ port.postMessage({ t: 'out', buf: buf.buffer }, [buf.buffer])
172
+ }
@@ -0,0 +1,227 @@
1
+ // P-B10: Ethernet, ARP, IPv4, ICMP, UDP and TCP framing for the guest's virtual network.
2
+ // Pure functions over Uint8Arrays; the stack (stack.js) builds and reads every frame here.
3
+
4
+ export const ETH_IPV4 = 0x0800
5
+ export const ETH_ARP = 0x0806
6
+ export const ETH_HLEN = 14
7
+ export const IP_ICMP = 1
8
+ export const IP_TCP = 6
9
+ export const IP_UDP = 17
10
+
11
+ export const TCP_FIN = 0x01
12
+ export const TCP_SYN = 0x02
13
+ export const TCP_RST = 0x04
14
+ export const TCP_PSH = 0x08
15
+ export const TCP_ACK = 0x10
16
+
17
+ export function ipToU32(s) {
18
+ const p = s.split('.').map(Number)
19
+ return ((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) >>> 0
20
+ }
21
+ export function u32ToIp(n) {
22
+ return `${n >>> 24}.${(n >>> 16) & 255}.${(n >>> 8) & 255}.${n & 255}`
23
+ }
24
+ export function macBytes(s) {
25
+ return Uint8Array.from(s.split(':').map(x => parseInt(x, 16)))
26
+ }
27
+ export function macStr(b) {
28
+ return Array.from(b, x => x.toString(16).padStart(2, '0')).join(':')
29
+ }
30
+
31
+ /** The Internet checksum's running 32-bit sum over bytes [start, end). */
32
+ export function sum16(u8, start, end, acc = 0) {
33
+ let i = start
34
+ for (; i + 1 < end; i += 2) acc += (u8[i] << 8) | u8[i + 1]
35
+ if (i < end) acc += u8[i] << 8
36
+ return acc
37
+ }
38
+ export function fold(acc) {
39
+ while (acc > 0xffff) acc = (acc & 0xffff) + Math.floor(acc / 0x10000)
40
+ return ~acc & 0xffff
41
+ }
42
+
43
+ const rd16 = (b, o) => (b[o] << 8) | b[o + 1]
44
+ const rd32 = (b, o) => ((b[o] << 24) | (b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]) >>> 0
45
+ const wr16 = (b, o, v) => {
46
+ b[o] = (v >>> 8) & 255
47
+ b[o + 1] = v & 255
48
+ }
49
+ const wr32 = (b, o, v) => {
50
+ b[o] = (v >>> 24) & 255
51
+ b[o + 1] = (v >>> 16) & 255
52
+ b[o + 2] = (v >>> 8) & 255
53
+ b[o + 3] = v & 255
54
+ }
55
+ export { rd16, rd32, wr16, wr32 }
56
+
57
+ /** Parse an Ethernet frame (no VLAN tags). */
58
+ export function parseEth(f) {
59
+ if (f.length < ETH_HLEN) return null
60
+ return { dst: f.subarray(0, 6), src: f.subarray(6, 12), type: rd16(f, 12), payload: f.subarray(ETH_HLEN) }
61
+ }
62
+
63
+ /** ARP request/reply (Ethernet + IPv4 only). */
64
+ export function parseArp(p) {
65
+ if (p.length < 28 || rd16(p, 0) !== 1 || rd16(p, 2) !== ETH_IPV4 || p[4] !== 6 || p[5] !== 4) return null
66
+ return { op: rd16(p, 6), sha: p.subarray(8, 14), spa: rd32(p, 14), tha: p.subarray(18, 24), tpa: rd32(p, 24) }
67
+ }
68
+
69
+ export function arpReply(myMac, myIp, toMac, toIp) {
70
+ const f = new Uint8Array(ETH_HLEN + 28)
71
+ f.set(toMac, 0)
72
+ f.set(myMac, 6)
73
+ wr16(f, 12, ETH_ARP)
74
+ const o = ETH_HLEN
75
+ wr16(f, o, 1)
76
+ wr16(f, o + 2, ETH_IPV4)
77
+ f[o + 4] = 6
78
+ f[o + 5] = 4
79
+ wr16(f, o + 6, 2)
80
+ f.set(myMac, o + 8)
81
+ wr32(f, o + 14, myIp)
82
+ f.set(toMac, o + 18)
83
+ wr32(f, o + 24, toIp)
84
+ return f
85
+ }
86
+
87
+ /** Parse an IPv4 packet. Fragments and bad headers return null. */
88
+ export function parseIp(p) {
89
+ if (p.length < 20 || p[0] >> 4 !== 4) return null
90
+ const ihl = (p[0] & 15) * 4
91
+ const total = rd16(p, 2)
92
+ if (ihl < 20 || total < ihl || total > p.length) return null
93
+ const frag = rd16(p, 6)
94
+ if (frag & 0x2000 || frag & 0x1fff) return { fragment: true } // MF or an offset: not supported
95
+ return { proto: p[9], src: rd32(p, 12), dst: rd32(p, 16), ttl: p[8], header: p.subarray(0, ihl), payload: p.subarray(ihl, total) }
96
+ }
97
+
98
+ let ipId = 1
99
+ /**
100
+ * A whole Ethernet + IPv4 frame with an l4len-byte transport part; fill(frame, offset)
101
+ * writes the transport bytes, then the IPv4 checksum (and the TCP/UDP checksum) is set.
102
+ */
103
+ export function ipFrame(srcMac, dstMac, src, dst, proto, l4len, fill) {
104
+ const f = new Uint8Array(ETH_HLEN + 20 + l4len)
105
+ f.set(dstMac, 0)
106
+ f.set(srcMac, 6)
107
+ wr16(f, 12, ETH_IPV4)
108
+ const o = ETH_HLEN
109
+ f[o] = 0x45
110
+ wr16(f, o + 2, 20 + l4len)
111
+ wr16(f, o + 4, ipId)
112
+ ipId = (ipId + 1) & 0xffff
113
+ wr16(f, o + 6, 0x4000) // DF
114
+ f[o + 8] = 64
115
+ f[o + 9] = proto
116
+ wr32(f, o + 12, src)
117
+ wr32(f, o + 16, dst)
118
+ wr16(f, o + 10, fold(sum16(f, o, o + 20)))
119
+ const t = o + 20
120
+ fill(f, t)
121
+ if (proto === IP_TCP || proto === IP_UDP) {
122
+ const ck = proto === IP_TCP ? t + 16 : t + 6
123
+ f[ck] = f[ck + 1] = 0
124
+ let acc = sum16(f, o + 12, o + 20) + proto + l4len // pseudo-header
125
+ acc = sum16(f, t, t + l4len, acc)
126
+ let c = fold(acc)
127
+ if (proto === IP_UDP && c === 0) c = 0xffff
128
+ wr16(f, ck, c)
129
+ } else if (proto === IP_ICMP) {
130
+ f[t + 2] = f[t + 3] = 0
131
+ wr16(f, t + 2, fold(sum16(f, t, t + l4len)))
132
+ }
133
+ return f
134
+ }
135
+
136
+ export function parseUdp(p) {
137
+ if (p.length < 8) return null
138
+ const len = rd16(p, 4)
139
+ if (len < 8 || len > p.length) return null
140
+ return { sport: rd16(p, 0), dport: rd16(p, 2), payload: p.subarray(8, len) }
141
+ }
142
+
143
+ export function udpFrame(srcMac, dstMac, src, dst, sport, dport, data) {
144
+ return ipFrame(srcMac, dstMac, src, dst, IP_UDP, 8 + data.length, (f, t) => {
145
+ wr16(f, t, sport)
146
+ wr16(f, t + 2, dport)
147
+ wr16(f, t + 4, 8 + data.length)
148
+ f.set(data, t + 8)
149
+ })
150
+ }
151
+
152
+ /** Parse a TCP segment with its options (MSS, window scale). */
153
+ export function parseTcp(p) {
154
+ if (p.length < 20) return null
155
+ const off = (p[12] >> 4) * 4
156
+ if (off < 20 || off > p.length) return null
157
+ const s = {
158
+ sport: rd16(p, 0), dport: rd16(p, 2), seq: rd32(p, 4), ack: rd32(p, 8), flags: p[13] & 0x3f, win: rd16(p, 14),
159
+ mss: 0, wscale: -1, payload: p.subarray(off),
160
+ }
161
+ if (s.flags & TCP_SYN) {
162
+ for (let i = 20; i < off; ) {
163
+ const kind = p[i]
164
+ if (kind === 0) break
165
+ if (kind === 1) {
166
+ i++
167
+ continue
168
+ }
169
+ const len = p[i + 1]
170
+ if (len < 2 || i + len > off) break
171
+ if (kind === 2 && len === 4) s.mss = rd16(p, i + 2)
172
+ else if (kind === 3 && len === 3) s.wscale = Math.min(p[i + 2], 14)
173
+ i += len
174
+ }
175
+ }
176
+ return s
177
+ }
178
+
179
+ /** A TCP segment frame. opts: {mss, wscale} only on SYN. */
180
+ export function tcpFrame(srcMac, dstMac, src, dst, sport, dport, seq, ack, flags, win, data, opts) {
181
+ const olen = opts ? 8 : 0 // MSS (4) + NOP + window scale (3)
182
+ const dlen = data ? data.length : 0
183
+ return ipFrame(srcMac, dstMac, src, dst, IP_TCP, 20 + olen + dlen, (f, t) => {
184
+ wr16(f, t, sport)
185
+ wr16(f, t + 2, dport)
186
+ wr32(f, t + 4, seq)
187
+ wr32(f, t + 8, ack)
188
+ f[t + 12] = ((20 + olen) / 4) << 4
189
+ f[t + 13] = flags
190
+ wr16(f, t + 14, Math.min(win, 0xffff))
191
+ if (opts) {
192
+ f[t + 20] = 2
193
+ f[t + 21] = 4
194
+ wr16(f, t + 22, opts.mss)
195
+ f[t + 24] = 1
196
+ f[t + 25] = 3
197
+ f[t + 26] = 3
198
+ f[t + 27] = opts.wscale
199
+ }
200
+ if (dlen) f.set(data, t + 20 + olen)
201
+ })
202
+ }
203
+
204
+ /** ICMP destination unreachable (type 3) quoting the original IP header + 8 bytes. */
205
+ export function icmpUnreachable(srcMac, dstMac, src, dst, code, origIp) {
206
+ const quote = origIp.subarray(0, Math.min(origIp.length, ((origIp[0] & 15) * 4) + 8))
207
+ return ipFrame(srcMac, dstMac, src, dst, IP_ICMP, 8 + quote.length, (f, t) => {
208
+ f[t] = 3
209
+ f[t + 1] = code
210
+ f.set(quote, t + 8)
211
+ })
212
+ }
213
+
214
+ /** ICMP echo reply for an echo request payload p (type 8). */
215
+ export function icmpEchoReply(srcMac, dstMac, src, dst, p) {
216
+ return ipFrame(srcMac, dstMac, src, dst, IP_ICMP, p.length, (f, t) => {
217
+ f.set(p, t)
218
+ f[t] = 0
219
+ f[t + 1] = 0
220
+ })
221
+ }
222
+
223
+ /** Sequence-number comparisons (mod 2^32). */
224
+ export const seqLt = (a, b) => ((a - b) | 0) < 0
225
+ export const seqLe = (a, b) => ((a - b) | 0) <= 0
226
+ export const seqDiff = (a, b) => (a - b) | 0 // a - b as a signed 32-bit number
227
+ export const seqAdd = (a, n) => (a + n) >>> 0
@@ -0,0 +1,230 @@
1
+ // W56: the network Worker's side of an in-page switch (switch.js). It is the stack's transport for
2
+ // connections between guests on the switch, as relay-client.js is for connections to the internet:
3
+ // the stack calls connect / data / end / abort / credit and is called back (onConnected,
4
+ // onRelayData, onRelayEnd, onRelayAbort, onRelayCredit) with the same stream ids. A connection a
5
+ // peer opens to this guest comes in as stack.peerOpen(); the stack answers with accepted().
6
+ //
7
+ // Between two Workers (one MessagePort each way, from the switch), a stream is named by the id its
8
+ // opener gave it and who opened it ("mine" is said by the sender):
9
+ // {t:'open', sid, sport, dport} open a connection to this guest's dport, from the opener's guest
10
+ // port sport; the opener may be sent WINDOW bytes
11
+ // {t:'opened', sid, code, credit} code 0: open, the opener may send `credit` bytes; else refused
12
+ // {t:'data', sid, mine, buf} bytes (an ArrayBuffer), within the credit the other side gave
13
+ // {t:'end', sid, mine} no more data (FIN)
14
+ // {t:'abort', sid, mine, code, reason} reset
15
+ // {t:'credit', sid, mine, n} the other side may send n more bytes
16
+ // The flow control is the relay's (proto.js): each side has at most the credit the other gave in
17
+ // flight, and gives credit back as its guest takes the bytes.
18
+
19
+ import { E, WINDOW } from './proto.js'
20
+
21
+ export class PeerNet {
22
+ /**
23
+ * @param {object} o
24
+ * @param {MessagePort} o.port the switch's port (switch.js attach())
25
+ * @param {(line: string) => void} [o.log]
26
+ */
27
+ constructor(o) {
28
+ this.port = o.port
29
+ this.log = o.log || (() => {})
30
+ this.stack = null
31
+ this.names = new Map() // name -> ip (u32)
32
+ this.ips = new Set()
33
+ this.links = new Map() // peer ip -> {port, ip}
34
+ this.waiting = new Map() // peer ip -> [fn(link|null)] connects waiting for the switch's port
35
+ this.streams = new Map() // stack id -> {link, sid, mine}
36
+ this.remote = new Map() // `${ip}:${sid}` of streams a peer opened -> stack id
37
+ this.stats = { opened: 0, accepted: 0, refused: 0, linked: 0, gone: 0, sent: 0, received: 0 }
38
+ this.port.onmessage = e => this.#fromSwitch(e.data)
39
+ }
40
+
41
+ attach(stack) {
42
+ this.stack = stack
43
+ }
44
+
45
+ /** A guest on the switch has this address. */
46
+ has(ip) {
47
+ return this.ips.has(ip >>> 0)
48
+ }
49
+
50
+ /** The address of a guest's name on the switch (undefined if none). */
51
+ lookup(name) {
52
+ return this.names.get(name)
53
+ }
54
+
55
+ #fromSwitch(x) {
56
+ if (x.t === 'table') {
57
+ this.names = new Map(x.members.map(m => [m.name, m.ip >>> 0]))
58
+ this.ips = new Set(x.members.map(m => m.ip >>> 0))
59
+ } else if (x.t === 'link') this.#linked(x.ip >>> 0, x.port)
60
+ else if (x.t === 'nolink') this.#ready(x.ip >>> 0, null)
61
+ else if (x.t === 'gone') this.#gone(x.ip >>> 0)
62
+ }
63
+
64
+ #linked(ip, port) {
65
+ const old = this.links.get(ip)
66
+ if (old) this.#gone(ip) // a new link replaces a stale one (the peer left and came back)
67
+ const link = { ip, port }
68
+ this.links.set(ip, link)
69
+ this.stats.linked++
70
+ port.onmessage = e => this.#fromPeer(link, e.data)
71
+ this.#ready(ip, link)
72
+ }
73
+
74
+ #ready(ip, link) {
75
+ const w = this.waiting.get(ip)
76
+ if (!w) return
77
+ this.waiting.delete(ip)
78
+ for (const f of w) f(link)
79
+ }
80
+
81
+ #gone(ip) {
82
+ const link = this.links.get(ip)
83
+ this.#ready(ip, null)
84
+ if (!link) return
85
+ this.links.delete(ip)
86
+ this.stats.gone++
87
+ link.port.onmessage = null
88
+ link.port.close()
89
+ for (const [id, s] of [...this.streams]) {
90
+ if (s.link !== link) continue
91
+ this.#forget(id)
92
+ this.stack.onRelayAbort(id, E.GONE, 'the peer left the network')
93
+ }
94
+ }
95
+
96
+ #forget(id) {
97
+ const s = this.streams.get(id)
98
+ if (!s) return
99
+ this.streams.delete(id)
100
+ if (!s.mine) this.remote.delete(`${s.link.ip}:${s.sid}`)
101
+ }
102
+
103
+ #withLink(ip, f) {
104
+ const link = this.links.get(ip)
105
+ if (link) return f(link)
106
+ let w = this.waiting.get(ip)
107
+ if (!w) {
108
+ w = []
109
+ this.waiting.set(ip, w)
110
+ this.port.postMessage({ t: 'link', ip })
111
+ }
112
+ w.push(f)
113
+ }
114
+
115
+ #send(s, m, transfer) {
116
+ s.link.port.postMessage(m, transfer)
117
+ }
118
+
119
+ // ---- the stack's transport (outbound streams and both directions' data) ----
120
+
121
+ /** Open stream id to the peer at host (a u32 or dotted string), from this guest's port sport. */
122
+ connect(id, host, port, sport) {
123
+ const ip = typeof host === 'number' ? host >>> 0 : ipU32(host)
124
+ this.#withLink(ip, link => {
125
+ if (!this.stack.hasConn(id)) return // given up meanwhile
126
+ if (!link) return this.stack.onConnected(id, E.UNREACHABLE, 0, 'no guest has that address')
127
+ const s = { link, sid: id, mine: true }
128
+ this.streams.set(id, s)
129
+ this.stats.opened++
130
+ this.#send(s, { t: 'open', sid: id, sport, dport: port })
131
+ })
132
+ }
133
+
134
+ data(id, u8) {
135
+ const s = this.streams.get(id)
136
+ if (!s) return
137
+ const buf = u8.slice().buffer
138
+ this.stats.sent += u8.length
139
+ this.#send(s, { t: 'data', sid: s.sid, mine: s.mine, buf }, [buf])
140
+ }
141
+
142
+ end(id) {
143
+ const s = this.streams.get(id)
144
+ if (s) this.#send(s, { t: 'end', sid: s.sid, mine: s.mine })
145
+ }
146
+
147
+ abort(id, code = E.RESET, reason = '') {
148
+ const s = this.streams.get(id)
149
+ if (!s) return
150
+ this.#forget(id)
151
+ this.#send(s, { t: 'abort', sid: s.sid, mine: s.mine, code, reason })
152
+ }
153
+
154
+ credit(id, n) {
155
+ const s = this.streams.get(id)
156
+ if (s) this.#send(s, { t: 'credit', sid: s.sid, mine: s.mine, n })
157
+ }
158
+
159
+ /** The stack's answer to an inbound open (stack.peerOpen): E.OK once the guest accepted, else why not. */
160
+ accepted(id, code, reason = '') {
161
+ const s = this.streams.get(id)
162
+ if (!s) return
163
+ if (code === E.OK) this.stats.accepted++
164
+ else {
165
+ this.stats.refused++
166
+ this.#forget(id)
167
+ }
168
+ this.#send(s, { t: 'opened', sid: s.sid, code, credit: code === E.OK ? WINDOW : 0, reason })
169
+ }
170
+
171
+ // ---- from a peer ----
172
+
173
+ #fromPeer(link, x) {
174
+ if (this.links.get(link.ip) !== link) return
175
+ if (x.t === 'open') {
176
+ const key = `${link.ip}:${x.sid}`
177
+ if (this.remote.has(key)) return
178
+ const id = this.stack.peerOpen(link.ip, x.sport, x.dport, this)
179
+ if (id === null) {
180
+ link.port.postMessage({ t: 'opened', sid: x.sid, code: E.REFUSED, credit: 0 })
181
+ return
182
+ }
183
+ this.remote.set(key, id)
184
+ this.streams.set(id, { link, sid: x.sid, mine: false })
185
+ return
186
+ }
187
+ // the sender's "mine" is from its side: its stream is one a peer opened here
188
+ const id = x.t === 'opened' || !x.mine ? x.sid : this.remote.get(`${link.ip}:${x.sid}`)
189
+ const s = id === undefined ? null : this.streams.get(id)
190
+ if (!s || s.link !== link) return
191
+ switch (x.t) {
192
+ case 'opened':
193
+ if (x.code !== E.OK) this.#forget(id)
194
+ this.stack.onConnected(id, x.code, x.credit, x.reason || '')
195
+ break
196
+ case 'data':
197
+ this.stats.received += x.buf.byteLength
198
+ this.stack.onRelayData(id, new Uint8Array(x.buf))
199
+ break
200
+ case 'end':
201
+ this.stack.onRelayEnd(id)
202
+ break
203
+ case 'abort':
204
+ this.#forget(id)
205
+ this.stack.onRelayAbort(id, x.code ?? E.RESET, x.reason || '')
206
+ break
207
+ case 'credit':
208
+ this.stack.onRelayCredit(id, x.n)
209
+ break
210
+ }
211
+ }
212
+
213
+ /** The stack forgot a stream (it ended cleanly): drop its record. */
214
+ closed(id) {
215
+ this.#forget(id)
216
+ }
217
+
218
+ snapshot() {
219
+ return { ...this.stats, links: this.links.size, streams: this.streams.size, names: Object.fromEntries([...this.names].map(([n, ip]) => [n, ipStr(ip)])) }
220
+ }
221
+ }
222
+
223
+ function ipU32(s) {
224
+ const p = String(s).split('.').map(Number)
225
+ return (((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) >>> 0)
226
+ }
227
+
228
+ function ipStr(v) {
229
+ return `${v >>> 24}.${(v >>> 16) & 255}.${(v >>> 8) & 255}.${v & 255}`
230
+ }