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.
Files changed (138) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +131 -0
  3. package/AGENTS.md +168 -0
  4. package/Dockerfile +30 -0
  5. package/README.md +134 -0
  6. package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
  7. package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
  8. package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
  9. package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
  10. package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
  11. package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
  12. package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
  13. package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
  14. package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
  15. package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
  16. package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
  17. package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
  18. package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
  19. package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
  20. package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
  21. package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
  22. package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
  23. package/changelog.d/20260902231542-reverse-close-position.md +1 -0
  24. package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
  25. package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
  26. package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
  27. package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
  28. package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
  29. package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
  30. package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
  31. package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
  32. package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
  33. package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
  34. package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
  35. package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
  36. package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
  37. package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
  38. package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
  39. package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
  40. package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
  41. package/docker-compose.dev.yml +19 -0
  42. package/docker-compose.reverse-gateway.yml +36 -0
  43. package/docker-compose.yml +73 -0
  44. package/docs/architecture.md +264 -0
  45. package/docs/docker-topology.md +164 -0
  46. package/docs/docker.md +281 -0
  47. package/docs/logging.md +185 -0
  48. package/docs/plans/initial-implementation.md +205 -0
  49. package/docs/plans/socket-docker-integration.md +221 -0
  50. package/docs/protocol.md +558 -0
  51. package/docs/releasing.md +18 -0
  52. package/docs/reverse-gateway.md +186 -0
  53. package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
  54. package/docs/running.md +215 -0
  55. package/docs/security.md +258 -0
  56. package/docs/session-lifecycle.md +127 -0
  57. package/docs/socket-api.md +448 -0
  58. package/eslint.config.js +36 -0
  59. package/package.json +44 -0
  60. package/scripts/deploy-gateway.js +194 -0
  61. package/scripts/test.js +28 -0
  62. package/src/auth/passwords.js +38 -0
  63. package/src/auth/static-token-store.js +41 -0
  64. package/src/auth/user-store.js +139 -0
  65. package/src/client/connect.js +76 -0
  66. package/src/client/deferred-virtual-socket.js +312 -0
  67. package/src/client/http-agent.js +101 -0
  68. package/src/client/https-agent.js +366 -0
  69. package/src/client/multiplexed-connection.js +1504 -0
  70. package/src/client/multiplexed-transport-set.js +917 -0
  71. package/src/client/multiplexed.js +3 -0
  72. package/src/client/socketduct-socket.js +1174 -0
  73. package/src/client/virtual-socket.js +256 -0
  74. package/src/config/environment.js +400 -0
  75. package/src/edge/tcp-edge.js +82 -0
  76. package/src/index.js +101 -0
  77. package/src/logging/structured-logger.js +745 -0
  78. package/src/protocol/compression.js +325 -0
  79. package/src/protocol/frame.js +341 -0
  80. package/src/protocol/v3-compression.js +44 -0
  81. package/src/protocol/v3-frame.js +209 -0
  82. package/src/proxy/multiplexed-relay.js +798 -0
  83. package/src/proxy/tcp-relay.js +1191 -0
  84. package/src/relay.js +3 -0
  85. package/src/reverse/client.js +721 -0
  86. package/src/reverse/common.js +88 -0
  87. package/src/reverse/gateway.js +663 -0
  88. package/src/reverse/hub.js +563 -0
  89. package/src/reverse/index.js +3 -0
  90. package/src/reverse/runtime.js +78 -0
  91. package/src/server/control-handler.js +170 -0
  92. package/src/server/primary.js +64 -0
  93. package/src/server/relay-runtime.js +124 -0
  94. package/src/server/worker.js +39 -0
  95. package/src/sessions/session.js +212 -0
  96. package/src/spool/spool-file.js +451 -0
  97. package/tensorbuzz.yml +36 -0
  98. package/test/e2e/gateway-process.test.js +262 -0
  99. package/test/e2e/reverse-gateway-process.test.js +285 -0
  100. package/test/fixtures/logging/enospc-matrix.mjs +238 -0
  101. package/test/fixtures/logging/enospc-recovery.mjs +146 -0
  102. package/test/fixtures/logging/process-writer.mjs +20 -0
  103. package/test/helpers/tls-certificates.js +115 -0
  104. package/test/integration/initial-target-recovery.test.js +518 -0
  105. package/test/integration/logging-recovery.test.js +163 -0
  106. package/test/integration/multiplexed-compatibility.test.js +254 -0
  107. package/test/integration/multiplexed-corrections.test.js +551 -0
  108. package/test/integration/multiplexed-failures.test.js +339 -0
  109. package/test/integration/multiplexed-transport-set.test.js +456 -0
  110. package/test/integration/multiplexed-transport.test.js +208 -0
  111. package/test/integration/reverse-gateway.test.js +1293 -0
  112. package/test/integration/socketduct-http-agent.test.js +435 -0
  113. package/test/integration/socketduct-https-agent.test.js +669 -0
  114. package/test/integration/stream-compression.test.js +1267 -0
  115. package/test/integration/tcp-forwarding.test.js +2878 -0
  116. package/test/integration/tls-forwarding.test.js +748 -0
  117. package/test/unit/auth/auth.test.js +64 -0
  118. package/test/unit/auth/static-token-store.test.js +14 -0
  119. package/test/unit/client/connect.test.js +58 -0
  120. package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
  121. package/test/unit/client/multiplexed-transport-set.test.js +906 -0
  122. package/test/unit/client/socketduct-socket.test.js +1124 -0
  123. package/test/unit/client/virtual-socket.test.js +98 -0
  124. package/test/unit/config/docker-gateway-files.test.js +274 -0
  125. package/test/unit/config/environment.test.js +507 -0
  126. package/test/unit/index.test.js +78 -0
  127. package/test/unit/logging/structured-logger.test.js +674 -0
  128. package/test/unit/protocol/compression.test.js +421 -0
  129. package/test/unit/protocol/frame.test.js +336 -0
  130. package/test/unit/protocol/v3-compression.test.js +31 -0
  131. package/test/unit/protocol/v3-frame.test.js +57 -0
  132. package/test/unit/release-configuration.test.js +21 -0
  133. package/test/unit/server/control-handler.test.js +291 -0
  134. package/test/unit/server/primary.test.js +59 -0
  135. package/test/unit/server/relay-runtime.test.js +434 -0
  136. package/test/unit/sessions/session.test.js +84 -0
  137. package/test/unit/spool/spool-file.test.js +295 -0
  138. package/tsconfig.json +20 -0
package/.dockerignore ADDED
@@ -0,0 +1,11 @@
1
+ /node_modules
2
+ /npm-debug.log
3
+ /.git
4
+ /.github
5
+ /.env
6
+ .env*
7
+ !.env.example
8
+ coverage
9
+ dist
10
+ .tmp
11
+ *.log
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,3 @@
1
+ # Fix V2 stream ACK framing validation
2
+
3
+ The TCP relay now validates post-V2-handshake `stream.ack` records using the decoder-provided wire format, so a length-prefixed envelope cannot spoof compact framing through its JSON header.
@@ -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