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/.dockerignore
ADDED
package/.env.example
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copy this file to .env on the remote gateway and fill the values.
|
|
2
|
+
# Keep .env private. Do not commit real tokens.
|
|
3
|
+
|
|
4
|
+
# Required for production gateway deployment. Replace the placeholder with the
|
|
5
|
+
# lowercase 64-character digest published for the intended GHCR image. Tags,
|
|
6
|
+
# including latest, branch names, and commit SHAs, are rejected.
|
|
7
|
+
SOCKETDUCT_IMAGE=ghcr.io/kaspernj/socketduct@sha256:<64-lowercase-hex>
|
|
8
|
+
|
|
9
|
+
SOCKETDUCT_MODE=relay
|
|
10
|
+
SOCKETDUCT_RELAY_HOST=0.0.0.0
|
|
11
|
+
SOCKETDUCT_RELAY_PORT=3100
|
|
12
|
+
# Protocol v3 multiplexes many virtual streams per physical transport. Keep 2
|
|
13
|
+
# during rollback, then change both relay and clients together.
|
|
14
|
+
SOCKETDUCT_PROTOCOL_VERSION=3
|
|
15
|
+
# Host address published by Docker Compose. The safe default is loopback;
|
|
16
|
+
# set this explicitly to a private interface address when remote edges need it.
|
|
17
|
+
SOCKETDUCT_PUBLISH_HOST=127.0.0.1
|
|
18
|
+
SOCKETDUCT_RESUME_WINDOW_MS=30000
|
|
19
|
+
# Emit secret-free JSON runtime counters at this interval. Set 0 to disable.
|
|
20
|
+
SOCKETDUCT_STATUS_INTERVAL_MS=30000
|
|
21
|
+
|
|
22
|
+
# Optional bounded persistent JSONL logs. Bind mount this directory from host
|
|
23
|
+
# storage in Docker. Files are isolated by role/PID; defaults are 10 MiB and 5
|
|
24
|
+
# retained files including the active file. Existing stdout/stderr is preserved.
|
|
25
|
+
# Host directory mounted by the checked-in production Compose files. Create it
|
|
26
|
+
# for uid/gid 1000 before deployment. Logging stays disabled unless the
|
|
27
|
+
# container-side SOCKETDUCT_LOG_DIRECTORY is also set.
|
|
28
|
+
# SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
|
|
29
|
+
# SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
|
|
30
|
+
# SOCKETDUCT_LOG_MAX_FILE_BYTES=10485760
|
|
31
|
+
# SOCKETDUCT_LOG_MAX_FILES=5
|
|
32
|
+
|
|
33
|
+
# Per-session hard byte limit for target-to-client data buffered while the
|
|
34
|
+
# edge tunnel is detached. When the limit is exceeded the relay terminates
|
|
35
|
+
# the target connection and the edge receives a terminal protocol error.
|
|
36
|
+
# Default (when omitted): 1048576 (1 MiB).
|
|
37
|
+
# SOCKETDUCT_MAX_TARGET_BUFFER_BYTES=1048576
|
|
38
|
+
|
|
39
|
+
# Process-wide byte limit for target-to-client buffered data across all
|
|
40
|
+
# sessions combined. When this aggregate limit is exceeded the relay
|
|
41
|
+
# terminates the session whose target data just crossed the limit and the
|
|
42
|
+
# edge receives a terminal protocol error.
|
|
43
|
+
# Default (when omitted): 16777216 (16 MiB).
|
|
44
|
+
# SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES=16777216
|
|
45
|
+
|
|
46
|
+
# Initial target connection retry starts at this delay and doubles until the
|
|
47
|
+
# maximum. Retry exists only before session.created; established operations are
|
|
48
|
+
# never replayed against a replacement target. Defaults: 250 and 5000.
|
|
49
|
+
# SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS=250
|
|
50
|
+
# SOCKETDUCT_INITIAL_TARGET_RETRY_MAX_DELAY_MS=5000
|
|
51
|
+
|
|
52
|
+
# Milliseconds to wait for authentication after a TCP/TLS connection is
|
|
53
|
+
# established. If no session.create or session.resume arrives within this
|
|
54
|
+
# window, the relay closes the connection with a HANDSHAKE_TIMEOUT error.
|
|
55
|
+
# Default (when omitted): 10000 (10 seconds).
|
|
56
|
+
# SOCKETDUCT_HANDSHAKE_TIMEOUT_MS=10000
|
|
57
|
+
|
|
58
|
+
# Per-connection input byte budget before the first authenticated session
|
|
59
|
+
# frame. If a connection sends more raw bytes than this limit before
|
|
60
|
+
# completing authentication, the relay terminates the connection.
|
|
61
|
+
# Default (when omitted): 65536 (64 KiB).
|
|
62
|
+
# SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION=65536
|
|
63
|
+
|
|
64
|
+
# Comma-separated username:token entries. Generate a token with:
|
|
65
|
+
# openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n'
|
|
66
|
+
# The placeholder below is intentionally rejected by the runtime.
|
|
67
|
+
SOCKETDUCT_AUTH_TOKENS=tensorbuzz-edge:replace-with-generated-token
|
|
68
|
+
|
|
69
|
+
# Comma-separated host:port entries the authenticated edge may request.
|
|
70
|
+
# For Docker-host gateway mode, enable the docker-host Compose profile and
|
|
71
|
+
# keep this value pointed at the private shim service; it is not published
|
|
72
|
+
# on the host network.
|
|
73
|
+
SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375
|
|
74
|
+
|
|
75
|
+
# --- TLS (atomic pair; both must be set or neither) ---
|
|
76
|
+
# Path to PEM-encoded certificate file for the relay listener.
|
|
77
|
+
# SOCKETDUCT_TLS_CERT_FILE=/certs/relay.crt
|
|
78
|
+
# Path to PEM-encoded private key file for the relay listener.
|
|
79
|
+
# SOCKETDUCT_TLS_KEY_FILE=/certs/relay.key
|
|
80
|
+
|
|
81
|
+
# --- Optional mTLS ---
|
|
82
|
+
# Path to PEM-encoded client CA certificate file.
|
|
83
|
+
# When set, the relay requires clients to present a valid certificate.
|
|
84
|
+
# SOCKETDUCT_TLS_CLIENT_CA_FILE=/certs/client-ca.crt
|
|
85
|
+
|
|
86
|
+
# --- Insecure public opt-out ---
|
|
87
|
+
# Set to 1 or true only when you intentionally bind to a non-loopback
|
|
88
|
+
# address without TLS. Only accepts 1 or true.
|
|
89
|
+
# SOCKETDUCT_ALLOW_INSECURE_PUBLIC=1
|
|
90
|
+
|
|
91
|
+
# --- stream.data compression ---
|
|
92
|
+
# Whether the relay negotiates compression of tunnelled stream data.
|
|
93
|
+
# "auto" (default) offers zstd when the Node runtime supports it (Node >= 22.15;
|
|
94
|
+
# older runtimes silently fall back to raw framing). "off" disables it entirely,
|
|
95
|
+
# so every tunnel uses the raw legacy format. Unknown values are rejected.
|
|
96
|
+
# Control, session and auth frames are never compressed.
|
|
97
|
+
# SOCKETDUCT_COMPRESSION=auto
|
|
98
|
+
|
|
99
|
+
# Payloads smaller than this are sent raw, because compressing them costs more
|
|
100
|
+
# CPU than it saves bytes. Default 256.
|
|
101
|
+
# SOCKETDUCT_MIN_COMPRESS_BYTES=256
|
|
102
|
+
|
|
103
|
+
# Hard cap on the decompressed size of a single stream.data frame. A frame that
|
|
104
|
+
# advertises more than this is rejected as a protocol violation. Default 16 MiB.
|
|
105
|
+
# SOCKETDUCT_MAX_DECOMPRESSED_BYTES=16777216
|
|
106
|
+
|
|
107
|
+
# Hard cap on the decompressed/compressed expansion ratio of a single frame,
|
|
108
|
+
# bounding decompression-bomb amplification. Raise this only if a legitimate,
|
|
109
|
+
# highly repetitive payload is being rejected. Default 1000.
|
|
110
|
+
# SOCKETDUCT_MAX_EXPANSION_RATIO=1000
|
|
111
|
+
|
|
112
|
+
# --- Native reverse gateway roles ---
|
|
113
|
+
# Server3 hub: change SOCKETDUCT_MODE to reverse-hub, keep the TLS server pair
|
|
114
|
+
# above, and bind every stable gateway ID to its credential username.
|
|
115
|
+
# SOCKETDUCT_REVERSE_GATEWAY_BINDINGS=o2:home-o2,zyxel:home-zyxel,starlink:home-starlink
|
|
116
|
+
# Authorize exact client username, gateway ID, and home-local target tuples.
|
|
117
|
+
# SOCKETDUCT_REVERSE_ROUTES=tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375,tensorbuzz:starlink:docker-server:2375
|
|
118
|
+
|
|
119
|
+
# Home member: change SOCKETDUCT_MODE to reverse-gateway. Run one process in
|
|
120
|
+
# each router-pinned namespace and use distinct stable identity/credential data.
|
|
121
|
+
# SOCKETDUCT_REVERSE_GATEWAY_ID=o2
|
|
122
|
+
# SOCKETDUCT_REVERSE_CONNECTION_ID=home_o2_gateway_01
|
|
123
|
+
# SOCKETDUCT_REVERSE_HUB_HOST=server3.example.net
|
|
124
|
+
# SOCKETDUCT_REVERSE_HUB_PORT=3100
|
|
125
|
+
# SOCKETDUCT_REVERSE_TOKEN=<secret-manager-value>
|
|
126
|
+
# SOCKETDUCT_REVERSE_HUB_CA_FILE=/certs/hub-ca.crt
|
|
127
|
+
# SOCKETDUCT_REVERSE_HUB_SERVERNAME=server3.example.net
|
|
128
|
+
# Optional outbound mTLS atomic pair:
|
|
129
|
+
# SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE=/certs/gateway.crt
|
|
130
|
+
# SOCKETDUCT_REVERSE_HUB_CLIENT_KEY_FILE=/certs/gateway.key
|
|
131
|
+
# Keep SOCKETDUCT_TARGET_ALLOWLIST=docker-server:2375 and do not publish it.
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
**Socketduct** is a resilient, authenticated tunnel/proxy written in Node.js.
|
|
8
|
+
|
|
9
|
+
The goal is to let applications connect to a stable local/LAN endpoint while Socketduct handles unreliable upstream internet links. When the WAN drops for a few seconds, the app-facing socket should stay open where possible. Socketduct reconnects to the remote server, resumes the named session by identifier, replays buffered state/data, and continues the stream.
|
|
10
|
+
|
|
11
|
+
Primary scenario:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Application on stable LAN machine
|
|
15
|
+
-> Socketduct local proxy/edge on same LAN
|
|
16
|
+
-> unreliable internet
|
|
17
|
+
-> Socketduct relay/server
|
|
18
|
+
-> target service / internet
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The application and the local Socketduct edge are assumed to be on a stable LAN. The unreliable portion is between the edge and the remote Socketduct relay.
|
|
22
|
+
|
|
23
|
+
## Non-negotiable requirements
|
|
24
|
+
|
|
25
|
+
- Use **Node.js**.
|
|
26
|
+
- Use multiple CPU cores for connection/session work.
|
|
27
|
+
- Require authentication before any session operation.
|
|
28
|
+
- Support users with passwords or password-derived credentials.
|
|
29
|
+
- Never behave as an unauthenticated open proxy.
|
|
30
|
+
- Sessions have both:
|
|
31
|
+
- human-readable `name`
|
|
32
|
+
- stable opaque `identifier`
|
|
33
|
+
- A client can create a new session or resume an existing session by `identifier`.
|
|
34
|
+
- On connection loss, persist resumable session state to temporary spool files.
|
|
35
|
+
- On resume, send/upload the relevant spool file/state to the server before continuing.
|
|
36
|
+
- Favor explicit protocol docs and tests over clever implicit behavior.
|
|
37
|
+
|
|
38
|
+
## Preferred architecture
|
|
39
|
+
|
|
40
|
+
Use separate process roles:
|
|
41
|
+
|
|
42
|
+
- **Control process**: config, auth store, lifecycle, worker supervision.
|
|
43
|
+
- **Worker processes**: connection handling, session streams, replay/spooling.
|
|
44
|
+
- **Optional admin/API process**: user management, health, metrics.
|
|
45
|
+
|
|
46
|
+
For multi-core Node.js, prefer one of these, in order:
|
|
47
|
+
|
|
48
|
+
1. `node:cluster` for distributing incoming TCP/proxy connections across CPU cores.
|
|
49
|
+
2. `node:worker_threads` for CPU-heavy work, if any appears later.
|
|
50
|
+
3. `node:child_process` only when isolation or external commands are needed.
|
|
51
|
+
|
|
52
|
+
The initial server should probably use `node:cluster` because socket/concurrency work is mostly I/O-bound and cluster maps naturally to per-core workers.
|
|
53
|
+
|
|
54
|
+
If looking for inspiration, inspect Velocious' HTTP server/client patterns, but do not assume Velocious is the right dependency. Socketduct should remain a small standalone networking tool unless there is a strong reason otherwise.
|
|
55
|
+
|
|
56
|
+
## Security model
|
|
57
|
+
|
|
58
|
+
- Auth happens before `session.create`, `session.resume`, or proxy connect.
|
|
59
|
+
- Store passwords as Argon2id hashes if practical; otherwise bcrypt/scrypt with sane parameters.
|
|
60
|
+
- Prefer session tokens after login instead of sending username/password on every reconnect.
|
|
61
|
+
- Bind tokens to user + session identifier where possible.
|
|
62
|
+
- Enforce per-user session ownership.
|
|
63
|
+
- Add destination allowlists before exposing general proxying.
|
|
64
|
+
- Default listen address should be local/LAN-safe, not public internet.
|
|
65
|
+
- Never log raw passwords, session tokens, or target credentials.
|
|
66
|
+
|
|
67
|
+
## Session model
|
|
68
|
+
|
|
69
|
+
A session represents one resumable tunnel/proxy stream.
|
|
70
|
+
|
|
71
|
+
Minimum fields:
|
|
72
|
+
|
|
73
|
+
- `identifier`: opaque stable id, client-generated or server-generated.
|
|
74
|
+
- `name`: human-friendly label.
|
|
75
|
+
- `userId`: authenticated owner.
|
|
76
|
+
- `state`: `creating`, `active`, `interrupted`, `resuming`, `closed`, `expired`.
|
|
77
|
+
- `createdAt`, `updatedAt`, `lastSeenAt`.
|
|
78
|
+
- `localSequence`: highest byte/chunk accepted from local app.
|
|
79
|
+
- `remoteSequence`: highest byte/chunk acknowledged by remote relay.
|
|
80
|
+
- `spoolPath`: path to temporary persisted resume data, if interrupted.
|
|
81
|
+
|
|
82
|
+
## Spooling/resume behavior
|
|
83
|
+
|
|
84
|
+
When a tunnel connection drops:
|
|
85
|
+
|
|
86
|
+
1. Keep the app-facing socket open if buffer/time limits allow.
|
|
87
|
+
2. Mark the session as `interrupted`.
|
|
88
|
+
3. Persist unacknowledged stream chunks and metadata into a temp spool file.
|
|
89
|
+
4. Attempt reconnect with exponential backoff.
|
|
90
|
+
5. Authenticate or refresh the reconnect token.
|
|
91
|
+
6. Resume by `identifier`.
|
|
92
|
+
7. Send the spool metadata/data.
|
|
93
|
+
8. Continue from the server-confirmed sequence position.
|
|
94
|
+
|
|
95
|
+
Spool files should be bounded:
|
|
96
|
+
|
|
97
|
+
- maximum bytes per session
|
|
98
|
+
- maximum age
|
|
99
|
+
- cleanup on successful close/expiry
|
|
100
|
+
- crash recovery scan on process start
|
|
101
|
+
|
|
102
|
+
## Protocol direction
|
|
103
|
+
|
|
104
|
+
Prefer a framed protocol over raw ad-hoc JSON lines.
|
|
105
|
+
|
|
106
|
+
Every frame should include:
|
|
107
|
+
|
|
108
|
+
- protocol version
|
|
109
|
+
- message type
|
|
110
|
+
- session identifier where relevant
|
|
111
|
+
- monotonically increasing sequence number where relevant
|
|
112
|
+
- payload length
|
|
113
|
+
- checksum or authentication tag if needed
|
|
114
|
+
|
|
115
|
+
Initial message types:
|
|
116
|
+
|
|
117
|
+
- `auth.login`
|
|
118
|
+
- `auth.ok`
|
|
119
|
+
- `auth.error`
|
|
120
|
+
- `session.create`
|
|
121
|
+
- `session.created`
|
|
122
|
+
- `session.resume`
|
|
123
|
+
- `session.resumed`
|
|
124
|
+
- `stream.data`
|
|
125
|
+
- `stream.ack`
|
|
126
|
+
- `stream.pause`
|
|
127
|
+
- `stream.resume`
|
|
128
|
+
- `session.close`
|
|
129
|
+
- `error`
|
|
130
|
+
|
|
131
|
+
## Development style
|
|
132
|
+
|
|
133
|
+
- Keep modules small and testable.
|
|
134
|
+
- Write tests before or alongside protocol/session logic.
|
|
135
|
+
- Keep stream replay logic deterministic and unit-tested.
|
|
136
|
+
- Fail loudly on protocol violations.
|
|
137
|
+
- Avoid silently dropping data unless the session has explicitly exceeded configured limits.
|
|
138
|
+
- Prefer TypeScript if/when the implementation begins, but plain modern Node.js is acceptable if that keeps the first prototype simpler.
|
|
139
|
+
|
|
140
|
+
## Suggested repository structure
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
src/
|
|
144
|
+
cli/
|
|
145
|
+
config/
|
|
146
|
+
auth/
|
|
147
|
+
protocol/
|
|
148
|
+
sessions/
|
|
149
|
+
spool/
|
|
150
|
+
server/
|
|
151
|
+
edge/
|
|
152
|
+
workers/
|
|
153
|
+
proxy/
|
|
154
|
+
test/
|
|
155
|
+
unit/
|
|
156
|
+
integration/
|
|
157
|
+
docs/
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Definition of done for implementation PRs
|
|
161
|
+
|
|
162
|
+
- Add a release-note fragment for every user-visible change, bug fix, security fix, or protocol change under `changelog.d/YYYYMMDDHHMMSS-short-slug.md`. Keep it to one concise, user-facing sentence; do not edit generated release notes directly.
|
|
163
|
+
- Tests added or updated.
|
|
164
|
+
- Relevant docs updated.
|
|
165
|
+
- `npm test` passes.
|
|
166
|
+
- Lint/typecheck passes once configured.
|
|
167
|
+
- Security-sensitive changes mention how credentials/tokens are protected.
|
|
168
|
+
- No open proxy behavior introduced.
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
FROM node:24-bookworm-slim@sha256:3638d9a6fe4030bd716be989438248074489337ba3275657f93595428be4fc03 AS base
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
FROM base AS development
|
|
6
|
+
|
|
7
|
+
ENV NODE_ENV=development
|
|
8
|
+
|
|
9
|
+
COPY package.json package-lock.json ./
|
|
10
|
+
RUN npm ci && npm cache clean --force
|
|
11
|
+
|
|
12
|
+
COPY --chown=node:node . .
|
|
13
|
+
|
|
14
|
+
USER node
|
|
15
|
+
CMD ["npm", "test"]
|
|
16
|
+
|
|
17
|
+
FROM base AS runtime
|
|
18
|
+
|
|
19
|
+
ENV NODE_ENV=production
|
|
20
|
+
|
|
21
|
+
COPY package.json package-lock.json ./
|
|
22
|
+
RUN npm ci --omit=dev && npm cache clean --force
|
|
23
|
+
|
|
24
|
+
COPY src ./src
|
|
25
|
+
|
|
26
|
+
RUN mkdir -p /certs && chown node:node /certs
|
|
27
|
+
|
|
28
|
+
USER node
|
|
29
|
+
EXPOSE 3100
|
|
30
|
+
CMD ["node", "src/index.js"]
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Socketduct
|
|
2
|
+
|
|
3
|
+
See the [wire protocol](docs/protocol.md) for multiplexed v3, rollback-compatible
|
|
4
|
+
compact framing v2, replay, flow control, and scheduling semantics.
|
|
5
|
+
|
|
6
|
+
Resilient authenticated tunnel/proxy for keeping socket streams alive across flaky links.
|
|
7
|
+
|
|
8
|
+
Socketduct is intended for setups where the application machine and a local Socketduct edge are on a stable LAN, but the edge's internet connection occasionally drops for a few seconds. Applications connect to the local edge; Socketduct reconnects and resumes the upstream tunnel when the WAN returns.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
App / client software
|
|
12
|
+
-> local Socketduct edge on stable LAN
|
|
13
|
+
-> flaky internet
|
|
14
|
+
-> Socketduct relay/server
|
|
15
|
+
-> target service
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Core idea
|
|
19
|
+
|
|
20
|
+
Normal TCP sockets cannot magically resume after the underlying internet path dies. Socketduct works by owning both sides of the unstable leg:
|
|
21
|
+
|
|
22
|
+
- the local edge keeps the application-facing socket alive
|
|
23
|
+
- the remote relay validates the requested target per session and keeps the target-facing side alive across brief edge disconnects when possible
|
|
24
|
+
- the edge and relay exchange framed stream data with sequence numbers and acknowledgements
|
|
25
|
+
- stream data payloads are compressed with zstd when both peers negotiate it, falling back to raw framing otherwise; sequences, acknowledgements and limits stay in uncompressed bytes (see [Protocol sketch](docs/protocol.md))
|
|
26
|
+
- on interruption, unacknowledged session data is persisted to a temporary spool file
|
|
27
|
+
- on reconnect, the client authenticates, resumes the session by identifier, sends the spool state/data, and continues from the last acknowledged sequence
|
|
28
|
+
- initial target acquisition is retried with bounded exponential backoff before readiness; established targets and application operations are never automatically replayed
|
|
29
|
+
- dedicated `socketduct/http` and `socketduct/https` agents support HTTP and strict target HTTPS/mTLS without a local listener
|
|
30
|
+
- protocol v3 exposes `MultiplexedSocketductConnection`: one authenticated
|
|
31
|
+
physical TCP/TLS transport carries many normal `VirtualSocket` Duplex streams,
|
|
32
|
+
with fair flow control and one shared reconnect/resume owner
|
|
33
|
+
- protocol v3 also exposes `MultiplexedSocketductTransportSet`: one explicit
|
|
34
|
+
warm member per gateway path, bounded readiness waiting for app-facing
|
|
35
|
+
sockets, and deterministic per-socket round-robin across ready members
|
|
36
|
+
- native reverse-gateway v3 lets authenticated private home members register
|
|
37
|
+
outbound with a Server3 hub, so CGNAT paths need no inbound forwarding
|
|
38
|
+
- optional bounded JSONL lifecycle logs can be written to per-process files in
|
|
39
|
+
a Docker bind-mounted directory without replacing existing stdout/stderr
|
|
40
|
+
|
|
41
|
+
## MVP goals
|
|
42
|
+
|
|
43
|
+
- Node.js implementation.
|
|
44
|
+
- Multi-core server using Node's cluster/worker model.
|
|
45
|
+
- Username/password authentication before session operations.
|
|
46
|
+
- Session creation with a human-readable name and stable opaque identifier.
|
|
47
|
+
- Session-scoped target selection through an explicit allowlist policy.
|
|
48
|
+
- Session resume by identifier.
|
|
49
|
+
- Temporary spool files for interrupted sessions.
|
|
50
|
+
- Bounded buffering and cleanup.
|
|
51
|
+
- No unauthenticated open proxy behavior.
|
|
52
|
+
|
|
53
|
+
## Multiplexed v3 API
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import {MultiplexedSocketductConnection} from "socketduct/multiplexed"
|
|
57
|
+
|
|
58
|
+
const transport = new MultiplexedSocketductConnection({
|
|
59
|
+
relay: {host: "relay.example.net", port: 3100, token: process.env.SOCKETDUCT_TOKEN}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const socket = transport.createConnection({host: "service.internal", port: 443})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Pass the pre-bound `transport.createConnection` directly to generic Node clients.
|
|
66
|
+
Every returned `VirtualSocket` is an independent `Duplex`; closing one does not
|
|
67
|
+
close the physical transport. Legacy v2 APIs and HTTP agents remain available
|
|
68
|
+
for rollback.
|
|
69
|
+
|
|
70
|
+
For process-scoped path diversity, configure a transport set with stable gateway
|
|
71
|
+
IDs and distinct gateway-pinned relay endpoints or physical socket factories:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import {MultiplexedSocketductTransportSet} from "socketduct/multiplexed"
|
|
75
|
+
|
|
76
|
+
const transports = new MultiplexedSocketductTransportSet({
|
|
77
|
+
connectionId: "build_runner_01",
|
|
78
|
+
gateways: [
|
|
79
|
+
{gatewayId: "starlink", endpoint: {relay: starlinkRelay}},
|
|
80
|
+
{gatewayId: "zyxel", endpoint: {relay: zyxelRelay}},
|
|
81
|
+
{gatewayId: "o2gateway", endpoint: {relay: o2Relay}}
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
const starting = transports.start()
|
|
85
|
+
const socket = transports.createConnection({host: "service.internal", port: 443})
|
|
86
|
+
await starting
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The default minimum readiness is one. Other configured paths continue warming
|
|
90
|
+
and automatically rejoin selection. A socket created before readiness is
|
|
91
|
+
returned immediately and waits inside Socketduct with normal callback, event,
|
|
92
|
+
timeout and backpressure behavior; assignment stays FIFO and occurs only on a
|
|
93
|
+
ready member. A virtual socket never changes members.
|
|
94
|
+
Durable process-replacement recovery uses `recoverConnection()` with the
|
|
95
|
+
original gateway ID before the replacement set starts.
|
|
96
|
+
|
|
97
|
+
For home Docker hosts behind O2, Zyxel, and Starlink, run
|
|
98
|
+
`OutboundReverseGateway` in each router-pinned namespace and
|
|
99
|
+
`startReverseGatewayHub` on Server3. TensorBuzz can keep this transport-set API
|
|
100
|
+
by configuring each member with `endpoint.reverseHub`; all three may share one
|
|
101
|
+
hub address because `gatewayId` selects the outbound member. Docker remains
|
|
102
|
+
`docker-server:2375` on a private Compose network and is never published. See
|
|
103
|
+
[Native reverse gateway](docs/reverse-gateway.md).
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
- [Architecture](docs/architecture.md)
|
|
108
|
+
- [Protocol sketch](docs/protocol.md)
|
|
109
|
+
- [Session lifecycle](docs/session-lifecycle.md)
|
|
110
|
+
- [Security model](docs/security.md)
|
|
111
|
+
- [Running Socketduct](docs/running.md)
|
|
112
|
+
- [Docker gateway setup](docs/docker.md)
|
|
113
|
+
- [Docker topology](docs/docker-topology.md)
|
|
114
|
+
- [Node-compatible socket API design](docs/socket-api.md)
|
|
115
|
+
- [Native reverse gateway](docs/reverse-gateway.md)
|
|
116
|
+
- [Persistent structured logging](docs/logging.md)
|
|
117
|
+
- [Initial implementation plan](docs/plans/initial-implementation.md)
|
|
118
|
+
- [Socket + Docker integration plan](docs/plans/socket-docker-integration.md)
|
|
119
|
+
|
|
120
|
+
## Contributing
|
|
121
|
+
|
|
122
|
+
Every user-visible change, bug fix, security fix, and protocol change must include a concise release-note fragment in `changelog.d/YYYYMMDDHHMMSS-short-slug.md`. Release tooling consumes these fragments; do not edit generated release notes directly.
|
|
123
|
+
|
|
124
|
+
TensorBuzz is the sole CI and private-container publication workflow owner for
|
|
125
|
+
this repository; `tensorbuzz.yml` defines dependency installation, tests,
|
|
126
|
+
typechecking, linting, and a real Dockerfile build against an isolated Docker
|
|
127
|
+
service. Pull-request builds never publish images. Private GHCR publication
|
|
128
|
+
remains disabled until TensorBuzz supplies trusted default-branch/tag/manual
|
|
129
|
+
trigger metadata and the Socketduct project has an established secret-backed
|
|
130
|
+
private-registry write credential input.
|
|
131
|
+
|
|
132
|
+
## Status
|
|
133
|
+
|
|
134
|
+
Initial MVP skeleton. The project includes authenticated session create/resume control messages with per-session targets, bounded initial target recovery, a cluster-based server skeleton, local TCP edge/relay forwarding, target-side socket retention across relay reconnects, spool-backed replay, multiplexed multi-gateway transport sets, HTTP and target HTTPS/mTLS adapters, and tests for those flows.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Require compact framing v2 for Socketduct application data and reject legacy length-prefixed stream records, including envelopes that spoof the compact marker.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Relay connection and TLS-handshake timeouts now destroy their owned sockets and ignore late readiness events before reconnecting.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now preserves structured relay errors, reports failed HTTP-agent connects correctly, emits secret-free relay health counters, and ships a Node 24 container that is loopback-published and privilege-hardened by default.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now waits and retries for initial target readiness and provides a first-class HTTPS/mTLS agent without replaying established operations.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now completes intentional HTTP session closes through TLS relays without retaining detached relay sessions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now closes interrupted sessions promptly and retries silently hanging initial target connections without delivering data after stream EOF.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct protocol v3 adds many fair, resumable virtual TCP sockets over one authenticated physical transport while retaining protocol v2 for rollback.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Protocol v3 now safely acknowledges terminal closes, preserves unread replay across replacement recovery, expires abandoned spools, enforces configured pre-authentication input limits, and resumes in-flight stream opens without duplication.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fixed socket idle timeouts firing after newer activity reset the timer.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Add a process-scoped v3 transport set that warms explicit gateways, fairly pins new virtual sockets, exposes each socket's stable gateway ID for recovery, and enforces shared memory and durable-spool limits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fixed relay interruption recovery so queued frames settle before a dropped tunnel is reported closed.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Production Docker gateways now require a digest-pinned Socketduct GHCR image and deploy through a selector-isolated, rendered-model-validated pull-only workflow.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now provides an authenticated TLS-and-zstd native reverse gateway for multiplexed access to deny-by-default private targets.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fix relay resume ordering so final target data is delivered before the target close without resetting wrapped TLS connections.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Multiplexed streams now coordinate durable spool setup and connection ownership so slow or cancelled opens do not stall later connections.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Multiplexed clients now suppress delayed acknowledgements after a relay CLOSE so later streams keep the shared connection.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse clients now close early-response streams at the transmitted request position without disrupting sibling streams.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Repeated reverse-client connection attempts now return the preserved authentication, selection, or TLS terminal error promptly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse gateway streams now preserve DATA/END ordering, bound pre-TLS clients, and reclaim disconnected client identities safely.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse gateways now preserve target DATA before clean termination and respect configured decompression limits in both directions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse peers now reconnect after graceful hub restarts and honor smaller advertised receive windows.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse gateway streams now preserve target data before terminal errors and release client capacity after terminal close.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Multi-gateway socket factories now return bounded pending sockets while gateways start or reconnect, then assign each open fairly when an authenticated member is ready.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Deferred multi-gateway sockets now snapshot open options and correctly buffer every Node.js TypedArray and DataView payload while awaiting readiness.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TensorBuzz now exclusively owns repository verification and private-container build validation, with pull-request publication disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Post-readiness transport-set failure now preserves ordered member causes behind a stable error code and exposes only privacy-safe cause codes in status.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Contain reverse-client authentication write races inside the reconnecting transport instead of terminating the host process with an unhandled rejection.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse hub stream closure no longer disconnects sibling streams when flow-control messages arrive after a gateway-local close.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct can now write bounded, privacy-safe JSONL lifecycle logs to per-process files in a configured persistent directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Preserve incomplete JSONL log segments through bounded rotation and pause unsafe file writes without interrupting tunnel traffic.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Correct the Docker log-directory setup command to create private mode-0700 directories accepted by the file logger.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse gateway clients now backpressure large individual writes without exceeding configured stream or connection buffer limits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct JSONL diagnostics now retain error names, messages, and stacks for actionable transport and virtual-stream failure investigation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Transport-set lifecycle diagnostics now redact arbitrary member error text while retaining normalized error classifications.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
services:
|
|
2
|
+
socketduct-dev:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
target: development
|
|
6
|
+
image: socketduct-dev:local
|
|
7
|
+
init: true
|
|
8
|
+
user: "1000:1000"
|
|
9
|
+
read_only: true
|
|
10
|
+
cap_drop:
|
|
11
|
+
- ALL
|
|
12
|
+
security_opt:
|
|
13
|
+
- no-new-privileges:true
|
|
14
|
+
pids_limit: 256
|
|
15
|
+
tmpfs:
|
|
16
|
+
- /tmp:rw,noexec,nosuid,nodev,size=256m
|
|
17
|
+
command:
|
|
18
|
+
- npm
|
|
19
|
+
- test
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
services:
|
|
2
|
+
socketduct-reverse-gateway:
|
|
3
|
+
image: ${SOCKETDUCT_IMAGE:?Set a digest-pinned Socketduct image}
|
|
4
|
+
restart: unless-stopped
|
|
5
|
+
init: true
|
|
6
|
+
user: "1000:1000"
|
|
7
|
+
read_only: true
|
|
8
|
+
cap_drop:
|
|
9
|
+
- ALL
|
|
10
|
+
security_opt:
|
|
11
|
+
- no-new-privileges:true
|
|
12
|
+
pids_limit: 256
|
|
13
|
+
stop_grace_period: 45s
|
|
14
|
+
tmpfs:
|
|
15
|
+
- /tmp:rw,noexec,nosuid,nodev,size=64m
|
|
16
|
+
env_file:
|
|
17
|
+
- .env
|
|
18
|
+
environment:
|
|
19
|
+
SOCKETDUCT_MODE: reverse-gateway
|
|
20
|
+
SOCKETDUCT_TARGET_ALLOWLIST: docker-server:2375
|
|
21
|
+
networks:
|
|
22
|
+
wan-egress:
|
|
23
|
+
gw_priority: 1
|
|
24
|
+
docker-control:
|
|
25
|
+
gw_priority: 0
|
|
26
|
+
volumes:
|
|
27
|
+
- ${SOCKETDUCT_REVERSE_CERT_DIR:-./certs}:/certs:ro
|
|
28
|
+
- ${SOCKETDUCT_LOG_HOST_DIRECTORY:-./logs}:/var/log/socketduct
|
|
29
|
+
|
|
30
|
+
networks:
|
|
31
|
+
wan-egress:
|
|
32
|
+
name: ${SOCKETDUCT_WAN_NETWORK:?Set the preconfigured router-pinned Docker network}
|
|
33
|
+
external: true
|
|
34
|
+
docker-control:
|
|
35
|
+
name: ${SOCKETDUCT_DOCKER_CONTROL_NETWORK:?Set the private network shared with docker-server}
|
|
36
|
+
external: true
|