socketduct 0.1.16 → 0.1.18
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/.env.example +3 -0
- package/changelog.d/20260924120000-wan-route-classes.md +1 -0
- package/changelog.d/20260925120000-reverse-stream-transport-resume.md +1 -0
- package/docs/architecture.md +28 -8
- package/docs/protocol.md +59 -15
- package/docs/reverse-gateway.md +121 -55
- package/docs/security.md +28 -5
- package/docs/session-lifecycle.md +21 -10
- package/docs/socket-api.md +59 -23
- package/package.json +1 -1
- package/src/client/deferred-virtual-socket.js +16 -4
- package/src/client/multiplexed-transport-set.js +106 -65
- package/src/config/environment.js +24 -1
- package/src/reverse/client.js +283 -50
- package/src/reverse/common.js +1 -0
- package/src/reverse/gateway.js +265 -38
- package/src/reverse/hub.js +464 -52
- package/src/reverse/runtime.js +32 -6
- package/test/e2e/reverse-gateway-process.test.js +93 -0
- package/test/integration/reverse-gateway.test.js +576 -28
- package/test/integration/reverse-route-class.test.js +377 -0
- package/test/unit/client/multiplexed-transport-set.test.js +42 -0
- package/test/unit/config/environment.test.js +7 -1
package/.env.example
CHANGED
|
@@ -115,6 +115,9 @@ SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375
|
|
|
115
115
|
# SOCKETDUCT_REVERSE_GATEWAY_BINDINGS=o2:home-o2,zyxel:home-zyxel,starlink:home-starlink
|
|
116
116
|
# Authorize exact client username, gateway ID, and home-local target tuples.
|
|
117
117
|
# SOCKETDUCT_REVERSE_ROUTES=tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375,tensorbuzz:starlink:docker-server:2375
|
|
118
|
+
# Or bind one client physical connection to a route class and resolve each
|
|
119
|
+
# requested target to one exact registered gateway. Ambiguous rows fail closed.
|
|
120
|
+
# SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES=tensorbuzz:starlink:server-a-starlink:docker-a:2375,tensorbuzz:starlink:server-b-starlink:docker-b:2375
|
|
118
121
|
|
|
119
122
|
# Home member: change SOCKETDUCT_MODE to reverse-gateway. Run one process in
|
|
120
123
|
# each router-pinned namespace and use distinct stable identity/credential data.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct reverse clients can now share one authenticated physical connection per route class while the hub securely resolves each target stream and isolates backpressured server-local gateways.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Native reverse-gateway streams now keep their app and target sockets alive across transient hub transport loss, preserve unread flow-control and terminal-close state, and replay durably spooled bytes exactly once after paired resume.
|
package/docs/architecture.md
CHANGED
|
@@ -78,6 +78,13 @@ failure retains the spool reservation and fails that member rather than
|
|
|
78
78
|
advertising capacity that still exists on disk. These process bounds supplement,
|
|
79
79
|
rather than replace, member and stream limits.
|
|
80
80
|
|
|
81
|
+
For native reverse mode, the set can instead receive `routeClasses`. It creates
|
|
82
|
+
one `ReverseGatewayConnection` and one stable derived connection identity per
|
|
83
|
+
`{process connectionId, routeClass}`. The hub chooses an exact server-local
|
|
84
|
+
gateway independently for each target `OPEN`, so adding target gateways does not
|
|
85
|
+
multiply client physical connections. Allocation metadata is `routeClass`;
|
|
86
|
+
legacy exact-gateway sets keep `gatewayId` and their existing identity derivation.
|
|
87
|
+
|
|
81
88
|
Replacement recovery is registered before set startup and addresses the
|
|
82
89
|
original configured gateway ID directly. The set derives the same member
|
|
83
90
|
identity/spool namespace and delegates recovery to that member before any
|
|
@@ -93,14 +100,27 @@ rollback. A relay listener selects v2 or v3 explicitly.
|
|
|
93
100
|
The reverse hub is a rendezvous and stream-ID translation boundary on Server3.
|
|
94
101
|
Authenticated home gateway processes connect outward, register stable IDs, and
|
|
95
102
|
accept hub-created even stream IDs. Authenticated local application members
|
|
96
|
-
select one ID and create odd-ID streams. The
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
select either one exact ID or a route class and create odd-ID streams. The class
|
|
104
|
+
stays bound to the client physical connection while the exact gateway is
|
|
105
|
+
resolved from the requested target for each `OPEN`. The hub maps frames without
|
|
106
|
+
opening the target or nesting another Socketduct transport. Each home gateway
|
|
107
|
+
applies the final local allowlist before connecting to its private Docker shim.
|
|
108
|
+
|
|
109
|
+
Gateway and client registrations reconnect with generation fencing. The hub
|
|
110
|
+
retains each established bridge for a bounded window while the live gateway
|
|
111
|
+
keeps its original target socket and both endpoints durably retain
|
|
112
|
+
unacknowledged send chunks. Replacement physical generations exchange paired
|
|
113
|
+
resume manifests; the hub reconciles receiver-committed sequence positions
|
|
114
|
+
before either side replays. This resumes the same byte stream without reopening
|
|
115
|
+
the target or issuing a second application request. Reverse process replacement
|
|
116
|
+
remains terminal because the original target/app socket objects cannot move to
|
|
117
|
+
a new process.
|
|
118
|
+
|
|
119
|
+
The hub serializes output independently for each physical destination with a
|
|
120
|
+
bounded queue, so one backpressured target gateway cannot stop client frames
|
|
121
|
+
bound for a healthy sibling gateway. Exceeding that destination bound closes
|
|
122
|
+
only the unhealthy physical owner and fails its mapped streams with the causal
|
|
123
|
+
limit error.
|
|
104
124
|
See [Native reverse gateway](reverse-gateway.md).
|
|
105
125
|
|
|
106
126
|
### Edge
|
package/docs/protocol.md
CHANGED
|
@@ -43,13 +43,17 @@ emits it and ordinary handlers reject it as unnegotiated.
|
|
|
43
43
|
before `AUTH_OK`.
|
|
44
44
|
- `PING` / `PONG`: connection heartbeat and readiness.
|
|
45
45
|
- `RESUME` / `RESUMED`: bounded pages of stream manifests. Each entry contains
|
|
46
|
-
stream ID, stable identifier and both directional acknowledgement
|
|
46
|
+
stream ID, stable identifier and both directional sequence/acknowledgement
|
|
47
|
+
positions. Ordinary v3 uses one client manifest; native reverse resume pairs
|
|
48
|
+
independently authenticated client and gateway manifests at the hub.
|
|
47
49
|
- `GOAWAY`: terminal physical-connection shutdown. Authentication,
|
|
48
50
|
authorization and TLS failures are terminal and are not retried forever.
|
|
49
51
|
- `GATEWAY_REGISTER` / `GATEWAY_REGISTERED`: reverse-only authenticated stable
|
|
50
|
-
gateway registration and generation fencing.
|
|
52
|
+
gateway registration and generation fencing. The response declares whether
|
|
53
|
+
retained logical streams require a resume manifest.
|
|
51
54
|
- `GATEWAY_SELECT` / `GATEWAY_SELECTED`: reverse-only binding of a client
|
|
52
|
-
physical member to one
|
|
55
|
+
physical member to one exact gateway or route class before readiness. The
|
|
56
|
+
response likewise declares retained resume state.
|
|
53
57
|
|
|
54
58
|
### Stream control (nonzero stream ID)
|
|
55
59
|
|
|
@@ -72,11 +76,13 @@ bounded per stream and in aggregate per connection. Control frames have
|
|
|
72
76
|
priority; DATA frames use round-robin scheduling with bounded frame sizes, so a
|
|
73
77
|
large upload cannot monopolize the physical connection.
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
After both peers reconcile
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
Scheduler position belongs to one physical generation, while receive-side
|
|
80
|
+
flow-control debt belongs to the logical stream. After both peers reconcile in
|
|
81
|
+
RESUME, a sender retains its negotiated window minus bytes the receiver already
|
|
82
|
+
accepted but has not application-acknowledged. Those bytes are not replayed,
|
|
83
|
+
but remain spooled until the durable ACK advances; replay starts strictly above
|
|
84
|
+
the receiver's high-water sequence. A failed transport therefore cannot turn
|
|
85
|
+
unread `VirtualSocket` bytes back into fresh credit.
|
|
80
86
|
|
|
81
87
|
Relay retention is bounded independently per stream, per connection identity,
|
|
82
88
|
and process-wide. The process-wide reservation is owned by the relay runtime
|
|
@@ -103,17 +109,55 @@ relay's per-stream result rather than being sent again.
|
|
|
103
109
|
### Native reverse-gateway extension
|
|
104
110
|
|
|
105
111
|
Reverse mode reuses the v3 physical transport and stream frames directly. A
|
|
106
|
-
home gateway registers outbound
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
home gateway registers outbound. A Server3 client selects exactly one legacy
|
|
113
|
+
`gatewayId` or one first-class `routeClass` in `GATEWAY_SELECT`; the response
|
|
114
|
+
echoes the same selector. Client streams are odd and the hub maps them to even
|
|
115
|
+
gateway-side IDs. For a class-bound connection, every `OPEN` repeats the bound
|
|
116
|
+
class and the hub resolves `{authenticated principal, routeClass, target}` to
|
|
117
|
+
exactly one current gateway. The client never supplies that gateway ID. The hub
|
|
118
|
+
then authorizes the exact principal/gateway/target tuple and the gateway
|
|
119
|
+
separately authorizes its local target. Zero, multiple, stale, disconnected, or
|
|
120
|
+
mismatched resolutions reset the stream without weakening sibling routes.
|
|
121
|
+
Registration reconnect uses a higher generation, but any established stream on
|
|
122
|
+
the lost gateway remains bound to its original client, gateway process, and
|
|
123
|
+
target socket during the bounded resume window. The hub also replaces the
|
|
124
|
+
corresponding client physical transport so neither endpoint can advance without
|
|
125
|
+
paired reconciliation.
|
|
126
|
+
|
|
127
|
+
On replacement, both reverse peers send paginated `RESUME` entries containing
|
|
128
|
+
the stable identifier, each direction's allocated sequence and durable ACK,
|
|
129
|
+
and directional END state. The hub verifies authenticated owner, stable stream
|
|
130
|
+
ID/identifier, monotonic bounds, and cross-peer positions. It preserves both
|
|
131
|
+
the receiver high-water sequence and its application-acknowledged boundary in
|
|
132
|
+
each per-stream `RESUMED` result, and only then admits replay. Senders replay
|
|
133
|
+
durable raw chunks strictly above the high-water sequence while retaining
|
|
134
|
+
unacknowledged chunks and charging their outstanding bytes to the new window.
|
|
135
|
+
A DATA frame that entered a failed physical socket but did not reach the
|
|
136
|
+
logical receiver is therefore replayed; a byte already buffered by the receiver
|
|
137
|
+
is not delivered twice. The target socket is never reopened and the application
|
|
138
|
+
request is never recreated as a new stream.
|
|
139
|
+
|
|
140
|
+
Reverse send replay is persisted before transmission on both the Server3 client
|
|
141
|
+
and outbound gateway. ACKs truncate those private temporary spools. Missing
|
|
142
|
+
targets, expired retention, identity or manifest mismatch, and configured
|
|
143
|
+
buffer/spool/output bounds produce one terminal stream error with the causal
|
|
144
|
+
code. Process-replacement recovery remains unsupported for reverse streams
|
|
145
|
+
because a replacement gateway process cannot inherit the original target TCP
|
|
146
|
+
socket.
|
|
147
|
+
|
|
148
|
+
Hub output has one ordered owner per physical destination. Frames for a
|
|
149
|
+
backpressured gateway remain in that gateway's bounded queue while frames for
|
|
150
|
+
other gateways continue independently; a destination that exceeds its output
|
|
151
|
+
bound is disconnected and its streams are reset without blocking healthy
|
|
152
|
+
destinations. Ordering within each destination and stream is preserved.
|
|
111
153
|
|
|
112
154
|
Stream IDs must increase monotonically on each physical connection. END and
|
|
113
155
|
CLOSE positions must match the sender's accepted raw-byte sequence; a
|
|
114
156
|
gateway-originated CLOSE is retained until the client acknowledges it with
|
|
115
|
-
CLOSE
|
|
116
|
-
|
|
157
|
+
CLOSE, including across physical resume. A client that echoes CLOSE immediately
|
|
158
|
+
after resume uses the receiver-confirmed client-to-gateway position rather than
|
|
159
|
+
a stale pre-resume transmit position. RESET is immediately terminal. Repeats,
|
|
160
|
+
data after END, and stale ACKs are protocol violations.
|
|
117
161
|
|
|
118
162
|
Reverse peers must negotiate `zstd` before selection/registration completes.
|
|
119
163
|
A compressed DATA payload is `uint32_be(uncompressedLength)` followed by one
|
package/docs/reverse-gateway.md
CHANGED
|
@@ -10,26 +10,29 @@ The roles are deliberately separate:
|
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
TensorBuzz / local application on Server3
|
|
13
|
-
-> ReverseGatewayConnection (verified TLS client, one selected
|
|
13
|
+
-> ReverseGatewayConnection (verified TLS client, one selected route class)
|
|
14
14
|
-> reverse hub on Server3
|
|
15
|
-
<- OutboundReverseGateway
|
|
16
|
-
->
|
|
15
|
+
<- OutboundReverseGateway A -> target A on its private Compose network
|
|
16
|
+
<- OutboundReverseGateway B -> target B on its private Compose network
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
- `startReverseGatewayHub(options)` is the reachable Server3 broker. It
|
|
20
20
|
authenticates both kinds of physical connection, binds gateway IDs to
|
|
21
|
-
principals,
|
|
22
|
-
|
|
21
|
+
principals, authorizes exact-gateway or route-class selection, resolves each
|
|
22
|
+
route-class `OPEN` to exactly one current gateway, authorizes the resulting
|
|
23
|
+
client/gateway/target tuple, and never connects to a target itself.
|
|
23
24
|
- `OutboundReverseGateway` is one home member. It makes one persistent outbound
|
|
24
25
|
TLS connection, registers one stable `gatewayId`, applies its own exact local
|
|
25
26
|
target policy, and multiplexes all accepted target streams on that transport.
|
|
26
|
-
- `ReverseGatewayConnection` is the local application API.
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
A three-member set
|
|
32
|
-
`o2`, `zyxel`, and
|
|
27
|
+
- `ReverseGatewayConnection` is the local application API. Supplying
|
|
28
|
+
`routeClass` binds one authenticated physical connection to that class; each
|
|
29
|
+
`createConnection()` is routed from its requested target. Supplying the
|
|
30
|
+
legacy `gatewayId` instead retains exact-gateway behavior unchanged.
|
|
31
|
+
- `MultiplexedSocketductTransportSet` accepts a `routeClasses` array of
|
|
32
|
+
`endpoint.reverseHub` members. A three-member WAN set therefore owns exactly
|
|
33
|
+
one client physical connection for each class such as `o2`, `zyxel`, and
|
|
34
|
+
`starlink`, independent of how many server-local gateways those classes can
|
|
35
|
+
reach.
|
|
33
36
|
|
|
34
37
|
Import these APIs from `socketduct/reverse`. The ordinary v2 and v3 edge/relay
|
|
35
38
|
APIs and wire handshakes remain unchanged.
|
|
@@ -42,11 +45,17 @@ frames authenticate registration and selection:
|
|
|
42
45
|
|
|
43
46
|
- `GATEWAY_REGISTER` / `GATEWAY_REGISTERED` bind the authenticated outbound
|
|
44
47
|
transport to a configured gateway ID and monotonically increasing generation.
|
|
45
|
-
- `GATEWAY_SELECT` / `GATEWAY_SELECTED`
|
|
46
|
-
|
|
48
|
+
- `GATEWAY_SELECT` / `GATEWAY_SELECTED` carry exactly one `gatewayId` or
|
|
49
|
+
`routeClass`. Exact selection still makes readiness depend on that gateway;
|
|
50
|
+
route-class readiness authenticates the class independently of individual
|
|
51
|
+
gateway availability.
|
|
47
52
|
- Client-created streams use odd IDs. The hub maps each one to a hub-created
|
|
48
|
-
even ID on the
|
|
49
|
-
`
|
|
53
|
+
even ID on the exact gateway transport selected for that `OPEN`. A class-bound
|
|
54
|
+
`OPEN` repeats its authenticated `routeClass` and never supplies a gateway ID;
|
|
55
|
+
the hub resolves `{principal, routeClass, target}` to exactly one registered
|
|
56
|
+
gateway before applying the existing exact route authorization. `OPEN`,
|
|
57
|
+
`OPENED`, `DATA`, `ACK`, `WINDOW`, `END`, `CLOSE`, and `RESET` then retain their
|
|
58
|
+
v3 meanings.
|
|
50
59
|
Gateway-originated `CLOSE` is acknowledged by a client `CLOSE`, after which the
|
|
51
60
|
hub releases both mapped IDs; `RESET` remains immediately terminal.
|
|
52
61
|
Valid `ACK` and `WINDOW` frames already in flight when that close starts are
|
|
@@ -56,9 +65,13 @@ The hub rejects unknown IDs, registration by the wrong principal, a second
|
|
|
56
65
|
connection identity claiming the same ID, stale generations, repeated IDs,
|
|
57
66
|
malformed frames, unauthorized routes, and configured limits. A higher
|
|
58
67
|
generation from the same stable connection identity fences its prior transport.
|
|
59
|
-
At the configured identity bound, the oldest disconnected client
|
|
60
|
-
evicted; active owners are never evicted and
|
|
61
|
-
|
|
68
|
+
At the configured identity bound, the oldest disconnected, streamless client
|
|
69
|
+
identity is evicted; retained-stream and active owners are never evicted and
|
|
70
|
+
retain generation fencing.
|
|
71
|
+
Zero eligible gateways denies the target, and multiple eligible gateways deny
|
|
72
|
+
the stream as ambiguous. Unknown classes, class mismatches, stale registrations,
|
|
73
|
+
and disconnected resolved gateways also fail closed. The home gateway
|
|
74
|
+
independently rejects any target outside its local allowlist.
|
|
62
75
|
|
|
63
76
|
Reverse DATA compression is negotiated as `zstd`. The one DATA-only flag means
|
|
64
77
|
the payload starts with a four-byte uncompressed length followed by one
|
|
@@ -70,9 +83,30 @@ zstd runtime support and always uses TLS with certificate verification enabled;
|
|
|
70
83
|
there is no plaintext or `rejectUnauthorized:false` reverse option.
|
|
71
84
|
|
|
72
85
|
Each peer advertises its receive window during authentication and stream open;
|
|
73
|
-
senders use the smaller local and peer window for stream credit. A
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
senders use the smaller local and peer window for stream credit. A hub-directed
|
|
87
|
+
physical replacement is transient and reconnects the client and gateway
|
|
88
|
+
registrations, while loss of the hub's retained state, authentication,
|
|
89
|
+
authorization, and stale-generation rejections remain terminal.
|
|
90
|
+
|
|
91
|
+
After an established stream loses either reverse physical transport, the hub
|
|
92
|
+
retains its authenticated bridge for the configured resume window and fences the
|
|
93
|
+
old generation. It makes both logical endpoints reconnect so each can present a
|
|
94
|
+
paginated `RESUME` manifest containing the stable stream identifier, both
|
|
95
|
+
directional sequence/ACK positions, and half-close state. The hub releases the
|
|
96
|
+
replacement generation only after both manifests agree. Each sender then
|
|
97
|
+
replays only durable chunks above the receiver-confirmed high-water position,
|
|
98
|
+
with its flow-control window reduced by bytes already buffered but not yet
|
|
99
|
+
consumed at the receiver. A registered gateway completing this paired resume is
|
|
100
|
+
not healthy or eligible for a new selection or `OPEN`; retained peers alone may
|
|
101
|
+
attach to submit their resume manifests.
|
|
102
|
+
|
|
103
|
+
The hub owns a separate ordered output queue for every client and gateway
|
|
104
|
+
physical connection. Each queue defaults to a 32 MiB bound and can be set with
|
|
105
|
+
the programmatic `maxOutputBytesPerConnection` hub option. Backpressure on one
|
|
106
|
+
gateway therefore cannot pause client input for a healthy gateway in the same
|
|
107
|
+
route class. If one destination exceeds its bound, the hub closes that physical
|
|
108
|
+
connection and fails its mapped streams with `OUTPUT_BACKPRESSURE_LIMIT`;
|
|
109
|
+
sibling gateways and the route-class client connection remain usable.
|
|
76
110
|
|
|
77
111
|
An application write may be larger than the configured reverse-stream buffer.
|
|
78
112
|
The client admits it in bounded slices no larger than the negotiated peer window
|
|
@@ -90,19 +124,35 @@ in-memory gateway registry across independent cluster workers.
|
|
|
90
124
|
## Failure and replay boundary
|
|
91
125
|
|
|
92
126
|
Gateway registration reconnects with bounded exponential delay and a higher
|
|
93
|
-
generation. An established Docker stream
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
generation. An established Docker stream survives a transient physical loss
|
|
128
|
+
when the hub process and its retained state, live client object, outbound gateway process, and
|
|
129
|
+
original target socket remain available through the configured resume window.
|
|
130
|
+
Socketduct keeps
|
|
131
|
+
the app-facing socket open, durably spools each direction's unacknowledged raw
|
|
132
|
+
chunks before sending them, reconciles the two receiver-committed ACK positions,
|
|
133
|
+
and separately preserves each receiver's accepted high-water position. It
|
|
134
|
+
replays only bytes above the high-water position, retains buffered-but-unread
|
|
135
|
+
bytes against the flow-control window until ACK, and keeps terminal CLOSE state
|
|
136
|
+
until the echoed CLOSE arrives. It never creates a replacement logical stream
|
|
137
|
+
or reconnects dockerd, so an application request is not executed again merely
|
|
138
|
+
because the WAN transport changed.
|
|
139
|
+
|
|
140
|
+
This is transparent transport replay, not application-operation retry. Bytes
|
|
141
|
+
already committed to dockerd are acknowledged by the surviving gateway and are
|
|
142
|
+
not replayed; bytes already accepted by the surviving app-facing socket are
|
|
143
|
+
likewise not delivered twice. Sequence gaps, overlaps, identity/ownership
|
|
144
|
+
mismatches, invalid manifests, spool/buffer limits, and authorization failures
|
|
145
|
+
fail closed.
|
|
146
|
+
|
|
147
|
+
Reverse replay state is bounded per stream and connection and stored in private
|
|
148
|
+
temporary spool directories. ACKed chunks are truncated, and terminal
|
|
149
|
+
close/reset removes the spool. `status().spoolBytes` reports retained durable
|
|
150
|
+
payload. `registerRecoveredConnection()` remains unsupported for reverse mode:
|
|
151
|
+
a process replacement cannot inherit the live target socket or the prior
|
|
152
|
+
`VirtualSocket`. If either endpoint process disappears, the target socket is
|
|
153
|
+
lost, or the hub resume window expires, the logical stream fails once with the
|
|
154
|
+
causal session-loss/reset code and the application must reconcile at its own
|
|
155
|
+
protocol layer.
|
|
106
156
|
|
|
107
157
|
Target bytes already observed by the gateway are ordered before a subsequent
|
|
108
158
|
target-error reset. Stream `END` remains a directional half-close, while protocol
|
|
@@ -118,40 +168,44 @@ SOCKETDUCT_RELAY_HOST=0.0.0.0 \
|
|
|
118
168
|
SOCKETDUCT_RELAY_PORT=3100 \
|
|
119
169
|
SOCKETDUCT_TLS_CERT_FILE=/certs/hub.crt \
|
|
120
170
|
SOCKETDUCT_TLS_KEY_FILE=/certs/hub.key \
|
|
121
|
-
SOCKETDUCT_AUTH_TOKENS='
|
|
122
|
-
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS='
|
|
123
|
-
|
|
171
|
+
SOCKETDUCT_AUTH_TOKENS='server-a-starlink:<secret>,server-b-starlink:<secret>,tensorbuzz:<secret>' \
|
|
172
|
+
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS='server-a-starlink:server-a-starlink,server-b-starlink:server-b-starlink' \
|
|
173
|
+
SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES='tensorbuzz:starlink:server-a-starlink:docker-a:2375,tensorbuzz:starlink:server-b-starlink:docker-b:2375' \
|
|
124
174
|
node src/index.js
|
|
125
175
|
```
|
|
126
176
|
|
|
127
177
|
Values shown as `<secret>` are placeholders for secret-manager input, not
|
|
128
178
|
literal values. The runtime refuses to start without TLS, credentials,
|
|
129
|
-
bindings, and routes.
|
|
179
|
+
bindings, and either exact or route-class routes. Exact legacy rows remain
|
|
180
|
+
available as `SOCKETDUCT_REVERSE_ROUTES=username:gatewayId:host:port`.
|
|
181
|
+
Route-class rows use `username:routeClass:gatewayId:host:port`. If two rows make
|
|
182
|
+
different gateways eligible for the same authenticated class and target, that
|
|
183
|
+
`OPEN` is rejected as ambiguous rather than choosing one implicitly.
|
|
130
184
|
|
|
131
|
-
## One outbound member per
|
|
185
|
+
## One outbound member per target and route class
|
|
132
186
|
|
|
133
|
-
Run this once in each
|
|
134
|
-
|
|
187
|
+
Run this once in each target server's route-pinned network namespace, changing
|
|
188
|
+
the stable IDs, credential, and private target name for the member:
|
|
135
189
|
|
|
136
190
|
```sh
|
|
137
191
|
SOCKETDUCT_MODE=reverse-gateway \
|
|
138
|
-
SOCKETDUCT_REVERSE_GATEWAY_ID=
|
|
139
|
-
SOCKETDUCT_REVERSE_CONNECTION_ID=
|
|
192
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID=server-a-starlink \
|
|
193
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID=server_a_starlink_gateway_01 \
|
|
140
194
|
SOCKETDUCT_REVERSE_HUB_HOST=server3.example.net \
|
|
141
195
|
SOCKETDUCT_REVERSE_HUB_PORT=3100 \
|
|
142
196
|
SOCKETDUCT_REVERSE_TOKEN='<secret>' \
|
|
143
197
|
SOCKETDUCT_REVERSE_HUB_CA_FILE=/certs/hub-ca.crt \
|
|
144
198
|
SOCKETDUCT_REVERSE_HUB_SERVERNAME=server3.example.net \
|
|
145
|
-
SOCKETDUCT_TARGET_ALLOWLIST=docker-
|
|
199
|
+
SOCKETDUCT_TARGET_ALLOWLIST=docker-a:2375 \
|
|
146
200
|
node src/index.js
|
|
147
201
|
```
|
|
148
202
|
|
|
149
|
-
The namespace/default-route configuration remains outside Socketduct.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
on
|
|
153
|
-
|
|
154
|
-
Compose network.
|
|
203
|
+
The namespace/default-route configuration remains outside Socketduct. Each
|
|
204
|
+
gateway registration represents one exact target server and WAN class;
|
|
205
|
+
Socketduct does not aggregate away either identity. Socketduct needs no inbound
|
|
206
|
+
rule on the WAN router. Do not publish port 2375: the gateway resolves only its
|
|
207
|
+
allowlisted Docker shim and reaches plaintext Docker TCP on that server's
|
|
208
|
+
private Compose network.
|
|
155
209
|
|
|
156
210
|
## TensorBuzz transport set
|
|
157
211
|
|
|
@@ -160,8 +214,8 @@ import {MultiplexedSocketductTransportSet} from "socketduct/multiplexed"
|
|
|
160
214
|
|
|
161
215
|
const set = new MultiplexedSocketductTransportSet({
|
|
162
216
|
connectionId: "tensorbuzz_docker_transport_01",
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
routeClasses: ["o2", "zyxel", "starlink"].map((routeClass) => ({
|
|
218
|
+
routeClass,
|
|
165
219
|
endpoint: {
|
|
166
220
|
reverseHub: {
|
|
167
221
|
host: "127.0.0.1",
|
|
@@ -174,13 +228,25 @@ const set = new MultiplexedSocketductTransportSet({
|
|
|
174
228
|
})
|
|
175
229
|
|
|
176
230
|
const starting = set.start()
|
|
177
|
-
const socket = set.createConnection({host: "docker-
|
|
231
|
+
const socket = set.createConnection({host: "docker-a", port: 2375})
|
|
178
232
|
await starting
|
|
179
233
|
```
|
|
180
234
|
|
|
235
|
+
Route-class sockets expose immutable `routeClass` allocation metadata and an
|
|
236
|
+
undefined `gatewayId`. Exact-gateway sets retain immutable `gatewayId` metadata.
|
|
237
|
+
Callers cannot override either selection per open. Reverse streams remain
|
|
238
|
+
sticky to their selected gateway/target but resume transparently after a
|
|
239
|
+
transient physical transport replacement.
|
|
240
|
+
|
|
181
241
|
The socket is returned immediately and waits inside Socketduct until the set
|
|
182
242
|
reaches minimum readiness. New sockets rotate equally across ready members and
|
|
183
243
|
stay on their assigned member. If all members are reconnecting, new sockets wait
|
|
184
244
|
FIFO without selecting a connecting gateway; a recovered member resumes those
|
|
185
245
|
opens. A disconnected member leaves readiness and rejoins after authenticated
|
|
186
|
-
registration; existing
|
|
246
|
+
registration and paired stream resume; existing sockets remain open unless the
|
|
247
|
+
target/session is genuinely lost or a configured resume/buffer bound expires.
|
|
248
|
+
|
|
249
|
+
The paired reverse-resume fields are a protocol compatibility boundary. Upgrade
|
|
250
|
+
the hub, outbound gateways, and Server3/TensorBuzz Socketduct consumer together;
|
|
251
|
+
0.1.17 and older reverse peers do not exchange these manifests and cannot
|
|
252
|
+
provide transparent established-stream recovery.
|
package/docs/security.md
CHANGED
|
@@ -153,10 +153,31 @@ Reverse transports require verified TLS and negotiated zstd. There is no
|
|
|
153
153
|
plaintext or certificate-verification opt-out. Docker port 2375 is permitted
|
|
154
154
|
only as a private `docker-server:2375` target and is never host-published.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
the
|
|
159
|
-
|
|
156
|
+
A route-class client authenticates one explicit class on its physical
|
|
157
|
+
connection. It cannot name a gateway per stream. For every `OPEN`, the hub uses
|
|
158
|
+
the authenticated principal, bound class, and exact requested target to obtain
|
|
159
|
+
the eligible gateway IDs and proceeds only when there is exactly one. Unknown
|
|
160
|
+
or unauthorized classes, missing routes, duplicate eligible routes, class
|
|
161
|
+
mismatches, stale owners, and disconnected gateways fail closed. The resolved
|
|
162
|
+
gateway still passes the existing hub authorization and its own server-local
|
|
163
|
+
target allowlist; route classes introduce no wildcard authorization or fallback.
|
|
164
|
+
Hub output is ordered and byte-bounded per physical destination. A gateway that
|
|
165
|
+
stops reading cannot grow an unbounded queue or stall authorization, DATA, or
|
|
166
|
+
OPEN processing for a healthy sibling; exceeding the bound disconnects only
|
|
167
|
+
that destination and resets its owned streams.
|
|
168
|
+
|
|
169
|
+
Native reverse peers retain established logical streams for a bounded resume
|
|
170
|
+
window when only a hub transport is lost. Both authenticated endpoints must
|
|
171
|
+
reconnect with the same stable identities and newer generations, then present
|
|
172
|
+
matching per-stream identifiers, byte positions, acknowledgements, and
|
|
173
|
+
half-close state. Application bytes are durably spooled before transmission;
|
|
174
|
+
the hub reconciles to each receiver's committed position and permits replay
|
|
175
|
+
only above it. This preserves exact-once delivery without reopening or
|
|
176
|
+
reissuing a privileged Docker request. Ownership mismatches, missing targets,
|
|
177
|
+
invalid or incomplete manifests, resource-bound failures, and expired resume
|
|
178
|
+
windows fail the affected logical stream once instead of falling back to a new
|
|
179
|
+
stream. Process replacement remains terminal because the live app and target
|
|
180
|
+
sockets cannot be inherited safely.
|
|
160
181
|
|
|
161
182
|
## Network exposure
|
|
162
183
|
|
|
@@ -255,4 +276,6 @@ To roll back TLS: remove the TLS env vars from `.env`. If the relay binds to a n
|
|
|
255
276
|
- reverse registration rejects wrong principals, duplicate IDs, and stale generations
|
|
256
277
|
- reverse clients reject unknown/unavailable gateway IDs and wrong TLS CAs
|
|
257
278
|
- both hub route policy and the home-local allowlist deny unauthorized Docker targets
|
|
258
|
-
-
|
|
279
|
+
- transient reverse hub transport loss preserves each live stream and target,
|
|
280
|
+
replays only unacknowledged spooled bytes, and cleans all retained state after
|
|
281
|
+
close; target loss and resume expiry fail once with their causal codes
|
|
@@ -11,22 +11,33 @@ spawn per-stream reconnects.
|
|
|
11
11
|
## Reverse-gateway lifecycle
|
|
12
12
|
|
|
13
13
|
An outbound gateway moves through `connecting -> authenticated -> registering
|
|
14
|
-
-> ready`. Transport loss
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
-> ready`. Transport loss advances the gateway generation and reconnects with
|
|
15
|
+
bounded exponential delay without destroying established local targets. The hub
|
|
16
|
+
retains authenticated bridge ownership for the configured resume window and
|
|
17
|
+
replaces the affected Server3 client physical generation as well.
|
|
18
|
+
|
|
19
|
+
Both reverse endpoints keep their logical sockets alive, persist unacknowledged
|
|
20
|
+
send chunks, and submit their stable identifiers plus both directional
|
|
21
|
+
sequence/ACK positions. The hub resumes a bridge only after the two manifests
|
|
22
|
+
agree and returns both the receiver's accepted high-water sequence and its
|
|
23
|
+
application-consumed ACK to each sender. Bytes above the high-water position
|
|
24
|
+
replay on the same logical stream; accepted-but-unread bytes stay charged to the
|
|
25
|
+
window and spooled until ACK. This preserves exact-once, in-order delivery
|
|
26
|
+
without opening a second target or retrying an application operation.
|
|
27
|
+
|
|
28
|
+
If the original target socket disappears, an owner/identifier or sequence does
|
|
29
|
+
not match, a buffer/spool bound is exceeded, or the resume window expires, that
|
|
30
|
+
logical stream fails once with the causal error and releases both sides. Reverse
|
|
31
|
+
process-replacement recovery remains unsupported because the live target and
|
|
32
|
+
app-facing socket cannot be transferred to replacement JavaScript objects.
|
|
23
33
|
|
|
24
34
|
The bounded, paginated resume manifest carries stable identifiers and both
|
|
25
35
|
directional acknowledgement positions. The relay returns an independent result
|
|
26
36
|
per stream. A stream omitted because it was closed while disconnected is
|
|
27
37
|
cleaned up at the relay. A target lost during interruption returns a stream-local
|
|
28
38
|
resume reset and a typed session-loss error, while retained siblings continue.
|
|
29
|
-
Only bytes above the peer
|
|
39
|
+
Only bytes above the peer's received high-water sequence are replayed; spool
|
|
40
|
+
cleanup remains governed by its acknowledgement.
|
|
30
41
|
|
|
31
42
|
## States
|
|
32
43
|
|