socketduct 0.1.15
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/.dockerignore +11 -0
- package/.env.example +131 -0
- package/AGENTS.md +168 -0
- package/Dockerfile +30 -0
- package/README.md +134 -0
- package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
- package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
- package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
- package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
- package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
- package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
- package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
- package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
- package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
- package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
- package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
- package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
- package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
- package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
- package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
- package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
- package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
- package/changelog.d/20260902231542-reverse-close-position.md +1 -0
- package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
- package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
- package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
- package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
- package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
- package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
- package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
- package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
- package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
- package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
- package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
- package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
- package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
- package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
- package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
- package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
- package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
- package/docker-compose.dev.yml +19 -0
- package/docker-compose.reverse-gateway.yml +36 -0
- package/docker-compose.yml +73 -0
- package/docs/architecture.md +264 -0
- package/docs/docker-topology.md +164 -0
- package/docs/docker.md +281 -0
- package/docs/logging.md +185 -0
- package/docs/plans/initial-implementation.md +205 -0
- package/docs/plans/socket-docker-integration.md +221 -0
- package/docs/protocol.md +558 -0
- package/docs/releasing.md +18 -0
- package/docs/reverse-gateway.md +186 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
- package/docs/running.md +215 -0
- package/docs/security.md +258 -0
- package/docs/session-lifecycle.md +127 -0
- package/docs/socket-api.md +448 -0
- package/eslint.config.js +36 -0
- package/package.json +44 -0
- package/scripts/deploy-gateway.js +194 -0
- package/scripts/test.js +28 -0
- package/src/auth/passwords.js +38 -0
- package/src/auth/static-token-store.js +41 -0
- package/src/auth/user-store.js +139 -0
- package/src/client/connect.js +76 -0
- package/src/client/deferred-virtual-socket.js +312 -0
- package/src/client/http-agent.js +101 -0
- package/src/client/https-agent.js +366 -0
- package/src/client/multiplexed-connection.js +1504 -0
- package/src/client/multiplexed-transport-set.js +917 -0
- package/src/client/multiplexed.js +3 -0
- package/src/client/socketduct-socket.js +1174 -0
- package/src/client/virtual-socket.js +256 -0
- package/src/config/environment.js +400 -0
- package/src/edge/tcp-edge.js +82 -0
- package/src/index.js +101 -0
- package/src/logging/structured-logger.js +745 -0
- package/src/protocol/compression.js +325 -0
- package/src/protocol/frame.js +341 -0
- package/src/protocol/v3-compression.js +44 -0
- package/src/protocol/v3-frame.js +209 -0
- package/src/proxy/multiplexed-relay.js +798 -0
- package/src/proxy/tcp-relay.js +1191 -0
- package/src/relay.js +3 -0
- package/src/reverse/client.js +721 -0
- package/src/reverse/common.js +88 -0
- package/src/reverse/gateway.js +663 -0
- package/src/reverse/hub.js +563 -0
- package/src/reverse/index.js +3 -0
- package/src/reverse/runtime.js +78 -0
- package/src/server/control-handler.js +170 -0
- package/src/server/primary.js +64 -0
- package/src/server/relay-runtime.js +124 -0
- package/src/server/worker.js +39 -0
- package/src/sessions/session.js +212 -0
- package/src/spool/spool-file.js +451 -0
- package/tensorbuzz.yml +36 -0
- package/test/e2e/gateway-process.test.js +262 -0
- package/test/e2e/reverse-gateway-process.test.js +285 -0
- package/test/fixtures/logging/enospc-matrix.mjs +238 -0
- package/test/fixtures/logging/enospc-recovery.mjs +146 -0
- package/test/fixtures/logging/process-writer.mjs +20 -0
- package/test/helpers/tls-certificates.js +115 -0
- package/test/integration/initial-target-recovery.test.js +518 -0
- package/test/integration/logging-recovery.test.js +163 -0
- package/test/integration/multiplexed-compatibility.test.js +254 -0
- package/test/integration/multiplexed-corrections.test.js +551 -0
- package/test/integration/multiplexed-failures.test.js +339 -0
- package/test/integration/multiplexed-transport-set.test.js +456 -0
- package/test/integration/multiplexed-transport.test.js +208 -0
- package/test/integration/reverse-gateway.test.js +1293 -0
- package/test/integration/socketduct-http-agent.test.js +435 -0
- package/test/integration/socketduct-https-agent.test.js +669 -0
- package/test/integration/stream-compression.test.js +1267 -0
- package/test/integration/tcp-forwarding.test.js +2878 -0
- package/test/integration/tls-forwarding.test.js +748 -0
- package/test/unit/auth/auth.test.js +64 -0
- package/test/unit/auth/static-token-store.test.js +14 -0
- package/test/unit/client/connect.test.js +58 -0
- package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
- package/test/unit/client/multiplexed-transport-set.test.js +906 -0
- package/test/unit/client/socketduct-socket.test.js +1124 -0
- package/test/unit/client/virtual-socket.test.js +98 -0
- package/test/unit/config/docker-gateway-files.test.js +274 -0
- package/test/unit/config/environment.test.js +507 -0
- package/test/unit/index.test.js +78 -0
- package/test/unit/logging/structured-logger.test.js +674 -0
- package/test/unit/protocol/compression.test.js +421 -0
- package/test/unit/protocol/frame.test.js +336 -0
- package/test/unit/protocol/v3-compression.test.js +31 -0
- package/test/unit/protocol/v3-frame.test.js +57 -0
- package/test/unit/release-configuration.test.js +21 -0
- package/test/unit/server/control-handler.test.js +291 -0
- package/test/unit/server/primary.test.js +59 -0
- package/test/unit/server/relay-runtime.test.js +434 -0
- package/test/unit/sessions/session.test.js +84 -0
- package/test/unit/spool/spool-file.test.js +295 -0
- package/tsconfig.json +20 -0
package/docs/security.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
Socketduct must not become an open proxy.
|
|
4
|
+
|
|
5
|
+
## Authentication
|
|
6
|
+
|
|
7
|
+
Authentication is required before any session operation:
|
|
8
|
+
|
|
9
|
+
- `session.create`
|
|
10
|
+
- `session.resume`
|
|
11
|
+
- stream attachment
|
|
12
|
+
- admin/user management
|
|
13
|
+
|
|
14
|
+
Initial implementation can use local users with password hashes.
|
|
15
|
+
|
|
16
|
+
Preferred password storage:
|
|
17
|
+
|
|
18
|
+
1. Argon2id
|
|
19
|
+
2. scrypt
|
|
20
|
+
3. bcrypt
|
|
21
|
+
|
|
22
|
+
Never store plaintext passwords.
|
|
23
|
+
|
|
24
|
+
## Authorization
|
|
25
|
+
|
|
26
|
+
Every session belongs to one user.
|
|
27
|
+
|
|
28
|
+
A user may only:
|
|
29
|
+
|
|
30
|
+
- create sessions within their allowed destination policy
|
|
31
|
+
- resume their own sessions
|
|
32
|
+
- close their own sessions
|
|
33
|
+
- inspect their own session status
|
|
34
|
+
|
|
35
|
+
Admin operations should be separate from normal tunnel operations.
|
|
36
|
+
|
|
37
|
+
## Destination policy
|
|
38
|
+
|
|
39
|
+
Default should be deny-by-default for generic proxying.
|
|
40
|
+
|
|
41
|
+
Protocol v3 authenticates the physical connection before accepting any OPEN,
|
|
42
|
+
DATA or resume operation. Authentication is performed once per physical
|
|
43
|
+
generation, but authorization is deliberately not cached across targets: every
|
|
44
|
+
virtual stream OPEN passes its exact `{host, port}` and authenticated principal
|
|
45
|
+
through the deny-by-default target policy. A denied/open-failed stream is reset
|
|
46
|
+
without affecting authorized siblings. Stream ID zero cannot carry application
|
|
47
|
+
data.
|
|
48
|
+
|
|
49
|
+
The current MVP relay is not a generic open proxy: every target is supplied in `session.create`, validated by an injected authorization function such as `authorizeTarget({principal, target})`, and stored on the session. Stream data is rejected until a `session.create` or `session.resume` message authenticates successfully. Broader proxy modes must keep this deny-by-default shape with explicit per-user allowlists before accepting arbitrary destinations.
|
|
50
|
+
|
|
51
|
+
Recommended first policy shape:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"allow": [
|
|
56
|
+
{ "host": "10.0.0.15", "port": 22 },
|
|
57
|
+
{ "host": "example.com", "port": 443 }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Later:
|
|
63
|
+
|
|
64
|
+
- CIDR allowlists
|
|
65
|
+
- DNS suffix allowlists
|
|
66
|
+
- per-user policies
|
|
67
|
+
- max bandwidth/session limits
|
|
68
|
+
|
|
69
|
+
## Tokens
|
|
70
|
+
|
|
71
|
+
After username/password login, the server may issue a short-lived reconnect token.
|
|
72
|
+
|
|
73
|
+
Token properties:
|
|
74
|
+
|
|
75
|
+
- random opaque value
|
|
76
|
+
- bound to user id
|
|
77
|
+
- optionally bound to session id
|
|
78
|
+
- expiration time
|
|
79
|
+
- revocable
|
|
80
|
+
|
|
81
|
+
Do not log token values.
|
|
82
|
+
|
|
83
|
+
## Spool files
|
|
84
|
+
|
|
85
|
+
Spool files may contain application traffic and must be treated as sensitive.
|
|
86
|
+
|
|
87
|
+
Requirements:
|
|
88
|
+
|
|
89
|
+
- owner-only file permissions (`0600`) where supported
|
|
90
|
+
- predictable directory but unpredictable filenames or session-owned paths
|
|
91
|
+
- no world-readable temp files
|
|
92
|
+
- cleanup on close/expiry
|
|
93
|
+
- safe startup cleanup of abandoned files
|
|
94
|
+
|
|
95
|
+
Optional later hardening:
|
|
96
|
+
|
|
97
|
+
- encrypt spool files at rest
|
|
98
|
+
- per-session spool keys
|
|
99
|
+
- authenticated checksums
|
|
100
|
+
|
|
101
|
+
## Logging
|
|
102
|
+
|
|
103
|
+
The optional persistent JSONL logger uses a closed event/field allowlist. It
|
|
104
|
+
records process/role identity, lifecycle state, bounded counters, validated
|
|
105
|
+
gateway IDs, and normalized error codes/classes. Unknown event names are
|
|
106
|
+
dropped and unknown error codes become `UNKNOWN`.
|
|
107
|
+
|
|
108
|
+
It never records protocol passwords, tokens, authentication frames, headers,
|
|
109
|
+
certificates or keys, environment values, session/stream names or identifiers,
|
|
110
|
+
spool paths/contents, raw application payload, or arbitrary objects. Error
|
|
111
|
+
diagnostics retain bounded error names, messages, and stacks so operational
|
|
112
|
+
failures can be investigated while credential-shaped values are redacted. Each
|
|
113
|
+
JSON line is capped at 1024 bytes. Logger I/O and custom-writer failures are
|
|
114
|
+
isolated from network behavior and produce only one bounded stderr
|
|
115
|
+
classification. See [Persistent structured logging](logging.md) for
|
|
116
|
+
configuration and rotation.
|
|
117
|
+
|
|
118
|
+
V3 status is payload-free and credential-free: physical counts/generation,
|
|
119
|
+
virtual stream state counts, active local targets, bounded queued/spool bytes,
|
|
120
|
+
resume outcomes, stable reset-reason counters and aggregate stream-open latency.
|
|
121
|
+
It never includes bearer tokens, passwords, target credentials, certificate/key
|
|
122
|
+
contents, stream names, payloads or spool contents.
|
|
123
|
+
|
|
124
|
+
Multi-gateway transport-set status follows the same boundary. It exposes only
|
|
125
|
+
configured gateway IDs, lifecycle/counter totals, assignment counts and bounded
|
|
126
|
+
byte counts. It never exposes relay or target addresses, bearer tokens, TLS
|
|
127
|
+
material, stream identifiers/names, payloads or spool contents. Gateway IDs are
|
|
128
|
+
restricted to short URL-safe values; opaque per-gateway connection identities
|
|
129
|
+
and spool subdirectories are derived with SHA-256 from the stable base identity
|
|
130
|
+
and gateway ID, preventing path traversal and cross-gateway spool collisions.
|
|
131
|
+
|
|
132
|
+
Transport-set lifecycle events include only normalized error codes and classes;
|
|
133
|
+
they never include arbitrary member error names, messages, or stacks.
|
|
134
|
+
|
|
135
|
+
Each gateway entry still authenticates its own v3 physical connection before
|
|
136
|
+
any target stream opens, and every target is authorized by its selected relay.
|
|
137
|
+
The set does not add unauthenticated fallback, anonymous members, or migration
|
|
138
|
+
to a sibling after an uncertain write. Shared process byte reservations prevent
|
|
139
|
+
multiple gateway members from multiplying the intended memory/spool ceiling.
|
|
140
|
+
Durable spool reservations remain charged until truncate/remove succeeds; a
|
|
141
|
+
cleanup failure is counted and closes the affected member without exposing its
|
|
142
|
+
stream identity, path or contents. Recovery requires the original configured
|
|
143
|
+
gateway ID before startup, preventing durable state from being probed or moved
|
|
144
|
+
through a sibling path.
|
|
145
|
+
|
|
146
|
+
Native reverse mode adds two independent authorization decisions. The hub
|
|
147
|
+
binds each configured gateway ID to an authenticated principal and authorizes
|
|
148
|
+
every client/gateway/target route. The selected home gateway then checks the
|
|
149
|
+
exact target against its own local deny-by-default allowlist. A gateway ID
|
|
150
|
+
cannot be claimed by another stable connection identity, stale generations are
|
|
151
|
+
rejected, and a newer generation from the same identity fences the old owner.
|
|
152
|
+
Reverse transports require verified TLS and negotiated zstd. There is no
|
|
153
|
+
plaintext or certificate-verification opt-out. Docker port 2375 is permitted
|
|
154
|
+
only as a private `docker-server:2375` target and is never host-published.
|
|
155
|
+
|
|
156
|
+
Reverse registration can reconnect; established Docker streams cannot resume
|
|
157
|
+
or spool across loss. They fail with an explicit uncertain-operation error so
|
|
158
|
+
the caller must reconcile before retrying. This prevents privileged Docker API
|
|
159
|
+
writes from being applied twice.
|
|
160
|
+
|
|
161
|
+
## Network exposure
|
|
162
|
+
|
|
163
|
+
Defaults should be conservative:
|
|
164
|
+
|
|
165
|
+
- edge local proxy listens on `127.0.0.1` unless configured otherwise
|
|
166
|
+
- relay tunnel endpoint listens on configured public interface
|
|
167
|
+
- admin API listens on `127.0.0.1` unless explicitly exposed
|
|
168
|
+
- require TLS or equivalent transport security before real use
|
|
169
|
+
|
|
170
|
+
Current implementation notes:
|
|
171
|
+
|
|
172
|
+
- `startTcpEdge` and `startTcpRelay` default to `127.0.0.1`.
|
|
173
|
+
- `startTcpRelay` has no static upstream target; callers must provide target authorization.
|
|
174
|
+
- `startTcpEdge` includes the requested target in `session.create`.
|
|
175
|
+
- The MVP protocol is framed TCP; protect it with TLS (configured via `SOCKETDUCT_TLS_CERT_FILE`/`SOCKETDUCT_TLS_KEY_FILE`) before exposing it on untrusted networks.
|
|
176
|
+
- Tokens are opaque and short-lived in memory, but should still be treated as bearer credentials.
|
|
177
|
+
- Spool chunks contain application payload and are written with owner-only file permissions where the platform supports them.
|
|
178
|
+
|
|
179
|
+
### TLS
|
|
180
|
+
|
|
181
|
+
For production deployment, configure TLS on the relay listener:
|
|
182
|
+
|
|
183
|
+
- Set `SOCKETDUCT_TLS_CERT_FILE` and `SOCKETDUCT_TLS_KEY_FILE` as an atomic pair pointing to PEM-encoded certificate and private key files. Both must be set or neither.
|
|
184
|
+
- Optionally set `SOCKETDUCT_TLS_CLIENT_CA_FILE` to a PEM-encoded client CA certificate to enable mutual TLS (mTLS). When set, clients must present a certificate signed by this CA.
|
|
185
|
+
- Certificate material is read at runtime with `readFileSync` and never logged. The relay fails fast on missing or unreadable certificate files.
|
|
186
|
+
- The relay enforces `rejectUnauthorized: true` for all TLS connections by default.
|
|
187
|
+
- When mTLS is enabled (`SOCKETDUCT_TLS_CLIENT_CA_FILE`), both `requestCert: true` and `rejectUnauthorized: true` are set.
|
|
188
|
+
|
|
189
|
+
### Insecure public opt-out
|
|
190
|
+
|
|
191
|
+
Binding to a non-loopback address (any interface other than `127.0.0.0/8`, `::1`, or `localhost`) without TLS is refused at startup unless you explicitly opt out:
|
|
192
|
+
|
|
193
|
+
- Set `SOCKETDUCT_ALLOW_INSECURE_PUBLIC=1` or `SOCKETDUCT_ALLOW_INSECURE_PUBLIC=true`.
|
|
194
|
+
- Any other value is rejected with a configuration error.
|
|
195
|
+
- Loopback plaintext binds remain compatible without this flag for local development and testing.
|
|
196
|
+
|
|
197
|
+
### Client-side TLS
|
|
198
|
+
|
|
199
|
+
The raw client (`connectSocketduct`, `SocketductSocket`, `SocketductHttpAgent`, `createSocketductConnectionFactory`) accepts an optional `tls` configuration for the edge-to-relay connection with:
|
|
200
|
+
|
|
201
|
+
- `caFile`: path to a PEM-encoded CA certificate to verify the relay server certificate.
|
|
202
|
+
- `certFile` and `keyFile`: an atomic pair of PEM-encoded client certificate and private key for mTLS. Both must be set or neither. Used when the relay requires client certificates.
|
|
203
|
+
- `servername`: SNI hostname for certificate identity validation. Defaults to `relayHost` by Node.js but can be overridden when the certificate CN/SAN differs from the connection host.
|
|
204
|
+
- `rejectUnauthorized`: defaults to `true`. Set to `false` only for testing.
|
|
205
|
+
|
|
206
|
+
`SocketductHttpsAgent` uses the clearer name `relayTls` for those relay settings and a separate `targetTls` object for target `ca`, client `cert`/`key`, `rejectUnauthorized`, and certificate `servername`. It never logs certificate or key material. Target verification defaults to strict, client certificate/key must be supplied atomically, and IP identities are verified without sending an IP literal as SNI.
|
|
207
|
+
|
|
208
|
+
Initial target retry does not weaken authorization: the requested destination is authenticated and allowlisted once before the session enters `creating`, and every attempt uses that stored target. No stream frames are accepted before readiness. After `session.created`, target failure and uncertain application writes are terminal to prevent duplicated privileged operations.
|
|
209
|
+
|
|
210
|
+
### Migration and rollback
|
|
211
|
+
|
|
212
|
+
Protocol v3 migration is explicit because v2 and v3 use different first bytes.
|
|
213
|
+
Set `SOCKETDUCT_PROTOCOL_VERSION=3` on the relay and use
|
|
214
|
+
`MultiplexedSocketductConnection` clients together. Keep the old v2 listener or
|
|
215
|
+
deployment available during a staged migration. Rollback consists of selecting
|
|
216
|
+
version `2` (the runtime default) and switching clients back to
|
|
217
|
+
`connectSocketduct`/the legacy agents. Never attempt an unauthenticated framing
|
|
218
|
+
fallback on the same connection.
|
|
219
|
+
|
|
220
|
+
To migrate an existing plaintext deployment to TLS:
|
|
221
|
+
|
|
222
|
+
1. Generate a TLS certificate and key pair for the relay hostname.
|
|
223
|
+
2. Place the certificate and key files where the relay container can read them (e.g., mount into `/certs`).
|
|
224
|
+
3. Set `SOCKETDUCT_TLS_CERT_FILE` and `SOCKETDUCT_TLS_KEY_FILE` in `.env`.
|
|
225
|
+
4. Mount the certificate directory in `docker-compose.yml` (`SOCKETDUCT_TLS_CERT_DIR` or the default `./certs`).
|
|
226
|
+
5. Update edges to use TLS by providing the server CA certificate via `tls.caFile`.
|
|
227
|
+
6. Set the verified digest-pinned `SOCKETDUCT_IMAGE` and restart the relay with
|
|
228
|
+
`npm run gateway:deploy` (or add `-- --docker-host` when using the private
|
|
229
|
+
Docker socket shim).
|
|
230
|
+
7. Verify edges connect successfully with TLS.
|
|
231
|
+
8. Once all edges are on TLS, optionally enable mTLS by setting `SOCKETDUCT_TLS_CLIENT_CA_FILE`.
|
|
232
|
+
|
|
233
|
+
To roll back TLS: remove the TLS env vars from `.env`. If the relay binds to a non-loopback address (`0.0.0.0`), you must set `SOCKETDUCT_ALLOW_INSECURE_PUBLIC=1` to start the relay without TLS on a public interface. For a loopback-only relay, plaintext works without the opt-out flag. Re-apply the previously verified image digest with the deployment command and restart edges without TLS options. Image rollback itself never uses a tag: change `SOCKETDUCT_IMAGE` back to the prior digest and re-apply.
|
|
234
|
+
|
|
235
|
+
### Threats to explicitly test
|
|
236
|
+
|
|
237
|
+
- unauthenticated session create is rejected
|
|
238
|
+
- wrong password is rejected
|
|
239
|
+
- user A cannot resume user B's session
|
|
240
|
+
- unknown identifier is rejected
|
|
241
|
+
- replaying an old spool file does not duplicate already-acked data
|
|
242
|
+
- spool path traversal is impossible
|
|
243
|
+
- destination not in allowlist is rejected
|
|
244
|
+
- non-loopback public bind without TLS or explicit opt-out is rejected at startup
|
|
245
|
+
- SOCKETDUCT_TLS_CERT_FILE without SOCKETDUCT_TLS_KEY_FILE (or vice versa) fails at config parse
|
|
246
|
+
- wrong CA on the client side is rejected during TLS handshake
|
|
247
|
+
- mTLS with a missing or untrusted client certificate is rejected
|
|
248
|
+
- plaintext clients can still connect to a plaintext relay
|
|
249
|
+
- TLS reconnection/replay preserves spooled data across TLS session resumption
|
|
250
|
+
- unauthenticated connections are terminated after the configurable handshake timeout
|
|
251
|
+
- connections exceeding the per-connection input byte budget before authentication are disconnected
|
|
252
|
+
- an initially unavailable authorized target is retried only before `session.created`, with abort/close returning all resources to baseline
|
|
253
|
+
- target HTTPS rejects a wrong CA or hostname and target mTLS rejects a missing/untrusted client certificate
|
|
254
|
+
- a slow/partial frame (fragmented uint32, incomplete header, incomplete payload) does not accumulate unbounded memory
|
|
255
|
+
- reverse registration rejects wrong principals, duplicate IDs, and stale generations
|
|
256
|
+
- reverse clients reject unknown/unavailable gateway IDs and wrong TLS CAs
|
|
257
|
+
- both hub route policy and the home-local allowlist deny unauthorized Docker targets
|
|
258
|
+
- gateway loss cleans every stream and target without replay, then a fresh stream works after re-registration
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Session Lifecycle
|
|
2
|
+
|
|
3
|
+
## Protocol-v3 connection and stream lifecycle
|
|
4
|
+
|
|
5
|
+
V3 separates the physical state (`connecting -> authenticated -> ready ->
|
|
6
|
+
interrupted -> resuming -> ready`) from each virtual stream (`opening -> open ->
|
|
7
|
+
half-closed -> closed`, with terminal `reset`). One transport interruption moves
|
|
8
|
+
all active streams into the same connection-wide resume attempt; it does not
|
|
9
|
+
spawn per-stream reconnects.
|
|
10
|
+
|
|
11
|
+
## Reverse-gateway lifecycle
|
|
12
|
+
|
|
13
|
+
An outbound gateway moves through `connecting -> authenticated -> registering
|
|
14
|
+
-> ready`. Transport loss destroys every local target stream, advances the
|
|
15
|
+
gateway generation, and reconnects with bounded exponential delay. Selected
|
|
16
|
+
Server3 client members leave readiness when that gateway disappears and rejoin
|
|
17
|
+
only after it registers again.
|
|
18
|
+
|
|
19
|
+
Reverse stream loss is terminal even when registration later recovers. No
|
|
20
|
+
established Docker bytes are persisted or replayed because their outcome may
|
|
21
|
+
already be committed at dockerd. The application receives
|
|
22
|
+
`GATEWAY_DISCONNECTED`, reconciles the operation, and opens a new stream.
|
|
23
|
+
|
|
24
|
+
The bounded, paginated resume manifest carries stable identifiers and both
|
|
25
|
+
directional acknowledgement positions. The relay returns an independent result
|
|
26
|
+
per stream. A stream omitted because it was closed while disconnected is
|
|
27
|
+
cleaned up at the relay. A target lost during interruption returns a stream-local
|
|
28
|
+
resume reset and a typed session-loss error, while retained siblings continue.
|
|
29
|
+
Only bytes above the peer acknowledgement are replayed.
|
|
30
|
+
|
|
31
|
+
## States
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
creating -> active -> interrupted -> resuming -> active
|
|
35
|
+
| |
|
|
36
|
+
v v
|
|
37
|
+
expired closed
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Additional terminal state:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
failed
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Create
|
|
47
|
+
|
|
48
|
+
1. Client connects to relay.
|
|
49
|
+
2. Client authenticates.
|
|
50
|
+
3. Client requests `session.create` with a human-readable `name` and requested `target` `{host, port}`.
|
|
51
|
+
4. Server validates the target through the authenticated user's allowlist policy.
|
|
52
|
+
5. Server creates or accepts a stable opaque `identifier`.
|
|
53
|
+
6. Server associates the session with the authenticated user and target metadata.
|
|
54
|
+
7. The authenticated session remains `creating` while the relay opens its target-side socket.
|
|
55
|
+
8. An initial refusal or per-attempt connect timeout produces no client error or readiness event. The relay retries after approximately 250ms, doubling to a configurable 5s cap while the creating tunnel remains open. Each connect attempt has a separately configurable 10s default bound.
|
|
56
|
+
9. Once target TCP connect succeeds, the relay transitions the session to `active` and returns `session.created` exactly once.
|
|
57
|
+
10. The client emits `connect`; only then can stream data reach the target.
|
|
58
|
+
|
|
59
|
+
The authentication deadline is cleared after successful authentication and authorization, before target acquisition. A tunnel close or client abort before `session.created` immediately deletes the unannounced session and cancels its target attempt and retry timer; it is not resumable.
|
|
60
|
+
|
|
61
|
+
This retry is deliberately narrow. It is safe because no application bytes have been accepted by the relay before `session.created`. Once a target has connected and the session has been announced, target close, uncertain writes, overflow, authorization failures, and protocol failures are terminal. Socketduct never reconnects the established target or automatically replays an application operation.
|
|
62
|
+
|
|
63
|
+
## Normal active streaming
|
|
64
|
+
|
|
65
|
+
- Every data frame has a monotonic sequence.
|
|
66
|
+
- Receiver sends acknowledgements.
|
|
67
|
+
- Sender keeps unacknowledged chunks in memory and/or spool.
|
|
68
|
+
- Acked chunks can be discarded.
|
|
69
|
+
|
|
70
|
+
## Interruption
|
|
71
|
+
|
|
72
|
+
When the edge loses the WAN tunnel:
|
|
73
|
+
|
|
74
|
+
1. Edge keeps the app-facing socket open if configured limits allow.
|
|
75
|
+
2. Edge marks the session `interrupted`.
|
|
76
|
+
3. Edge writes session metadata and unacknowledged chunks to a temporary spool file.
|
|
77
|
+
4. Edge reconnects in the background.
|
|
78
|
+
5. If local buffers fill, edge applies backpressure to the app-facing socket.
|
|
79
|
+
6. If max interruption time or spool size is exceeded, edge closes the app-facing socket and marks the session `failed` or `expired`.
|
|
80
|
+
|
|
81
|
+
## Resume
|
|
82
|
+
|
|
83
|
+
1. Edge reconnects to relay.
|
|
84
|
+
2. Edge authenticates using token or username/password.
|
|
85
|
+
3. Edge sends `session.resume` with the existing `identifier`.
|
|
86
|
+
4. Relay verifies user ownership and session state.
|
|
87
|
+
5. Relay reattaches the new tunnel socket to the existing session and retained target socket when possible.
|
|
88
|
+
6. Relay answers with last known sequence/ack positions.
|
|
89
|
+
7. Edge reads the temporary spool file.
|
|
90
|
+
8. Edge sends spool metadata and unacknowledged frames from the requested sequence.
|
|
91
|
+
9. Relay deduplicates already-seen frames.
|
|
92
|
+
10. Streaming continues.
|
|
93
|
+
11. Edge truncates/deletes acked spool data.
|
|
94
|
+
|
|
95
|
+
## Clean close
|
|
96
|
+
|
|
97
|
+
1. Either side sends `session.close`.
|
|
98
|
+
2. When the relay processes a client-requested logical close it answers with
|
|
99
|
+
`{"type":"session.close","direction":"target-to-client","acknowledged":true}`.
|
|
100
|
+
A target-originated terminal close omits `acknowledged`; this distinction
|
|
101
|
+
prevents an already-in-flight terminal notification from being mistaken for
|
|
102
|
+
confirmation that a later client close reached the relay.
|
|
103
|
+
3. Both sides flush pending stream acknowledgements.
|
|
104
|
+
4. Target/app sockets close.
|
|
105
|
+
5. Spool files are deleted.
|
|
106
|
+
6. Session metadata is marked `closed`.
|
|
107
|
+
|
|
108
|
+
## Expiry
|
|
109
|
+
|
|
110
|
+
A session expires when:
|
|
111
|
+
|
|
112
|
+
- interruption exceeds configured duration
|
|
113
|
+
- spool file exceeds configured age
|
|
114
|
+
- target-side connection closes and cannot be held
|
|
115
|
+
- target hold/resume limits are exceeded
|
|
116
|
+
- user/admin deletes it
|
|
117
|
+
- authentication/token policy invalidates it
|
|
118
|
+
|
|
119
|
+
## Initial defaults to consider
|
|
120
|
+
|
|
121
|
+
- Max interruption hold time: 60 seconds.
|
|
122
|
+
- Max spool size per session: 64 MiB.
|
|
123
|
+
- Max sessions per user: 32.
|
|
124
|
+
- Initial target acquisition: 250ms -> 500ms -> 1s -> 2s -> 4s -> 5s, capped.
|
|
125
|
+
- Session identifier: UUIDv7 or 128-bit random URL-safe string.
|
|
126
|
+
|
|
127
|
+
These are starting points, not final decisions.
|