socketduct 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -5
- package/README.md +3 -3
- package/changelog.d/20260925170000-disconnect-attribution-compressed-logs.md +1 -0
- package/changelog.d/20260925180000-manual-ack-durable-admission.md +1 -0
- package/docs/docker.md +5 -4
- package/docs/logging.md +131 -159
- package/docs/protocol.md +48 -7
- package/docs/reverse-gateway.md +83 -8
- package/docs/running.md +17 -13
- package/docs/security.md +7 -3
- package/docs/session-lifecycle.md +22 -0
- package/docs/socket-api.md +22 -17
- package/package.json +5 -1
- package/scripts/verify-zstd-interoperability.mjs +41 -0
- package/src/cli/socketduct.js +143 -0
- package/src/client/deferred-virtual-socket.js +42 -1
- package/src/client/multiplexed-connection.js +17 -0
- package/src/client/multiplexed-transport-set.js +43 -8
- package/src/client/multiplexed.js +1 -1
- package/src/client/virtual-socket.js +119 -4
- package/src/config/environment.js +56 -7
- package/src/index.js +65 -27
- package/src/logging/compressed-log-reader.js +256 -0
- package/src/logging/compressed-log-worker.js +430 -0
- package/src/logging/compressed-log-writer.js +538 -0
- package/src/logging/log-batch.js +72 -0
- package/src/logging/log-retention.js +241 -0
- package/src/logging/log-segment-index.js +176 -0
- package/src/logging/structured-logger.js +136 -30
- package/src/logging/zstd-frame-codec.js +73 -0
- package/src/reverse/client.js +245 -34
- package/src/reverse/disconnect-attribution.js +191 -0
- package/src/reverse/gateway.js +68 -9
- package/src/reverse/hub.js +215 -32
- package/src/reverse/runtime.js +5 -3
- package/src/spool/spool-file.js +92 -6
- package/tensorbuzz.yml +2 -1
- package/test/e2e/logger-shutdown-process.test.js +86 -0
- package/test/e2e/reverse-client-recovery-process.test.js +3 -0
- package/test/e2e/reverse-gateway-process.test.js +15 -14
- package/test/fixtures/reverse-client-recovery.mjs +14 -2
- package/test/integration/logging-cli.test.js +168 -0
- package/test/integration/logging-compressed-recovery.test.js +186 -0
- package/test/integration/multiplexed-transport-set.test.js +1 -5
- package/test/integration/reverse-gateway.test.js +334 -1
- package/test/integration/reverse-route-class.test.js +6 -1
- package/test/unit/client/multiplexed-transport-set.test.js +50 -3
- package/test/unit/client/virtual-socket.test.js +123 -1
- package/test/unit/config/docker-gateway-files.test.js +2 -0
- package/test/unit/config/environment.test.js +54 -14
- package/test/unit/index.test.js +5 -0
- package/test/unit/logging/compressed-log-worker.test.js +29 -0
- package/test/unit/logging/compressed-log-writer.test.js +183 -0
- package/test/unit/logging/log-retention.test.js +86 -0
- package/test/unit/logging/log-segment-index.test.js +104 -0
- package/test/unit/logging/structured-logger.test.js +14 -1
- package/test/unit/logging/zstd-frame-codec.test.js +24 -0
- package/test/unit/reverse/disconnect-attribution.test.js +82 -0
- package/test/unit/spool/spool-file.test.js +58 -0
package/.env.example
CHANGED
|
@@ -19,16 +19,23 @@ SOCKETDUCT_RESUME_WINDOW_MS=30000
|
|
|
19
19
|
# Emit secret-free JSON runtime counters at this interval. Set 0 to disable.
|
|
20
20
|
SOCKETDUCT_STATUS_INTERVAL_MS=30000
|
|
21
21
|
|
|
22
|
-
# Optional
|
|
23
|
-
# storage in Docker.
|
|
24
|
-
#
|
|
22
|
+
# Optional durable compressed JSONL logs. Bind mount this directory from host
|
|
23
|
+
# storage in Docker. Standard checksummed zstd frames and their private index
|
|
24
|
+
# are written by an isolated worker. Existing stdout/stderr is preserved.
|
|
25
25
|
# Host directory mounted by the checked-in production Compose files. Create it
|
|
26
26
|
# for uid/gid 1000 before deployment. Logging stays disabled unless the
|
|
27
27
|
# container-side SOCKETDUCT_LOG_DIRECTORY is also set.
|
|
28
28
|
# SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
|
|
29
29
|
# SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# SOCKETDUCT_LOG_FRAME_MAX_BYTES=1048576
|
|
31
|
+
# SOCKETDUCT_LOG_FLUSH_INTERVAL_MS=1000
|
|
32
|
+
# SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS=250
|
|
33
|
+
# SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
|
|
34
|
+
# SOCKETDUCT_LOG_RETENTION_DAYS=30
|
|
35
|
+
# SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
|
|
36
|
+
# SOCKETDUCT_LOG_QUEUE_MAX_BYTES=67108864
|
|
37
|
+
# SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES=8388608
|
|
38
|
+
# SOCKETDUCT_LOG_COMPRESSION_LEVEL=19
|
|
32
39
|
|
|
33
40
|
# Per-session hard byte limit for target-to-client data buffered while the
|
|
34
41
|
# edge tunnel is detached. When the limit is exceeded the relay terminates
|
|
@@ -113,6 +120,11 @@ SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375
|
|
|
113
120
|
# Server3 hub: change SOCKETDUCT_MODE to reverse-hub, keep the TLS server pair
|
|
114
121
|
# above, and bind every stable gateway ID to its credential username.
|
|
115
122
|
# SOCKETDUCT_REVERSE_GATEWAY_BINDINGS=o2:home-o2,zyxel:home-zyxel,starlink:home-starlink
|
|
123
|
+
# Required when a reverse hub enables SOCKETDUCT_LOG_DIRECTORY. Generate 32-64
|
|
124
|
+
# random bytes, encode them as unpadded base64url, keep the key hub-only, and
|
|
125
|
+
# rotate it with the public URL-safe epoch label at least every 30 days.
|
|
126
|
+
# SOCKETDUCT_CONNECTION_FINGERPRINT_KEY=<unpadded-base64url-secret>
|
|
127
|
+
# SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH=2026-09
|
|
116
128
|
# Authorize exact client username, gateway ID, and home-local target tuples.
|
|
117
129
|
# SOCKETDUCT_REVERSE_ROUTES=tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375,tensorbuzz:starlink:docker-server:2375
|
|
118
130
|
# Or bind one client physical connection to a route class and resolve each
|
package/README.md
CHANGED
|
@@ -35,8 +35,8 @@ Normal TCP sockets cannot magically resume after the underlying internet path di
|
|
|
35
35
|
sockets, and deterministic per-socket round-robin across ready members
|
|
36
36
|
- native reverse-gateway v3 lets authenticated private home members register
|
|
37
37
|
outbound with a Server3 hub, so CGNAT paths need no inbound forwarding
|
|
38
|
-
- optional bounded JSONL
|
|
39
|
-
|
|
38
|
+
- optional bounded diagnostic JSONL is stored as checksummed standard zstd
|
|
39
|
+
frames with an owned tail/search CLI, without replacing stdout/stderr
|
|
40
40
|
|
|
41
41
|
## MVP goals
|
|
42
42
|
|
|
@@ -113,7 +113,7 @@ hub address because `gatewayId` selects the outbound member. Docker remains
|
|
|
113
113
|
- [Docker topology](docs/docker-topology.md)
|
|
114
114
|
- [Node-compatible socket API design](docs/socket-api.md)
|
|
115
115
|
- [Native reverse gateway](docs/reverse-gateway.md)
|
|
116
|
-
- [
|
|
116
|
+
- [Durable compressed diagnostic logs](docs/logging.md)
|
|
117
117
|
- [Initial implementation plan](docs/plans/initial-implementation.md)
|
|
118
118
|
- [Socket + Docker integration plan](docs/plans/socket-docker-integration.md)
|
|
119
119
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Socketduct now preserves reverse disconnect causes across terminal errors and writes bounded, recovery-safe indexed zstd diagnostics with incremental owned tail/search behavior.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reverse clients can now durably admit complete writes, manually acknowledge application-committed receive bytes, and recover transport-set streams by stable identifier alone.
|
package/docs/docker.md
CHANGED
|
@@ -162,14 +162,15 @@ sudo install -d -o 1000 -g 1000 -m 0700 /srv/socketduct/logs
|
|
|
162
162
|
```text
|
|
163
163
|
SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
|
|
164
164
|
SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
|
|
166
|
+
SOCKETDUCT_LOG_RETENTION_DAYS=30
|
|
167
|
+
SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
|
|
167
168
|
```
|
|
168
169
|
|
|
169
170
|
The production relay and reverse-gateway Compose files mount the host directory
|
|
170
171
|
at `/var/log/socketduct`. Existing container stdout/stderr remains enabled. See
|
|
171
|
-
[
|
|
172
|
-
|
|
172
|
+
[durable compressed diagnostic logs](logging.md) for the indexed frame layout,
|
|
173
|
+
owned tail/search CLI, rotation, embedded configuration, and privacy contract.
|
|
173
174
|
|
|
174
175
|
Expected startup log (formatted here for readability; the actual line is JSON):
|
|
175
176
|
|
package/docs/logging.md
CHANGED
|
@@ -1,185 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Durable compressed diagnostic logs
|
|
2
2
|
|
|
3
|
-
Socketduct can write lifecycle diagnostics as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Socketduct’s managed runtime can write sanitized lifecycle diagnostics as
|
|
4
|
+
newline-delimited JSON records inside standard Zstandard frames. Logging remains
|
|
5
|
+
opt-in: set `SOCKETDUCT_LOG_DIRECTORY` for a `relay`, `reverse-hub`,
|
|
6
|
+
`reverse-gateway`, or summary process. Existing stdout/stderr output is unchanged.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The managed sink never falls back to plaintext. The older
|
|
9
|
+
`createJsonlFileLogger()` API remains available only for embedded callers that
|
|
10
|
+
explicitly select its legacy plaintext files; managed runtimes use
|
|
11
|
+
`createCompressedJsonlFileLogger()`.
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
`reverse-gateway` process:
|
|
13
|
+
## Configuration
|
|
12
14
|
|
|
13
15
|
```sh
|
|
14
16
|
SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
SOCKETDUCT_LOG_FRAME_MAX_BYTES=1048576
|
|
18
|
+
SOCKETDUCT_LOG_FLUSH_INTERVAL_MS=1000
|
|
19
|
+
SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS=250
|
|
20
|
+
SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
|
|
21
|
+
SOCKETDUCT_LOG_RETENTION_DAYS=30
|
|
22
|
+
SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
|
|
23
|
+
SOCKETDUCT_LOG_QUEUE_MAX_BYTES=67108864
|
|
24
|
+
SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES=8388608
|
|
25
|
+
SOCKETDUCT_LOG_COMPRESSION_LEVEL=19
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
retains at most the configured file count. A reused PID continues the same
|
|
29
|
-
bounded filename family.
|
|
30
|
-
|
|
31
|
-
On its first write, a logger also caps files left by definitely exited
|
|
32
|
-
same-role process instances to the configured file count in total, retaining
|
|
33
|
-
the newest files. Repeated signal-0 PID probes must report `ESRCH`, and the file
|
|
34
|
-
must remain unchanged, before it can be deleted. A live/reused PID, permission
|
|
35
|
-
failure, unsupported probe, changed file, or other indeterminate result
|
|
36
|
-
preserves every file in that family. Files are created with mode `0600`; a newly
|
|
37
|
-
created log directory uses mode `0700`.
|
|
38
|
-
|
|
39
|
-
For Docker, bind mount a persistent host directory and point Socketduct at the
|
|
40
|
-
container path:
|
|
41
|
-
|
|
42
|
-
```yaml
|
|
43
|
-
services:
|
|
44
|
-
socketduct:
|
|
45
|
-
environment:
|
|
46
|
-
SOCKETDUCT_LOG_DIRECTORY: /var/log/socketduct
|
|
47
|
-
SOCKETDUCT_LOG_MAX_FILE_BYTES: "10485760"
|
|
48
|
-
SOCKETDUCT_LOG_MAX_FILES: "5"
|
|
49
|
-
volumes:
|
|
50
|
-
- /srv/socketduct/logs:/var/log/socketduct
|
|
51
|
-
```
|
|
28
|
+
Those values are the defaults. Reaching the uncompressed frame limit starts a
|
|
29
|
+
flush immediately. Flush deadlines are measured from the oldest queued work and
|
|
30
|
+
are never extended by later records; critical lifecycle, security, and resume
|
|
31
|
+
events use the shorter deadline and may use the reserved queue capacity.
|
|
32
|
+
Segment size, age, and directory quota are bounded independently. Invalid or mutually
|
|
33
|
+
inconsistent values stop startup.
|
|
34
|
+
The former `SOCKETDUCT_LOG_MAX_FILE_BYTES` and `SOCKETDUCT_LOG_MAX_FILES`
|
|
35
|
+
plaintext settings are rejected instead of being silently applied to the new
|
|
36
|
+
format.
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`sudo install -d -o 1000 -g 1000 -m 0700 /srv/socketduct/logs`.
|
|
56
|
-
A permission, disk, or rotation error does not change tunnel,
|
|
57
|
-
session, target, or reconnect behavior. Socketduct reports only
|
|
58
|
-
`Socketduct logger failure (LOGGER_IO_ERROR)` to stderr, at most once per
|
|
59
|
-
logger, without including the path or underlying error text.
|
|
60
|
-
|
|
61
|
-
## Interrupted writes and retained evidence
|
|
62
|
-
|
|
63
|
-
Before appending, the file sink checks the last byte of a nonempty active file.
|
|
64
|
-
This bounded check also runs for a fresh logger or restarted process reusing
|
|
65
|
-
the same filename; it does not scan or parse historical records. A failed
|
|
66
|
-
append may have written a prefix even when the filesystem reports an error.
|
|
67
|
-
|
|
68
|
-
If the active file does not end in a newline, Socketduct moves the entire file
|
|
69
|
-
byte-for-byte into the existing numbered retention family and writes subsequent
|
|
70
|
-
events to a clean active file. The retained segment may still contain an
|
|
71
|
-
incomplete final record: it is incident evidence, not repaired JSONL. Normal
|
|
72
|
-
bounded retention eventually ages it out, so preserve incident evidence outside
|
|
73
|
-
the managed family before rollout or forensic work. New complete records must
|
|
74
|
-
parse independently; ignoring malformed historical lines is not proof of recovery.
|
|
75
|
-
|
|
76
|
-
If tail inspection, rotation, clean-file creation, or mode verification fails,
|
|
77
|
-
that logging call writes no new record. Later ordinary calls recheck the
|
|
78
|
-
filesystem; there is no background retry loop or replay of missed diagnostics.
|
|
79
|
-
If an append itself fails, it may leave another incomplete prefix that the next
|
|
80
|
-
call must separate. Logging failures still do not interrupt tunnel traffic.
|
|
81
|
-
|
|
82
|
-
With `maxFiles: 1`, there is no retained slot: an incomplete active file is left
|
|
83
|
-
untouched and file logging pauses. An incomplete active file already larger
|
|
84
|
-
than a newly lowered size limit also pauses logging **before startup pruning**;
|
|
85
|
-
it is not deleted to satisfy that limit. These pre-existing bytes can remain
|
|
86
|
-
outside the newly configured bound, but Socketduct will not grow them. An
|
|
87
|
-
operator must preserve the evidence separately and provide a clean path before
|
|
88
|
-
logging can continue. Socketduct never truncates or rewrites damaged records.
|
|
89
|
-
Already newline-terminated malformed historical records are not repaired.
|
|
90
|
-
|
|
91
|
-
The sink requires a regular active file with mode `0600` and a directory with
|
|
92
|
-
mode `0700`, including on reused paths. It checks the newly created file's
|
|
93
|
-
actual mode before writing and does not follow an active-file symlink. Fix
|
|
94
|
-
ownership/modes on an operator-preserved path rather than making logs public.
|
|
95
|
-
|
|
96
|
-
See [the private rollout runbook](runbooks/jsonl-integrity-rollout.md) for the
|
|
97
|
-
isolated fault commands and evidence-preserving Apoc-only rollout gates.
|
|
98
|
-
|
|
99
|
-
The checked-in production relay and reverse-gateway Compose files already mount
|
|
100
|
-
`${SOCKETDUCT_LOG_HOST_DIRECTORY:-./logs}` at `/var/log/socketduct`. Enable the
|
|
101
|
-
sink in `.env` after creating the host directory for the container's uid/gid
|
|
102
|
-
1000:
|
|
38
|
+
A reverse hub that enables managed logging must also configure a hub-only
|
|
39
|
+
correlation key:
|
|
103
40
|
|
|
104
41
|
```sh
|
|
105
|
-
|
|
106
|
-
|
|
42
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY='<unpadded-base64url-at-least-32-bytes>'
|
|
43
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH='2026-09'
|
|
107
44
|
```
|
|
108
45
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
46
|
+
Generate the key from random bytes, keep it out of gateways and clients, rotate
|
|
47
|
+
it at least every 30 days and on suspected exposure, and remove the previous
|
|
48
|
+
key after at most 24 hours of rollout overlap. The epoch is a public 1–32
|
|
49
|
+
character URL-safe label. Socketduct logs only the role-separated first 128 bits
|
|
50
|
+
of an HMAC-SHA-256 fingerprint; it never emits the key or raw connection ID.
|
|
112
51
|
|
|
113
|
-
##
|
|
52
|
+
## Storage and durability
|
|
114
53
|
|
|
115
|
-
|
|
116
|
-
|
|
54
|
+
The directory must be a real `0700` directory. Socketduct creates exclusive
|
|
55
|
+
`0600` files without following symlinks:
|
|
117
56
|
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
|
|
57
|
+
```text
|
|
58
|
+
<role>-<writerEpoch>-<segmentUuid>.jsonl.zst
|
|
59
|
+
<role>-<writerEpoch>-<segmentUuid>.jsonl.zst.idx
|
|
60
|
+
<role>-<writerEpoch>-<segmentUuid>.jsonl.zst.sealed
|
|
61
|
+
```
|
|
121
62
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
63
|
+
Every `.jsonl.zst` segment is a concatenation of independently decodable,
|
|
64
|
+
checksummed, standard zstd frames at level 19. Each frame contains complete
|
|
65
|
+
newline-terminated records only. Compression and filesystem sync run in a
|
|
66
|
+
dedicated worker thread, off the protocol event loop.
|
|
67
|
+
|
|
68
|
+
The versioned big-endian sidecar index contains structural identities, sequence
|
|
69
|
+
ranges, offsets, lengths, timestamps, frame SHA-256, and CRC32C checks. It does
|
|
70
|
+
not contain gateways, routes, incidents, reasons, connection IDs, or search
|
|
71
|
+
digests. A batch becomes durable only after Socketduct writes and `fdatasync`s
|
|
72
|
+
the data frame, appends and `fdatasync`s its index entry, then acknowledges the
|
|
73
|
+
exact deterministic batch identity to the producer.
|
|
74
|
+
|
|
75
|
+
On worker restart, Socketduct validates every retained segment, accepts a
|
|
76
|
+
retention-deleted sequence prefix, and still rejects gaps or duplicates inside
|
|
77
|
+
the retained range. It rebuilds one fully decodable data-synced orphan entry,
|
|
78
|
+
truncates only a torn final data or index tail, and replays only a batch not
|
|
79
|
+
proven durable by the high-water mark plus the single latest batch identity;
|
|
80
|
+
worker memory and acknowledgement size do not grow with history. Interior
|
|
81
|
+
corruption is preserved and marked quarantined; readers report incomplete
|
|
82
|
+
evidence. Retention deletes only oldest sealed, non-quarantined segments under
|
|
83
|
+
an exclusive process/thread-instance lock. A crashed owner is reclaimed using
|
|
84
|
+
PID/TID plus kernel start identities, while a live or unprovable owner remains fail-closed.
|
|
85
|
+
Active and quarantined bytes count toward the quota and are never deleted to
|
|
86
|
+
hide a failure.
|
|
87
|
+
|
|
88
|
+
`log()` is nonblocking and reports `accepted`, `coalesced`, or `dropped`.
|
|
89
|
+
`flush()` is a caller-time sequence barrier: it resolves after every record
|
|
90
|
+
accepted through that call is data-and-index durable, without waiting for later
|
|
91
|
+
producers. Idempotent async `close()` stops acceptance, drains all records and
|
|
92
|
+
bounded sink lifecycle records, seals, syncs, and terminates the worker. `status()` exposes writer and
|
|
93
|
+
durable high-water marks, allocated bytes, health, restart count, and exact drop
|
|
94
|
+
counters. Orderly SIGINT/SIGTERM closes the network runtime first, records the
|
|
95
|
+
final process lifecycle event, then awaits logger flush and close before exit.
|
|
96
|
+
The startup record includes the effective numeric logger settings but never the
|
|
97
|
+
configured directory or key material.
|
|
98
|
+
|
|
99
|
+
## Owned reader and CLI
|
|
100
|
+
|
|
101
|
+
Do not use `tail -f` on compressed bytes. Socketduct reads each indexed frame
|
|
102
|
+
independently and verifies the full integrity chain:
|
|
128
103
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
104
|
+
```sh
|
|
105
|
+
socketduct logs tail --directory /var/log/socketduct
|
|
106
|
+
socketduct logs tail --directory /var/log/socketduct --follow
|
|
107
|
+
socketduct logs search --directory /var/log/socketduct --incident '<22-char-id>'
|
|
108
|
+
socketduct logs search --directory /var/log/socketduct --event reverse_hub.physical_detached
|
|
135
109
|
```
|
|
136
110
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
111
|
+
Search filters include `--event`, `--incident`, `--gateway`, `--route-class`,
|
|
112
|
+
`--client-generation`, `--gateway-generation`, `--reason`, `--since`, and
|
|
113
|
+
`--until`. Records from all writers are merged deterministically by timestamp,
|
|
114
|
+
raw writer epoch, and numeric event sequence; that ordering does not imply
|
|
115
|
+
cross-process causality. Search exits `0` for one or more matches, `1` for no
|
|
116
|
+
matches in complete evidence, and `2` for invalid arguments, I/O failure, or
|
|
117
|
+
any selected corrupt/quarantined/incomplete segment. Tail returns `2` on the
|
|
118
|
+
same evidence failures. Closed segments also remain compatible with ordinary
|
|
119
|
+
`zstdcat` and `zstdgrep`. Follow mode advances sidecar-index cursors and decodes
|
|
120
|
+
only newly committed frames. Cursor state is limited to currently retained
|
|
121
|
+
segments and is pruned as retention removes closed segments, so rotation does
|
|
122
|
+
not cause duplicate output or repeated historical decompression.
|
|
123
|
+
|
|
124
|
+
## Privacy boundary
|
|
125
|
+
|
|
126
|
+
Records retain the existing 1,024-byte closed event/field allowlist. Disconnect
|
|
127
|
+
events may include a random incident ID, first local cause, origin and physical
|
|
128
|
+
role, explicitly named client/gateway generations, fan-out/resume/terminal
|
|
129
|
+
counts, safe gateway or route identity, safe socket code, and the hub-only keyed
|
|
130
|
+
fingerprint. Public terminal codes classify the outcome; they do not overwrite
|
|
131
|
+
the physical cause.
|
|
132
|
+
|
|
133
|
+
Socketduct never persists payload bytes, Docker requests or targets, tokens,
|
|
134
|
+
passwords, certificates or keys, raw connection IDs, HMAC keys, unkeyed identity
|
|
135
|
+
digests, peer-controlled reserved fields, spool paths/data, or arbitrary caller
|
|
136
|
+
objects. Logger health reports use fixed secret-free classifications.
|
|
137
|
+
|
|
138
|
+
## Embedded use
|
|
144
139
|
|
|
145
140
|
```js
|
|
146
|
-
import {
|
|
141
|
+
import {createCompressedJsonlFileLogger} from "socketduct"
|
|
147
142
|
|
|
148
|
-
const logger =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
destination.append(line)
|
|
152
|
-
}
|
|
143
|
+
const logger = createCompressedJsonlFileLogger({
|
|
144
|
+
directory: "/var/log/socketduct",
|
|
145
|
+
role: "tensorbuzz-client"
|
|
153
146
|
})
|
|
147
|
+
|
|
148
|
+
transportOptions.logger = logger
|
|
149
|
+
// At shutdown, after stopping producers:
|
|
150
|
+
await logger.flush()
|
|
151
|
+
await logger.close()
|
|
154
152
|
```
|
|
155
153
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
promises from custom loggers/writers are isolated from connection behavior.
|
|
161
|
-
|
|
162
|
-
## Event and data contract
|
|
163
|
-
|
|
164
|
-
The built-in logger emits one JSON object per line, capped at 1024 bytes. Every
|
|
165
|
-
entry contains an ISO-8601 `timestamp`, an allowlisted `event`, numeric
|
|
166
|
-
`processId`, and validated `role`. Depending on the component, events cover:
|
|
167
|
-
|
|
168
|
-
- process startup and periodic status counters
|
|
169
|
-
- physical transport connect, disconnect, retry scheduling, recovery, and
|
|
170
|
-
terminal failure
|
|
171
|
-
- transport-set readiness, member state/generation replacement, and terminal
|
|
172
|
-
loss
|
|
173
|
-
- reverse-gateway target open success, open failure, and close
|
|
174
|
-
- virtual-stream opening, opened, closed, and error transitions
|
|
175
|
-
|
|
176
|
-
Only finite allowlisted booleans, non-negative integer counters, validated
|
|
177
|
-
gateway IDs, states, and error classifications can be included. Unknown events
|
|
178
|
-
are dropped. Unknown error codes become `UNKNOWN`. Errors supplied through
|
|
179
|
-
`errorDiagnostics()` retain their name, message, and stack so terminal
|
|
180
|
-
transport and virtual-stream failures are actionable.
|
|
181
|
-
|
|
182
|
-
The diagnostic path never serializes protocol payloads or arbitrary caller
|
|
183
|
-
fields. It serializes error messages and stack traces produced by
|
|
184
|
-
`errorDiagnostics()` after redacting bearer credentials, URL userinfo, and
|
|
185
|
-
password/token/authorization values.
|
|
154
|
+
The `logger` option remains supported by Socketduct client, agent, transport-set,
|
|
155
|
+
and reverse-gateway APIs. A direct custom logger remains responsible for its own
|
|
156
|
+
durability. The legacy plaintext factory does not gain compressed recovery or
|
|
157
|
+
CLI guarantees and does not rewrite existing `.jsonl` files.
|
package/docs/protocol.md
CHANGED
|
@@ -133,9 +133,9 @@ each per-stream `RESUMED` result, and only then admits replay. Senders replay
|
|
|
133
133
|
durable raw chunks strictly above the high-water sequence while retaining
|
|
134
134
|
unacknowledged chunks and charging their outstanding bytes to the new window.
|
|
135
135
|
A DATA frame that entered a failed physical socket but did not reach the
|
|
136
|
-
logical receiver is therefore replayed; a byte
|
|
137
|
-
is not delivered twice. The target socket is never reopened and the
|
|
138
|
-
request is never recreated as a new stream.
|
|
136
|
+
logical receiver is therefore replayed; a byte durably acknowledged by the
|
|
137
|
+
receiver is not delivered twice. The target socket is never reopened and the
|
|
138
|
+
application request is never recreated as a new stream.
|
|
139
139
|
|
|
140
140
|
Reverse send replay is persisted before transmission on both the Server3 client
|
|
141
141
|
and outbound gateway. ACKs truncate those private temporary spools. Missing
|
|
@@ -151,6 +151,24 @@ returned to the application; merely buffering them below the readable
|
|
|
151
151
|
high-water mark is process-local and does not make them safe across replacement.
|
|
152
152
|
It never emits `OPEN`.
|
|
153
153
|
|
|
154
|
+
Reverse sockets default to this automatic application-consumption ACK. With the
|
|
155
|
+
persisted `manualReceiveAck` stream flag, consumption only advances an
|
|
156
|
+
application-delivered counter. An explicit byte count is valid only through
|
|
157
|
+
that counter. Socketduct atomically persists the resulting cumulative
|
|
158
|
+
`receiveAck` before emitting the wire `ACK`; persistence failure emits no ACK
|
|
159
|
+
and terminates the stream. ACK positions are application-byte positions and may
|
|
160
|
+
split a stored DATA chunk, in which case only its suffix remains replayable.
|
|
161
|
+
|
|
162
|
+
The reverse client also supports an all-or-nothing durable application
|
|
163
|
+
admission. It preflights the complete operation against current stream,
|
|
164
|
+
connection, and aggregate budgets, splits it into ordinary bounded DATA chunks,
|
|
165
|
+
writes the complete raw batch, and writes recovery metadata containing the new
|
|
166
|
+
`sendSequence` last. No DATA becomes send-eligible until that commit succeeds.
|
|
167
|
+
An interrupted pre-commit batch is an uncommitted tail: live retry replaces it,
|
|
168
|
+
and replacement recovery discards it using the older committed sequence. After
|
|
169
|
+
commit, ordinary flow-control, compression, transmission, ACK, replay, and END
|
|
170
|
+
ordering are unchanged.
|
|
171
|
+
|
|
154
172
|
Only the reverse client is replaceable in this protocol boundary. The same hub
|
|
155
173
|
process and retained bridge, outbound gateway process, and original target TCP
|
|
156
174
|
socket must survive. Hub restart reconstruction, gateway process/socket-keeper
|
|
@@ -181,6 +199,28 @@ payloads terminate the physical connection. TLS certificate verification is
|
|
|
181
199
|
mandatory for every reverse client and gateway transport. The complete role
|
|
182
200
|
contract is in [Native reverse gateway](reverse-gateway.md).
|
|
183
201
|
|
|
202
|
+
### Reverse disconnect attribution
|
|
203
|
+
|
|
204
|
+
Authenticated reverse GOAWAY and terminal RESET controls may carry the hub’s
|
|
205
|
+
closed attribution object: a 22-character random `disconnectIncidentId`, fixed
|
|
206
|
+
`disconnectReason`, `incidentOrigin`, `physicalRole`, applicable
|
|
207
|
+
`clientConnectionGeneration` / `gatewayConnectionGeneration`, and nonnegative
|
|
208
|
+
`fanOutCount`, `resumeSuccessCount`, and `terminalStreamCount`. A valid hub
|
|
209
|
+
incident takes precedence over a locally observed close. Endpoints preserve it
|
|
210
|
+
on public typed errors. The `code` remains a terminal classification and is not
|
|
211
|
+
interpreted as the transport cause.
|
|
212
|
+
|
|
213
|
+
The hub creates attribution before every retained or terminal fan-out. This
|
|
214
|
+
includes protocol/output-limit termination, replacement, peer GOAWAY, orderly
|
|
215
|
+
hub shutdown, direct transport loss, and resume expiry. A syntactically safe
|
|
216
|
+
peer GOAWAY `code` may classify resulting terminal RESETs, but peer attribution
|
|
217
|
+
fields are never trusted and cannot replace the hub-observed physical cause.
|
|
218
|
+
|
|
219
|
+
These names are hub-reserved. Attribution, fingerprint, and epoch fields in a
|
|
220
|
+
gateway-forwarded RESET are stripped before forwarding; malformed or unknown
|
|
221
|
+
values never become trusted telemetry. No control carries raw connection IDs,
|
|
222
|
+
HMAC keys, targets, credentials, certificates, or payload data.
|
|
223
|
+
|
|
184
224
|
## Legacy protocol version 2
|
|
185
225
|
|
|
186
226
|
V2 starts every connection with the length-prefixed JSON envelope
|
|
@@ -516,10 +556,11 @@ it, because every frame declares its own encoding.
|
|
|
516
556
|
|
|
517
557
|
### Runtime support
|
|
518
558
|
|
|
519
|
-
zstd is provided by `node:zlib
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
559
|
+
zstd is provided by Node 24's `node:zlib`; the package requires Node 24 or
|
|
560
|
+
newer. Stream-data support is still negotiated, so an explicitly disabled
|
|
561
|
+
codec runs raw. Durable diagnostic storage separately requires checksummed
|
|
562
|
+
standard zstd frames and never falls back to plaintext. No runtime dependency
|
|
563
|
+
is added.
|
|
523
564
|
|
|
524
565
|
## Acknowledgements
|
|
525
566
|
|
package/docs/reverse-gateway.md
CHANGED
|
@@ -116,6 +116,22 @@ per-stream and per-connection ceilings, and released connection capacity rotates
|
|
|
116
116
|
among streams with pending writes. The ceilings limit retained flow-control state,
|
|
117
117
|
not the size of one application write.
|
|
118
118
|
|
|
119
|
+
Reverse `VirtualSocket` instances also expose `writeDurable(bytes)` for one
|
|
120
|
+
complete serialized application operation such as an HTTP request. Unlike an
|
|
121
|
+
ordinary streaming `write()`, it requires the whole operation to fit the
|
|
122
|
+
current per-stream, per-connection, and transport-set retained/spool budgets.
|
|
123
|
+
Socketduct reserves the complete byte count, replaces any uncommitted spool
|
|
124
|
+
tail, writes every frame-sized raw chunk, and commits the new send sequence
|
|
125
|
+
before resolving. Only then can the first byte be transmitted. Limit rejection
|
|
126
|
+
uses `SOCKETDUCT_DURABLE_WRITE_LIMIT` (or an applicable `SOCKETDUCT_SET_*`
|
|
127
|
+
limit), and persistence failure uses
|
|
128
|
+
`SOCKETDUCT_DURABLE_WRITE_PERSISTENCE`; both admit zero bytes and leave the
|
|
129
|
+
socket available for retry. Each successful repeated call is a distinct
|
|
130
|
+
operation. A physical write failure after admission uses normal durable replay.
|
|
131
|
+
Destroy/close rejects with `ERR_STREAM_DESTROYED`, while detach finishes an
|
|
132
|
+
admission already committing, rejects queued admissions with
|
|
133
|
+
`SOCKETDUCT_CONNECTION_DETACHED`, and preserves committed spool state.
|
|
134
|
+
|
|
119
135
|
Compression and decompression use Node's asynchronous native zstd APIs, whose
|
|
120
136
|
work runs in the libuv worker pool across CPU cores. The hub keeps registration
|
|
121
137
|
and stream ownership on one deterministic event loop instead of splitting its
|
|
@@ -150,10 +166,17 @@ Reverse replay state is bounded per stream and connection and stored in private
|
|
|
150
166
|
spool directories. ACKed chunks are truncated, and terminal close/reset removes
|
|
151
167
|
the spool. `status().spoolBytes` reports retained durable payload. Reverse-client
|
|
152
168
|
replacement requires the same explicit `connectionId`, an explicit persistent
|
|
153
|
-
`spoolDirectory`, and the same stream identifier
|
|
154
|
-
|
|
155
|
-
`registerRecoveredConnection({...options, recover: true})` before
|
|
156
|
-
|
|
169
|
+
`spoolDirectory`, and the same stream identifier. A direct
|
|
170
|
+
`ReverseGatewayConnection` may still register the stored name, target, and
|
|
171
|
+
selector with `registerRecoveredConnection({...options, recover: true})` before
|
|
172
|
+
`connect()`. A transport set can instead call
|
|
173
|
+
`recoverConnection({identifier})` before `start()`; it validates every
|
|
174
|
+
configured member namespace and restores the stored target, name, receive-ACK
|
|
175
|
+
mode, and original selector without selecting a live path. No match fails with
|
|
176
|
+
`SOCKETDUCT_RECOVERY_NOT_FOUND`; more than one match fails with
|
|
177
|
+
`SOCKETDUCT_RECOVERY_AMBIGUOUS`. Corrupt, expired, or wrong-owner state fails
|
|
178
|
+
rather than being treated as absent. Legacy selector-based recovery remains
|
|
179
|
+
supported. Registration synchronously rejects missing, expired, corrupt, wrong-owner,
|
|
157
180
|
mismatched, noncontiguous, or oversized state before any network attempt. The
|
|
158
181
|
stored generation is advanced and persisted before AUTH, recovered outbound
|
|
159
182
|
replay starts above the gateway high-water position, and inbound delivery starts
|
|
@@ -165,6 +188,23 @@ repair derivable usage accounting after an abrupt process loss; structural,
|
|
|
165
188
|
identity, sequence, quota, and non-tail corruption still fail closed. Recovery
|
|
166
189
|
never sends `OPEN`.
|
|
167
190
|
|
|
191
|
+
The default remains automatic receive ACK. A reverse stream opts into durable
|
|
192
|
+
application ownership with `manualReceiveAck: true`. Socketduct then records
|
|
193
|
+
bytes delivered to the application but does not advance the durable ACK until
|
|
194
|
+
`await socket.acknowledgeReceive(bytes)` succeeds after the parser or journal
|
|
195
|
+
commit. Counts are application bytes and may end inside a DATA frame; sender
|
|
196
|
+
memory and spool state retain only the unacknowledged suffix. Calls serialize
|
|
197
|
+
and return the cumulative receive sequence. Non-positive counts or counts above
|
|
198
|
+
delivered, not-yet-acknowledged bytes fail with
|
|
199
|
+
`SOCKETDUCT_INVALID_RECEIVE_ACK`; a repeated successful call acknowledges new
|
|
200
|
+
bytes rather than duplicating the previous call. Calling without opting in
|
|
201
|
+
fails with `SOCKETDUCT_MANUAL_ACK_DISABLED`. Persistence failure rejects the
|
|
202
|
+
call and terminates the stream before a wire ACK advances. Calls after socket
|
|
203
|
+
close reject with `ERR_STREAM_DESTROYED`, and calls after detach reject with
|
|
204
|
+
`SOCKETDUCT_CONNECTION_DETACHED`; an ACK already admitted is persisted before
|
|
205
|
+
detach completes. Recovery preserves manual mode so a replacement cannot
|
|
206
|
+
silently fall back to automatic ACK.
|
|
207
|
+
|
|
168
208
|
The default reverse spool is under `/tmp`, and the sample reverse-gateway
|
|
169
209
|
Compose mounts `/tmp` as tmpfs. Those defaults do not survive a container or
|
|
170
210
|
host replacement. A caller claiming process/container recovery must supply a
|
|
@@ -198,6 +238,9 @@ SOCKETDUCT_TLS_KEY_FILE=/certs/hub.key \
|
|
|
198
238
|
SOCKETDUCT_AUTH_TOKENS='server-a-starlink:<secret>,server-b-starlink:<secret>,tensorbuzz:<secret>' \
|
|
199
239
|
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS='server-a-starlink:server-a-starlink,server-b-starlink:server-b-starlink' \
|
|
200
240
|
SOCKETDUCT_REVERSE_ROUTE_CLASS_ROUTES='tensorbuzz:starlink:server-a-starlink:docker-a:2375,tensorbuzz:starlink:server-b-starlink:docker-b:2375' \
|
|
241
|
+
SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct \
|
|
242
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY='<unpadded-base64url-secret>' \
|
|
243
|
+
SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH='2026-09' \
|
|
201
244
|
node src/index.js
|
|
202
245
|
```
|
|
203
246
|
|
|
@@ -209,6 +252,37 @@ Route-class rows use `username:routeClass:gatewayId:host:port`. If two rows make
|
|
|
209
252
|
different gateways eligible for the same authenticated class and target, that
|
|
210
253
|
`OPEN` is rejected as ambiguous rather than choosing one implicitly.
|
|
211
254
|
|
|
255
|
+
When hub logging is enabled, the fingerprint key is mandatory, hub-only, at
|
|
256
|
+
least 32 random bytes encoded as canonical unpadded base64url, and never transmitted or
|
|
257
|
+
logged. Rotate it at least every 30 days. The public epoch label identifies the
|
|
258
|
+
key generation used for a 128-bit role-separated HMAC correlation value.
|
|
259
|
+
|
|
260
|
+
## Disconnect attribution
|
|
261
|
+
|
|
262
|
+
Every reverse physical loss has one 128-bit random `disconnectIncidentId` and
|
|
263
|
+
one first local `disconnectReason`. The initial socket error, EOF, peer GOAWAY,
|
|
264
|
+
protocol violation, output limit, replacement, shutdown, or send-after-loss
|
|
265
|
+
cause wins over later close callbacks. The incident records its originating
|
|
266
|
+
component, physical client/gateway role, explicitly named connection
|
|
267
|
+
generations, and fan-out/resume/terminal stream counts. A public RESET code such
|
|
268
|
+
as `RESUME_WINDOW_EXPIRED` classifies the stream outcome; it does not replace
|
|
269
|
+
the physical cause.
|
|
270
|
+
|
|
271
|
+
The hub propagates its validated incident in RESET/GOAWAY controls and typed
|
|
272
|
+
`SocketductStreamError` / `SocketductSessionLostError` instances. Direct client
|
|
273
|
+
or gateway loss creates and caches an equivalent local incident so concurrent
|
|
274
|
+
stream errors share it, including writes racing the close callback. A new
|
|
275
|
+
authenticated physical clears that cache. Peer-provided reserved attribution
|
|
276
|
+
fields are stripped; peers cannot claim hub origin, inject correlation values,
|
|
277
|
+
or alter named generations.
|
|
278
|
+
|
|
279
|
+
Terminal, non-resumable hub paths assign the incident before removing their
|
|
280
|
+
bridges, including protocol violations, output overflow, replacement, peer
|
|
281
|
+
GOAWAY, and shutdown. A safe peer GOAWAY code survives as terminal
|
|
282
|
+
classification while the first hub-observed `peer_goaway` cause and hub-created
|
|
283
|
+
incident identity remain authoritative; fatal client and gateway connection
|
|
284
|
+
errors retain that validated attribution.
|
|
285
|
+
|
|
212
286
|
## One outbound member per target and route class
|
|
213
287
|
|
|
214
288
|
Run this once in each target server's route-pinned network namespace, changing
|
|
@@ -265,10 +339,11 @@ Callers cannot override either selection per open. Reverse streams remain
|
|
|
265
339
|
sticky to their selected gateway/target but resume transparently after a
|
|
266
340
|
transient physical transport replacement.
|
|
267
341
|
|
|
268
|
-
For process replacement, call `recoverConnection()` before
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
342
|
+
For process replacement, call `recoverConnection({identifier})` before
|
|
343
|
+
`start()`. Recovery locates exactly one configured durable member from the
|
|
344
|
+
stable stream identifier, derives its original identity and spool namespace,
|
|
345
|
+
and never falls back to or probes a sibling. The older explicit `gatewayId` or
|
|
346
|
+
`routeClass` form remains available when the caller already stores it.
|
|
272
347
|
|
|
273
348
|
The socket is returned immediately and waits inside Socketduct until the set
|
|
274
349
|
reaches minimum readiness. New sockets rotate equally across ready members and
|