socketduct 0.1.16 → 0.1.17
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/docs/architecture.md +16 -3
- package/docs/protocol.md +17 -5
- package/docs/reverse-gateway.md +68 -37
- package/docs/security.md +13 -0
- package/docs/socket-api.md +50 -21
- package/package.json +1 -1
- package/src/client/deferred-virtual-socket.js +16 -4
- package/src/client/multiplexed-transport-set.js +105 -65
- package/src/config/environment.js +24 -1
- package/src/reverse/client.js +31 -17
- package/src/reverse/common.js +1 -0
- package/src/reverse/hub.js +163 -32
- package/src/reverse/runtime.js +31 -6
- package/test/e2e/reverse-gateway-process.test.js +93 -0
- package/test/integration/reverse-route-class.test.js +373 -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.
|
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,20 @@ 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
|
-
|
|
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.
|
|
99
108
|
|
|
100
109
|
Gateway registration reconnects with generation fencing. Established reverse
|
|
101
110
|
streams are terminal on transport loss because replaying an ambiguous Docker
|
|
102
111
|
operation would be unsafe. Live-transport buffers remain bounded; durable
|
|
103
112
|
spooling and resume continue to apply to ordinary Socketduct sessions only.
|
|
113
|
+
The hub serializes output independently for each physical destination with a
|
|
114
|
+
bounded queue, so one backpressured target gateway cannot stop client frames
|
|
115
|
+
bound for a healthy sibling gateway. Exceeding that destination bound closes
|
|
116
|
+
only the unhealthy physical owner and releases its mapped streams.
|
|
104
117
|
See [Native reverse gateway](reverse-gateway.md).
|
|
105
118
|
|
|
106
119
|
### Edge
|
package/docs/protocol.md
CHANGED
|
@@ -103,11 +103,23 @@ relay's per-stream result rather than being sent again.
|
|
|
103
103
|
### Native reverse-gateway extension
|
|
104
104
|
|
|
105
105
|
Reverse mode reuses the v3 physical transport and stream frames directly. A
|
|
106
|
-
home gateway registers outbound
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
home gateway registers outbound. A Server3 client selects exactly one legacy
|
|
107
|
+
`gatewayId` or one first-class `routeClass` in `GATEWAY_SELECT`; the response
|
|
108
|
+
echoes the same selector. Client streams are odd and the hub maps them to even
|
|
109
|
+
gateway-side IDs. For a class-bound connection, every `OPEN` repeats the bound
|
|
110
|
+
class and the hub resolves `{authenticated principal, routeClass, target}` to
|
|
111
|
+
exactly one current gateway. The client never supplies that gateway ID. The hub
|
|
112
|
+
then authorizes the exact principal/gateway/target tuple and the gateway
|
|
113
|
+
separately authorizes its local target. Zero, multiple, stale, disconnected, or
|
|
114
|
+
mismatched resolutions reset the stream without weakening sibling routes.
|
|
115
|
+
Registration reconnect uses a higher generation, but any established stream on
|
|
116
|
+
the lost gateway is reset and never replayed.
|
|
117
|
+
|
|
118
|
+
Hub output has one ordered owner per physical destination. Frames for a
|
|
119
|
+
backpressured gateway remain in that gateway's bounded queue while frames for
|
|
120
|
+
other gateways continue independently; a destination that exceeds its output
|
|
121
|
+
bound is disconnected and its streams are reset without blocking healthy
|
|
122
|
+
destinations. Ordering within each destination and stream is preserved.
|
|
111
123
|
|
|
112
124
|
Stream IDs must increase monotonically on each physical connection. END and
|
|
113
125
|
CLOSE positions must match the sender's accepted raw-byte sequence; a
|
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
|
|
@@ -58,7 +67,10 @@ malformed frames, unauthorized routes, and configured limits. A higher
|
|
|
58
67
|
generation from the same stable connection identity fences its prior transport.
|
|
59
68
|
At the configured identity bound, the oldest disconnected client identity is
|
|
60
69
|
evicted; active owners are never evicted and retain generation fencing.
|
|
61
|
-
|
|
70
|
+
Zero eligible gateways denies the target, and multiple eligible gateways deny
|
|
71
|
+
the stream as ambiguous. Unknown classes, class mismatches, stale registrations,
|
|
72
|
+
and disconnected resolved gateways also fail closed. The home gateway
|
|
73
|
+
independently rejects any target outside its local allowlist.
|
|
62
74
|
|
|
63
75
|
Reverse DATA compression is negotiated as `zstd`. The one DATA-only flag means
|
|
64
76
|
the payload starts with a four-byte uncompressed length followed by one
|
|
@@ -74,6 +86,14 @@ senders use the smaller local and peer window for stream credit. A graceful hub
|
|
|
74
86
|
shutdown is transient and reconnects the client and gateway registrations, while
|
|
75
87
|
authentication, authorization, and stale-generation rejections remain terminal.
|
|
76
88
|
|
|
89
|
+
The hub owns a separate ordered output queue for every client and gateway
|
|
90
|
+
physical connection. Each queue defaults to a 32 MiB bound and can be set with
|
|
91
|
+
the programmatic `maxOutputBytesPerConnection` hub option. Backpressure on one
|
|
92
|
+
gateway therefore cannot pause client input for a healthy gateway in the same
|
|
93
|
+
route class. If one destination exceeds its bound, the hub closes that physical
|
|
94
|
+
connection and its mapped streams through the normal detach path; sibling
|
|
95
|
+
gateways and the route-class client connection remain usable.
|
|
96
|
+
|
|
77
97
|
An application write may be larger than the configured reverse-stream buffer.
|
|
78
98
|
The client admits it in bounded slices no larger than the negotiated peer window
|
|
79
99
|
as acknowledgements release capacity, holding Node's Writable callback while
|
|
@@ -92,7 +112,9 @@ in-memory gateway registry across independent cluster workers.
|
|
|
92
112
|
Gateway registration reconnects with bounded exponential delay and a higher
|
|
93
113
|
generation. An established Docker stream does **not** survive that transport
|
|
94
114
|
loss: the hub resets it with `GATEWAY_DISCONNECTED`, both endpoints release the
|
|
95
|
-
stream, and the gateway destroys its local Docker socket.
|
|
115
|
+
stream, and the gateway destroys its local Docker socket. A route-class client
|
|
116
|
+
physical connection stays authenticated, so streams through other gateways in
|
|
117
|
+
the class and later opens to those targets remain usable. This is intentional.
|
|
96
118
|
A Docker request whose write reached dockerd but whose response did not reach
|
|
97
119
|
TensorBuzz has an ambiguous outcome and must never be replayed automatically.
|
|
98
120
|
TensorBuzz must reconcile that Docker operation and then create a new stream.
|
|
@@ -118,40 +140,44 @@ SOCKETDUCT_RELAY_HOST=0.0.0.0 \
|
|
|
118
140
|
SOCKETDUCT_RELAY_PORT=3100 \
|
|
119
141
|
SOCKETDUCT_TLS_CERT_FILE=/certs/hub.crt \
|
|
120
142
|
SOCKETDUCT_TLS_KEY_FILE=/certs/hub.key \
|
|
121
|
-
SOCKETDUCT_AUTH_TOKENS='
|
|
122
|
-
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS='
|
|
123
|
-
|
|
143
|
+
SOCKETDUCT_AUTH_TOKENS='server-a-starlink:<secret>,server-b-starlink:<secret>,tensorbuzz:<secret>' \
|
|
144
|
+
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS='server-a-starlink:server-a-starlink,server-b-starlink:server-b-starlink' \
|
|
145
|
+
SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES='tensorbuzz:starlink:server-a-starlink:docker-a:2375,tensorbuzz:starlink:server-b-starlink:docker-b:2375' \
|
|
124
146
|
node src/index.js
|
|
125
147
|
```
|
|
126
148
|
|
|
127
149
|
Values shown as `<secret>` are placeholders for secret-manager input, not
|
|
128
150
|
literal values. The runtime refuses to start without TLS, credentials,
|
|
129
|
-
bindings, and routes.
|
|
151
|
+
bindings, and either exact or route-class routes. Exact legacy rows remain
|
|
152
|
+
available as `SOCKETDUCT_REVERSE_ROUTES=username:gatewayId:host:port`.
|
|
153
|
+
Route-class rows use `username:routeClass:gatewayId:host:port`. If two rows make
|
|
154
|
+
different gateways eligible for the same authenticated class and target, that
|
|
155
|
+
`OPEN` is rejected as ambiguous rather than choosing one implicitly.
|
|
130
156
|
|
|
131
|
-
## One outbound member per
|
|
157
|
+
## One outbound member per target and route class
|
|
132
158
|
|
|
133
|
-
Run this once in each
|
|
134
|
-
|
|
159
|
+
Run this once in each target server's route-pinned network namespace, changing
|
|
160
|
+
the stable IDs, credential, and private target name for the member:
|
|
135
161
|
|
|
136
162
|
```sh
|
|
137
163
|
SOCKETDUCT_MODE=reverse-gateway \
|
|
138
|
-
SOCKETDUCT_REVERSE_GATEWAY_ID=
|
|
139
|
-
SOCKETDUCT_REVERSE_CONNECTION_ID=
|
|
164
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID=server-a-starlink \
|
|
165
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID=server_a_starlink_gateway_01 \
|
|
140
166
|
SOCKETDUCT_REVERSE_HUB_HOST=server3.example.net \
|
|
141
167
|
SOCKETDUCT_REVERSE_HUB_PORT=3100 \
|
|
142
168
|
SOCKETDUCT_REVERSE_TOKEN='<secret>' \
|
|
143
169
|
SOCKETDUCT_REVERSE_HUB_CA_FILE=/certs/hub-ca.crt \
|
|
144
170
|
SOCKETDUCT_REVERSE_HUB_SERVERNAME=server3.example.net \
|
|
145
|
-
SOCKETDUCT_TARGET_ALLOWLIST=docker-
|
|
171
|
+
SOCKETDUCT_TARGET_ALLOWLIST=docker-a:2375 \
|
|
146
172
|
node src/index.js
|
|
147
173
|
```
|
|
148
174
|
|
|
149
|
-
The namespace/default-route configuration remains outside Socketduct.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
on
|
|
153
|
-
|
|
154
|
-
Compose network.
|
|
175
|
+
The namespace/default-route configuration remains outside Socketduct. Each
|
|
176
|
+
gateway registration represents one exact target server and WAN class;
|
|
177
|
+
Socketduct does not aggregate away either identity. Socketduct needs no inbound
|
|
178
|
+
rule on the WAN router. Do not publish port 2375: the gateway resolves only its
|
|
179
|
+
allowlisted Docker shim and reaches plaintext Docker TCP on that server's
|
|
180
|
+
private Compose network.
|
|
155
181
|
|
|
156
182
|
## TensorBuzz transport set
|
|
157
183
|
|
|
@@ -160,8 +186,8 @@ import {MultiplexedSocketductTransportSet} from "socketduct/multiplexed"
|
|
|
160
186
|
|
|
161
187
|
const set = new MultiplexedSocketductTransportSet({
|
|
162
188
|
connectionId: "tensorbuzz_docker_transport_01",
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
routeClasses: ["o2", "zyxel", "starlink"].map((routeClass) => ({
|
|
190
|
+
routeClass,
|
|
165
191
|
endpoint: {
|
|
166
192
|
reverseHub: {
|
|
167
193
|
host: "127.0.0.1",
|
|
@@ -174,10 +200,15 @@ const set = new MultiplexedSocketductTransportSet({
|
|
|
174
200
|
})
|
|
175
201
|
|
|
176
202
|
const starting = set.start()
|
|
177
|
-
const socket = set.createConnection({host: "docker-
|
|
203
|
+
const socket = set.createConnection({host: "docker-a", port: 2375})
|
|
178
204
|
await starting
|
|
179
205
|
```
|
|
180
206
|
|
|
207
|
+
Route-class sockets expose immutable `routeClass` allocation metadata and an
|
|
208
|
+
undefined `gatewayId`. Exact-gateway sets retain immutable `gatewayId` metadata.
|
|
209
|
+
Callers cannot override either selection per open. Reverse streams remain
|
|
210
|
+
non-recoverable after an uncertain transport loss.
|
|
211
|
+
|
|
181
212
|
The socket is returned immediately and waits inside Socketduct until the set
|
|
182
213
|
reaches minimum readiness. New sockets rotate equally across ready members and
|
|
183
214
|
stay on their assigned member. If all members are reconnecting, new sockets wait
|
package/docs/security.md
CHANGED
|
@@ -153,6 +153,19 @@ 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
|
+
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
|
+
|
|
156
169
|
Reverse registration can reconnect; established Docker streams cannot resume
|
|
157
170
|
or spool across loss. They fail with an explicit uncertain-operation error so
|
|
158
171
|
the caller must reconcile before retrying. This prevents privileged Docker API
|
package/docs/socket-api.md
CHANGED
|
@@ -104,7 +104,7 @@ responsible for durably reconciling application state after uncertain results.
|
|
|
104
104
|
`detach()` requires an explicit `connectionId`, preserves active spools and
|
|
105
105
|
does not send stream resets. `close()` remains terminal and removes spools.
|
|
106
106
|
|
|
107
|
-
### `MultiplexedSocketductTransportSet` (multi-
|
|
107
|
+
### `MultiplexedSocketductTransportSet` (multi-route v3 API)
|
|
108
108
|
|
|
109
109
|
Use one transport set per Node process when the process has several genuinely
|
|
110
110
|
distinct gateway paths:
|
|
@@ -133,14 +133,38 @@ agent.createConnection = set.createConnection
|
|
|
133
133
|
await starting
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
For native reverse hubs, configure first-class route classes instead of exact
|
|
137
|
+
gateways. Exactly one of `gateways` or `routeClasses` is accepted:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const set = new MultiplexedSocketductTransportSet({
|
|
141
|
+
connectionId: "runner_01_stable_identity",
|
|
142
|
+
minimumReadyMembers: 1,
|
|
143
|
+
routeClasses: ["starlink", "zyxel", "o2"].map((routeClass) => ({
|
|
144
|
+
routeClass,
|
|
145
|
+
endpoint: {
|
|
146
|
+
reverseHub: {host: "hub.example.net", port: 3100, token},
|
|
147
|
+
tls: {caFile: "/certs/hub-ca.crt", servername: "hub.example.net"}
|
|
148
|
+
}
|
|
149
|
+
}))
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Each route-class member owns one physical connection and stable identity for
|
|
154
|
+
the `{connectionId, routeClass}` pair. The hub resolves every requested target
|
|
155
|
+
to one exact registered gateway, so the physical count is independent of the
|
|
156
|
+
number of server-local target gateways. Route classes require a verified
|
|
157
|
+
`reverseHub` endpoint; they cannot wrap an ordinary relay.
|
|
158
|
+
|
|
159
|
+
For the exact-gateway relay form, every entry must have a stable 2-64 character
|
|
160
|
+
`gatewayId` and an explicit `endpoint.relay`. An optional, gateway-specific
|
|
161
|
+
`endpoint.createPhysicalSocket` can pin an interface, routing table or external
|
|
162
|
+
transport. Repeating the same relay address without distinct pinned factories
|
|
163
|
+
is rejected: assigning three names to one load-balanced endpoint is not path
|
|
164
|
+
diversity. Socketduct cannot inspect arbitrary DNS or factory routing, so callers
|
|
165
|
+
remain responsible for making each declared endpoint/factory genuinely
|
|
166
|
+
gateway-pinned. Blank, duplicate and path-unsafe IDs and malformed endpoint
|
|
167
|
+
entries fail synchronously.
|
|
144
168
|
|
|
145
169
|
`start()` constructs no new members: the constructor creates exactly one
|
|
146
170
|
`MultiplexedSocketductConnection` per gateway and `start()` warms all of them
|
|
@@ -159,12 +183,15 @@ its normal `connecting`/`pending` state. Opens wait FIFO until initial
|
|
|
159
183
|
member may resume the queue. New calls never wait behind a reconnecting route
|
|
160
184
|
when a healthy sibling is ready.
|
|
161
185
|
|
|
162
|
-
Waiting does not assign an anonymous or merely connecting member.
|
|
163
|
-
is `undefined` until one ready member accepts the open, then
|
|
164
|
-
immutable configured ID
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
member
|
|
186
|
+
Waiting does not assign an anonymous or merely connecting member. For an exact
|
|
187
|
+
set, `gatewayId` is `undefined` until one ready member accepts the open, then
|
|
188
|
+
becomes the stable, immutable configured ID. For a route-class set,
|
|
189
|
+
`routeClass` behaves the same way and `gatewayId` remains undefined. Destroying
|
|
190
|
+
the socket first removes the pending open, so later readiness cannot leak a
|
|
191
|
+
member stream. An exact relay member selected immediately before its path drops
|
|
192
|
+
retains the stream and performs member-local v3 recovery; the set never
|
|
193
|
+
speculatively migrates it. A reverse route-class stream fails terminally on an
|
|
194
|
+
uncertain transport loss and is never replayed.
|
|
168
195
|
|
|
169
196
|
`createConnection(options, callback)` is pre-bound and invokes its callback once
|
|
170
197
|
on `connect` or the terminal pre-connect error; the socket emits `connect` before
|
|
@@ -189,9 +216,11 @@ socket with ordinary `tls.connect` target options such as `servername`, `ca`,
|
|
|
189
216
|
`cert` and `key`. Node TLS owns the handshake, `secureConnect`, TLS errors and
|
|
190
217
|
the callback, which runs exactly once only after `secureConnect` or failure.
|
|
191
218
|
Both methods count only completed member assignments and preserve stream
|
|
192
|
-
stickiness. Every returned raw or TLS socket exposes enumerable `gatewayId
|
|
193
|
-
|
|
194
|
-
|
|
219
|
+
stickiness. Every returned raw or TLS socket exposes enumerable `gatewayId` and
|
|
220
|
+
`routeClass` allocation metadata. Only the configured identity is set. Persist
|
|
221
|
+
an exact set's immutable `gatewayId` after connection so a replacement can
|
|
222
|
+
recover the stream on its original member. Reverse route-class streams are not
|
|
223
|
+
recoverable after an uncertain transport loss.
|
|
195
224
|
|
|
196
225
|
Process-replacement recovery is deliberately separate from ordinary selection.
|
|
197
226
|
First detach the old set when an orderly handoff is possible. Construct the
|
|
@@ -217,8 +246,8 @@ await replacement.start()
|
|
|
217
246
|
gateway ID, an explicit `recover` option, or registration after `start()` has
|
|
218
247
|
begun. It bypasses round-robin only to reach that configured member; it never
|
|
219
248
|
migrates or replays the stream on a sibling. Ordinary `createConnection()` and
|
|
220
|
-
`createTlsConnection()` reject `recover: true` and `
|
|
221
|
-
cannot force a path. `detach()` coalesces with concurrent shutdown calls,
|
|
249
|
+
`createTlsConnection()` reject `recover: true`, `gatewayId`, and `routeClass`,
|
|
250
|
+
so new opens cannot force a path. `detach()` coalesces with concurrent shutdown calls,
|
|
222
251
|
preserves active member spools, and retires empty warmed lanes; `close()` remains
|
|
223
252
|
terminal and removes their spools.
|
|
224
253
|
|
|
@@ -239,7 +268,7 @@ capacity occupied and fails the affected member visibly.
|
|
|
239
268
|
|
|
240
269
|
`status()` returns only safe aggregate counts/bytes, `pendingOpens`, its maximum,
|
|
241
270
|
and an ordered member list:
|
|
242
|
-
gateway ID, lifecycle state, assignments, physical connects/reconnects, virtual
|
|
271
|
+
gateway ID or route class, lifecycle state, assignments, physical connects/reconnects, virtual
|
|
243
272
|
stream count and queued/spool bytes. It excludes relays, targets, tokens,
|
|
244
273
|
credentials, names, payloads and spool contents. Concurrent `close()` calls
|
|
245
274
|
share one promise. Every member is attempted once in configured order; multiple
|
package/package.json
CHANGED
|
@@ -212,6 +212,9 @@ export class DeferredVirtualSocket extends VirtualSocket {
|
|
|
212
212
|
/** @type {string | undefined} */
|
|
213
213
|
#gatewayId
|
|
214
214
|
|
|
215
|
+
/** @type {string | undefined} */
|
|
216
|
+
#routeClass
|
|
217
|
+
|
|
215
218
|
/**
|
|
216
219
|
* @param {{host: string, port: number, allowHalfOpen?: boolean, readableHighWaterMark?: number, writableHighWaterMark?: number}} socketOptions - Target/socket options.
|
|
217
220
|
* @param {DeferredVirtualSocketOptions} deferredOptions - Set-owned wait policy.
|
|
@@ -222,6 +225,7 @@ export class DeferredVirtualSocket extends VirtualSocket {
|
|
|
222
225
|
this.#owner = owner
|
|
223
226
|
owner.attach(this)
|
|
224
227
|
Object.defineProperty(this, "gatewayId", {get: () => this.#gatewayId, enumerable: true, configurable: false})
|
|
228
|
+
Object.defineProperty(this, "routeClass", {get: () => this.#routeClass, enumerable: true, configurable: false})
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
/** @returns {string | undefined} Immutable assignment selected only after readiness. */
|
|
@@ -229,6 +233,11 @@ export class DeferredVirtualSocket extends VirtualSocket {
|
|
|
229
233
|
return this.#gatewayId
|
|
230
234
|
}
|
|
231
235
|
|
|
236
|
+
/** @returns {string | undefined} Immutable route-class assignment selected only after readiness. */
|
|
237
|
+
get routeClass() {
|
|
238
|
+
return this.#routeClass
|
|
239
|
+
}
|
|
240
|
+
|
|
232
241
|
/**
|
|
233
242
|
* @override
|
|
234
243
|
* @returns {number} Assigned protocol stream ID, or zero while unassigned.
|
|
@@ -239,13 +248,16 @@ export class DeferredVirtualSocket extends VirtualSocket {
|
|
|
239
248
|
|
|
240
249
|
/**
|
|
241
250
|
* @param {VirtualSocket} socket - Selected ready-member socket.
|
|
242
|
-
* @param {string
|
|
251
|
+
* @param {string | {gatewayId?: string, routeClass?: string}} selection - Stable selected member identity.
|
|
243
252
|
* @returns {boolean} Whether assignment succeeded before cancellation.
|
|
244
253
|
*/
|
|
245
|
-
assign(socket,
|
|
246
|
-
if (this.#gatewayId !== undefined) throw new Error("Deferred Socketduct socket
|
|
254
|
+
assign(socket, selection) {
|
|
255
|
+
if (this.#gatewayId !== undefined || this.#routeClass !== undefined) throw new Error("Deferred Socketduct socket member is already assigned")
|
|
256
|
+
const allocation = typeof selection === "string" ? {gatewayId: selection} : selection
|
|
257
|
+
if ((typeof allocation.gatewayId === "string") === (typeof allocation.routeClass === "string")) throw new Error("Deferred Socketduct socket requires exactly one gatewayId or routeClass assignment")
|
|
247
258
|
if (!this.#owner.assign(socket)) return false
|
|
248
|
-
this.#gatewayId = gatewayId
|
|
259
|
+
this.#gatewayId = allocation.gatewayId
|
|
260
|
+
this.#routeClass = allocation.routeClass
|
|
249
261
|
return true
|
|
250
262
|
}
|
|
251
263
|
|