@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
|
@@ -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
|
package/src/net/peer.js
ADDED
|
@@ -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
|
+
}
|