@threadedstack/wmd-web 0.1.18 → 0.1.20
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 +24 -3
- package/RELEASE.json +28 -44
- package/guest/arm64/Image +1 -1
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +32 -18
- package/relay/main.ts +1 -1
- package/relay/relay.ts +0 -1
- package/relay/token.ts +5 -34
- package/relay/wisp.ts +5 -37
- package/src/blob.js +12 -0
- package/src/boot.js +9 -1
- package/src/build/builder.js +197 -12
- package/src/build/context.js +3 -2
- package/src/host/disk-worker.js +8 -1
- package/src/host/disk.js +1 -1
- package/src/host/jit.js +36 -0
- package/src/image-files.js +2 -1
- package/src/net/stack.js +153 -1
- package/src/registry/load-tar.js +2 -1
- package/wmdpack.wasm +0 -0
package/src/net/stack.js
CHANGED
|
@@ -57,6 +57,22 @@ const HALF_OPEN_IDLE_MS = 10 * 60_000
|
|
|
57
57
|
const SYNACK_GIVE_UP_MS = 60_000 // Linux: tcp_synack_retries (5), about 63 s
|
|
58
58
|
const GUEST_CLOSED_IDLE_MS = 120_000 // the guest's FIN is in, the remote says nothing: its kernel forgot it (FIN_WAIT2, 60 s)
|
|
59
59
|
const MAX_PENDING = 32 // handshakes not finished at once (more get a RST), so abandoned ones cannot take every relay slot
|
|
60
|
+
// D-551: npm's CDN ends (FIN) a connection whose TLS handshake is not done 15 s after the connect, and the relay
|
|
61
|
+
// connects when the guest's SYN comes. A slow guest (WebKit) can miss that before its ClientHello. A remote that
|
|
62
|
+
// ends a connection nothing has crossed, REDIAL_MIN_MS or more after it opened, is dialled once more instead.
|
|
63
|
+
const REDIAL_MIN_MS = 5_000
|
|
64
|
+
// D-552: a guest that runs many TLS handshakes at once (pnpm opens 16) splits its CPU among them, and in a slow guest
|
|
65
|
+
// (WebKit) none ends inside npm's 15 s. At most TLS_HANDSHAKES run at once: a ClientHello waits here for its turn, then
|
|
66
|
+
// goes out on a stream dialled again when the one it has is TLS_FRESH_MS old. A remote that ends a stream before it
|
|
67
|
+
// answers the ClientHello gets the hello once more on a new stream. A handshake holds its turn at most TLS_TURN_MS.
|
|
68
|
+
const TLS_HANDSHAKES = 4
|
|
69
|
+
const TLS_FRESH_MS = 3_000
|
|
70
|
+
const TLS_TURN_MS = 20_000
|
|
71
|
+
const TLS_HELLO_MAX = 16 << 10 // bytes kept for a replay
|
|
72
|
+
const T_NONE = 0 // no guest bytes yet
|
|
73
|
+
const T_WAIT = 1 // a ClientHello waits for a turn
|
|
74
|
+
const T_TURN = 2 // the handshake has a turn
|
|
75
|
+
const T_DONE = 3 // past the handshake, or not TLS
|
|
60
76
|
const INTERNAL_ZONE = 'wmd.internal'
|
|
61
77
|
|
|
62
78
|
const P_CONNECTING = 1 // waiting for the relay's CONNECTED
|
|
@@ -122,6 +138,14 @@ class Conn {
|
|
|
122
138
|
this.lastRemote = 0 // the last data or end from the relay
|
|
123
139
|
this.bytesIn = 0 // guest -> relay
|
|
124
140
|
this.bytesOut = 0 // relay -> guest
|
|
141
|
+
this.redialAt = 0 // D-551: when the stream was dialled again (0: not); its answer is pending while redialing
|
|
142
|
+
this.redialing = false
|
|
143
|
+
this.dialAt = 0 // when the relay stream the guest's bytes go to connected
|
|
144
|
+
this.tls = T_NONE // D-552
|
|
145
|
+
this.tlsAt = 0 // when the handshake got its turn
|
|
146
|
+
this.hello = null // the guest's bytes sent before the remote answered (a replay sends them again)
|
|
147
|
+
this.helloBytes = 0
|
|
148
|
+
this.helloReplayed = false
|
|
125
149
|
}
|
|
126
150
|
inflight() {
|
|
127
151
|
return seqDiff(this.sndNxt, this.sndUna)
|
|
@@ -154,6 +178,9 @@ export class NetStack {
|
|
|
154
178
|
this.peers = o.peers || null // W56: the switch's guests (peer.js)
|
|
155
179
|
this.now = o.now || (() => performance.now())
|
|
156
180
|
this.maxConns = o.maxConns ?? 256
|
|
181
|
+
this.tlsMax = o.tlsHandshakes ?? TLS_HANDSHAKES
|
|
182
|
+
this.tlsTurns = new Set() // D-552: handshakes with a turn
|
|
183
|
+
this.tlsWait = [] // and those waiting for one, oldest first
|
|
157
184
|
this.log = o.log || (() => {})
|
|
158
185
|
this.conns = new Map() // key -> Conn
|
|
159
186
|
this.byId = new Map() // relay stream id -> Conn
|
|
@@ -473,6 +500,7 @@ export class NetStack {
|
|
|
473
500
|
/** The relay's answer to a connect. */
|
|
474
501
|
onConnected(id, code, credit, detail) {
|
|
475
502
|
const c = this.byId.get(id)
|
|
503
|
+
if (c?.redialing) return this.#redialed(c, code, credit, detail)
|
|
476
504
|
if (!c || c.phase !== P_CONNECTING) {
|
|
477
505
|
if (!c && code === E.OK) this.relay.abort(id) // a connect that came back after we gave up on it
|
|
478
506
|
return
|
|
@@ -494,7 +522,7 @@ export class NetStack {
|
|
|
494
522
|
if (this.stats.connectMs.length > 100) this.stats.connectMs.shift()
|
|
495
523
|
c.relayCredit = credit
|
|
496
524
|
c.phase = P_SYNACK
|
|
497
|
-
c.synAckAt = this.now()
|
|
525
|
+
c.synAckAt = c.dialAt = this.now()
|
|
498
526
|
c.sndNxt = seqAdd(c.iss, 1)
|
|
499
527
|
this.#synAck(c)
|
|
500
528
|
}
|
|
@@ -615,6 +643,8 @@ export class NetStack {
|
|
|
615
643
|
|
|
616
644
|
/** Send held guest bytes to the relay as credit allows, in messages of up to MAX_DATA (one per segment cost a WebSocket message each). */
|
|
617
645
|
#flushToRelay(c) {
|
|
646
|
+
if (c.redialing) return // D-551: the new stream has no answer yet; #redialed flushes
|
|
647
|
+
if (c.tls !== T_DONE && c.via === this.relay && !this.#tlsGate(c)) return
|
|
618
648
|
while (c.toRelay.length && c.relayCredit > 0) {
|
|
619
649
|
const n = Math.min(c.toRelayBytes, c.relayCredit, MAX_DATA)
|
|
620
650
|
let part
|
|
@@ -637,6 +667,13 @@ export class NetStack {
|
|
|
637
667
|
}
|
|
638
668
|
c.toRelayBytes -= n
|
|
639
669
|
c.relayCredit -= n
|
|
670
|
+
if (c.tls === T_TURN && !c.bytesOut && c.hello) {
|
|
671
|
+
// past the cap the hello is not kept at all (a replay with a gap would be garbage)
|
|
672
|
+
if (c.helloBytes + n <= TLS_HELLO_MAX) {
|
|
673
|
+
c.hello.push(part.slice())
|
|
674
|
+
c.helloBytes += n
|
|
675
|
+
} else c.hello = null
|
|
676
|
+
}
|
|
640
677
|
c.via.data(c.id, part)
|
|
641
678
|
}
|
|
642
679
|
if (c.guestFin && !c.toRelay.length && !c.relayEndSent) {
|
|
@@ -676,16 +713,119 @@ export class NetStack {
|
|
|
676
713
|
onRelayEnd(id) {
|
|
677
714
|
const c = this.byId.get(id)
|
|
678
715
|
if (!c) return
|
|
716
|
+
if (this.#redial(c) || this.#tlsReplay(c)) return
|
|
679
717
|
c.remoteEnd = true
|
|
680
718
|
c.lastRemote = this.now()
|
|
681
719
|
this.wantsSend.add(c)
|
|
682
720
|
this.#trySend(c)
|
|
683
721
|
}
|
|
684
722
|
|
|
723
|
+
/** D-551: the remote ended a stream nothing has crossed, a while after it opened: dial it again, once, and say nothing to the guest. False when it does not qualify. */
|
|
724
|
+
#redial(c) {
|
|
725
|
+
if (c.redialAt || c.via !== this.relay || c.phase !== P_OPEN || c.bytesOut || c.bytesIn - c.toRelayBytes || c.guestFin) return false
|
|
726
|
+
if (this.now() - c.dialAt < REDIAL_MIN_MS) return false
|
|
727
|
+
this.stats.redials = (this.stats.redials || 0) + 1
|
|
728
|
+
this.#dialAgain(c, 'ended unused')
|
|
729
|
+
return true
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** A new relay stream for c, the old one aborted; the guest hears nothing. Its bytes wait in toRelay until #redialed. */
|
|
733
|
+
#dialAgain(c, why) {
|
|
734
|
+
this.relay.abort(c.id)
|
|
735
|
+
this.byId.delete(c.id)
|
|
736
|
+
c.id = this.nextId++
|
|
737
|
+
this.byId.set(c.id, c)
|
|
738
|
+
c.relayCredit = 0
|
|
739
|
+
c.redialing = true
|
|
740
|
+
c.redialAt = this.now()
|
|
741
|
+
this.#event({ ev: 'redial', why, to: `${u32ToIp(c.rip)}:${c.rport}`, afterMs: Math.round(c.redialAt - c.dialAt) })
|
|
742
|
+
this.relay.connect(c.id, u32ToIp(c.rip), c.rport)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** D-552: may the guest's bytes go to the relay now? Sorts a stream's first bytes into TLS or not, and holds a ClientHello until it has a turn. */
|
|
746
|
+
#tlsGate(c) {
|
|
747
|
+
if (c.tls === T_TURN) {
|
|
748
|
+
// the guest's bytes after the server's first flight end its part of the handshake
|
|
749
|
+
if (c.bytesOut && c.toRelayBytes) this.#tlsDone(c)
|
|
750
|
+
return true
|
|
751
|
+
}
|
|
752
|
+
if (c.tls === T_WAIT) return false
|
|
753
|
+
if (!c.toRelayBytes) return true
|
|
754
|
+
const h = c.toRelay[0]
|
|
755
|
+
if (c.bytesOut || h[0] !== 0x16 || (h.length > 1 && h[1] !== 0x03)) {
|
|
756
|
+
c.tls = T_DONE // the remote spoke first, or not a TLS handshake record
|
|
757
|
+
return true
|
|
758
|
+
}
|
|
759
|
+
if (this.tlsTurns.size >= this.tlsMax) {
|
|
760
|
+
c.tls = T_WAIT
|
|
761
|
+
this.tlsWait.push(c)
|
|
762
|
+
this.stats.tlsWaits = (this.stats.tlsWaits || 0) + 1
|
|
763
|
+
return false
|
|
764
|
+
}
|
|
765
|
+
return this.#tlsStart(c)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** The handshake gets a turn. False when its stream is dialled again first (#redialed sends the hello). */
|
|
769
|
+
#tlsStart(c) {
|
|
770
|
+
c.tls = T_TURN
|
|
771
|
+
c.tlsAt = this.now()
|
|
772
|
+
c.hello = []
|
|
773
|
+
c.helloBytes = 0
|
|
774
|
+
this.tlsTurns.add(c)
|
|
775
|
+
if (!c.redialing && c.phase === P_OPEN && c.bytesIn === c.toRelayBytes && this.now() - c.dialAt >= TLS_FRESH_MS) {
|
|
776
|
+
this.stats.tlsFresh = (this.stats.tlsFresh || 0) + 1
|
|
777
|
+
this.#dialAgain(c, 'fresh for a ClientHello')
|
|
778
|
+
return false
|
|
779
|
+
}
|
|
780
|
+
return true
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#tlsDone(c) {
|
|
784
|
+
c.tls = T_DONE
|
|
785
|
+
c.hello = null
|
|
786
|
+
if (this.tlsTurns.delete(c)) this.#tlsNext()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Turns free: the oldest waiting hellos go. */
|
|
790
|
+
#tlsNext() {
|
|
791
|
+
while (this.tlsTurns.size < this.tlsMax && this.tlsWait.length) {
|
|
792
|
+
const c = this.tlsWait.shift()
|
|
793
|
+
if (c.tls !== T_WAIT || this.conns.get(c.key) !== c) continue // closed, or its 4-tuple reused by a new connection
|
|
794
|
+
if (this.#tlsStart(c)) this.#flushToRelay(c)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** D-552: the remote ended or reset a stream before it answered the guest's ClientHello: send the hello once more on a new stream. */
|
|
799
|
+
#tlsReplay(c) {
|
|
800
|
+
if (c.tls !== T_TURN || c.bytesOut || c.helloReplayed || !c.hello || !c.helloBytes || c.guestFin || c.via !== this.relay) return false
|
|
801
|
+
c.helloReplayed = true
|
|
802
|
+
c.toRelay.unshift(...c.hello)
|
|
803
|
+
c.toRelayBytes += c.helloBytes
|
|
804
|
+
c.hello = []
|
|
805
|
+
c.helloBytes = 0
|
|
806
|
+
this.stats.tlsReplays = (this.stats.tlsReplays || 0) + 1
|
|
807
|
+
this.#dialAgain(c, 'replay the ClientHello')
|
|
808
|
+
return true
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
#redialed(c, code, credit, detail) {
|
|
812
|
+
c.redialing = false
|
|
813
|
+
if (code !== E.OK) return this.#close(c, `redial: ${E_NAMES[code] || code}${detail ? ` (${detail})` : ''}`, false)
|
|
814
|
+
c.dialAt = this.now()
|
|
815
|
+
c.relayCredit = credit
|
|
816
|
+
this.#flushToRelay(c)
|
|
817
|
+
if (this.#rcvWnd(c) > c.lastWndSent) {
|
|
818
|
+
c.needAck = true
|
|
819
|
+
this.acks.add(c)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
685
823
|
onRelayAbort(id, code, reason) {
|
|
686
824
|
const c = this.byId.get(id)
|
|
687
825
|
if (!c) return
|
|
826
|
+
if (c.redialing) return this.#redialed(c, code || E.GONE, 0, reason)
|
|
688
827
|
if (c.phase === P_CONNECTING) return this.onConnected(id, code || E.GONE, 0, reason)
|
|
828
|
+
if (this.#tlsReplay(c)) return
|
|
689
829
|
this.#event({ ev: 'abort', to: `${u32ToIp(c.rip)}:${c.rport}`, why: E_NAMES[code] || code, reason })
|
|
690
830
|
this.#close(c, `relay: ${E_NAMES[code] || code} ${reason || ''}`.trim(), false)
|
|
691
831
|
}
|
|
@@ -765,6 +905,12 @@ export class NetStack {
|
|
|
765
905
|
this.byId.delete(c.id)
|
|
766
906
|
this.wantsSend.delete(c)
|
|
767
907
|
this.acks.delete(c)
|
|
908
|
+
if (c.tls === T_TURN) this.#tlsDone(c)
|
|
909
|
+
else if (c.tls === T_WAIT) {
|
|
910
|
+
c.tls = T_DONE
|
|
911
|
+
const i = this.tlsWait.indexOf(c)
|
|
912
|
+
if (i >= 0) this.tlsWait.splice(i, 1)
|
|
913
|
+
}
|
|
768
914
|
}
|
|
769
915
|
|
|
770
916
|
/** After a batch of input: one ACK per connection that needs one. */
|
|
@@ -782,6 +928,12 @@ export class NetStack {
|
|
|
782
928
|
tick() {
|
|
783
929
|
const t = this.now()
|
|
784
930
|
for (const c of [...this.conns.values()]) {
|
|
931
|
+
if (c.redialing && t - c.redialAt > CONNECT_TIMEOUT_MS) {
|
|
932
|
+
this.relay.abort(c.id)
|
|
933
|
+
this.#redialed(c, E.TIMEOUT, 0, 'no answer from the relay')
|
|
934
|
+
continue
|
|
935
|
+
}
|
|
936
|
+
if (c.tls === T_TURN && t - c.tlsAt > TLS_TURN_MS) this.#tlsDone(c) // a slow handshake goes on without its turn
|
|
785
937
|
if (c.phase === P_CONNECTING) {
|
|
786
938
|
if (t - c.openedAt > CONNECT_TIMEOUT_MS) {
|
|
787
939
|
c.via.abort(c.id)
|
package/src/registry/load-tar.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// copy is the layer handed to the converter Worker); a Blob is read one entry at a time, so the whole tar is
|
|
12
12
|
// never in memory at once.
|
|
13
13
|
|
|
14
|
+
import { blobBytes } from '../blob.js'
|
|
14
15
|
import { createConverter } from './converter.js'
|
|
15
16
|
import { RegistryError, sha256 } from './hub.js'
|
|
16
17
|
import { CONVERTER } from './pull.js'
|
|
@@ -126,7 +127,7 @@ function source(input) {
|
|
|
126
127
|
if (input instanceof Uint8Array) return { size: input.length, read: async (off, len) => input.subarray(off, off + len) }
|
|
127
128
|
if (input instanceof ArrayBuffer) return source(new Uint8Array(input))
|
|
128
129
|
if (typeof Blob !== 'undefined' && input instanceof Blob) {
|
|
129
|
-
return { size: input.size, read:
|
|
130
|
+
return { size: input.size, read: (off, len) => blobBytes(input.slice(off, off + len)) }
|
|
130
131
|
}
|
|
131
132
|
throw new TypeError('loadImageTar: the tar as a Uint8Array, ArrayBuffer or Blob')
|
|
132
133
|
}
|
package/wmdpack.wasm
CHANGED
|
Binary file
|