socketduct 0.1.18 → 0.1.20
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 +5 -5
- package/changelog.d/20260925143000-reverse-client-process-recovery.md +1 -0
- package/changelog.d/20260925170000-disconnect-attribution-compressed-logs.md +1 -0
- package/docs/architecture.md +11 -9
- package/docs/docker.md +5 -4
- package/docs/logging.md +131 -159
- package/docs/protocol.md +43 -7
- package/docs/reverse-gateway.md +75 -9
- package/docs/running.md +16 -13
- package/docs/security.md +16 -5
- package/docs/session-lifecycle.md +14 -3
- package/docs/socket-api.md +54 -20
- package/package.json +5 -1
- package/scripts/verify-zstd-interoperability.mjs +41 -0
- package/src/cli/socketduct.js +143 -0
- package/src/client/multiplexed-transport-set.js +12 -5
- package/src/client/virtual-socket.js +52 -0
- package/src/config/environment.js +56 -7
- package/src/index.js +64 -26
- 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 +372 -82
- package/src/reverse/disconnect-attribution.js +191 -0
- package/src/reverse/gateway.js +68 -18
- package/src/reverse/hub.js +215 -32
- package/src/reverse/runtime.js +5 -3
- package/src/spool/spool-file.js +136 -7
- package/tensorbuzz.yml +4 -2
- package/test/e2e/logger-shutdown-process.test.js +86 -0
- package/test/e2e/reverse-client-recovery-process.test.js +170 -0
- package/test/e2e/reverse-gateway-process.test.js +15 -14
- package/test/fixtures/reverse-client-recovery.mjs +123 -0
- package/test/integration/logging-cli.test.js +168 -0
- package/test/integration/logging-compressed-recovery.test.js +186 -0
- package/test/integration/reverse-gateway.test.js +597 -1
- package/test/integration/reverse-route-class.test.js +6 -1
- package/test/unit/client/multiplexed-transport-set.test.js +12 -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 +3 -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 +133 -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
|
|
|
@@ -91,8 +91,8 @@ and automatically rejoin selection. A socket created before readiness is
|
|
|
91
91
|
returned immediately and waits inside Socketduct with normal callback, event,
|
|
92
92
|
timeout and backpressure behavior; assignment stays FIFO and occurs only on a
|
|
93
93
|
ready member. A virtual socket never changes members.
|
|
94
|
-
Durable process-replacement recovery uses `recoverConnection()` with
|
|
95
|
-
original gateway ID before the replacement set starts.
|
|
94
|
+
Durable process-replacement recovery uses `recoverConnection()` with exactly
|
|
95
|
+
the original gateway ID or reverse route class before the replacement set starts.
|
|
96
96
|
|
|
97
97
|
For home Docker hosts behind O2, Zyxel, and Starlink, run
|
|
98
98
|
`OutboundReverseGateway` in each router-pinned namespace and
|
|
@@ -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
|
+
Reverse clients can now recover crash-consistent persistent streams in a replacement process with application-consumption ACKs and a quiescent detach handoff, without reopening the surviving gateway target, while unsupported hub, gateway, and target loss fails closed.
|
|
@@ -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.
|
package/docs/architecture.md
CHANGED
|
@@ -85,12 +85,12 @@ gateway independently for each target `OPEN`, so adding target gateways does not
|
|
|
85
85
|
multiply client physical connections. Allocation metadata is `routeClass`;
|
|
86
86
|
legacy exact-gateway sets keep `gatewayId` and their existing identity derivation.
|
|
87
87
|
|
|
88
|
-
Replacement recovery is registered before set startup and addresses
|
|
89
|
-
original configured
|
|
90
|
-
identity/spool namespace and delegates recovery to that member
|
|
91
|
-
physical attempt; round-robin is never involved. An orderly set
|
|
92
|
-
active spools while retiring empty warmed lanes. Ordinary new
|
|
93
|
-
a
|
|
88
|
+
Replacement recovery is registered before set startup and addresses exactly one
|
|
89
|
+
original configured selector: `gatewayId` or `routeClass`. The set derives the
|
|
90
|
+
same member identity/spool namespace and delegates recovery to that member
|
|
91
|
+
before any physical attempt; round-robin is never involved. An orderly set
|
|
92
|
+
detach preserves active spools while retiring empty warmed lanes. Ordinary new
|
|
93
|
+
opens cannot name a member.
|
|
94
94
|
|
|
95
95
|
Protocol v2 retains the original one-session-per-tunnel architecture for
|
|
96
96
|
rollback. A relay listener selects v2 or v3 explicitly.
|
|
@@ -112,9 +112,11 @@ keeps its original target socket and both endpoints durably retain
|
|
|
112
112
|
unacknowledged send chunks. Replacement physical generations exchange paired
|
|
113
113
|
resume manifests; the hub reconciles receiver-committed sequence positions
|
|
114
114
|
before either side replays. This resumes the same byte stream without reopening
|
|
115
|
-
the target or issuing a second application request.
|
|
116
|
-
|
|
117
|
-
a new
|
|
115
|
+
the target or issuing a second application request. The Server3 reverse-client
|
|
116
|
+
object/process may be replaced from persistent spool state because the home
|
|
117
|
+
gateway still owns the target socket; the replacement creates a new app-facing
|
|
118
|
+
`VirtualSocket` and resumes from its durable receive ACK. The hub process,
|
|
119
|
+
gateway process, and target socket are not reconstructable by this mechanism.
|
|
118
120
|
|
|
119
121
|
The hub serializes output independently for each physical destination with a
|
|
120
122
|
bounded queue, so one backpressured target gateway cannot stop client frames
|
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
|
@@ -141,9 +141,22 @@ Reverse send replay is persisted before transmission on both the Server3 client
|
|
|
141
141
|
and outbound gateway. ACKs truncate those private temporary spools. Missing
|
|
142
142
|
targets, expired retention, identity or manifest mismatch, and configured
|
|
143
143
|
buffer/spool/output bounds produce one terminal stream error with the causal
|
|
144
|
-
code.
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
code. A replacement reverse client may restore its generation, selector,
|
|
145
|
+
lifecycle, directional sequences/ACKs, END state, and contiguous send replay
|
|
146
|
+
from persistent spool state before making a physical attempt. It presents the
|
|
147
|
+
durable receive ACK, not the dead object's readable-buffer high-water mark, so
|
|
148
|
+
the surviving gateway conservatively replays any target bytes that were not
|
|
149
|
+
durably acknowledged. A reverse client advances that ACK only after bytes are
|
|
150
|
+
returned to the application; merely buffering them below the readable
|
|
151
|
+
high-water mark is process-local and does not make them safe across replacement.
|
|
152
|
+
It never emits `OPEN`.
|
|
153
|
+
|
|
154
|
+
Only the reverse client is replaceable in this protocol boundary. The same hub
|
|
155
|
+
process and retained bridge, outbound gateway process, and original target TCP
|
|
156
|
+
socket must survive. Hub restart reconstruction, gateway process/socket-keeper
|
|
157
|
+
or FD transfer, target reconnection, and replay after target loss are not
|
|
158
|
+
defined. A fresh gateway that encounters retained bridges submits an empty
|
|
159
|
+
manifest; normal omitted-stream resets retire them without target creation.
|
|
147
160
|
|
|
148
161
|
Hub output has one ordered owner per physical destination. Frames for a
|
|
149
162
|
backpressured gateway remain in that gateway's bounded queue while frames for
|
|
@@ -168,6 +181,28 @@ payloads terminate the physical connection. TLS certificate verification is
|
|
|
168
181
|
mandatory for every reverse client and gateway transport. The complete role
|
|
169
182
|
contract is in [Native reverse gateway](reverse-gateway.md).
|
|
170
183
|
|
|
184
|
+
### Reverse disconnect attribution
|
|
185
|
+
|
|
186
|
+
Authenticated reverse GOAWAY and terminal RESET controls may carry the hub’s
|
|
187
|
+
closed attribution object: a 22-character random `disconnectIncidentId`, fixed
|
|
188
|
+
`disconnectReason`, `incidentOrigin`, `physicalRole`, applicable
|
|
189
|
+
`clientConnectionGeneration` / `gatewayConnectionGeneration`, and nonnegative
|
|
190
|
+
`fanOutCount`, `resumeSuccessCount`, and `terminalStreamCount`. A valid hub
|
|
191
|
+
incident takes precedence over a locally observed close. Endpoints preserve it
|
|
192
|
+
on public typed errors. The `code` remains a terminal classification and is not
|
|
193
|
+
interpreted as the transport cause.
|
|
194
|
+
|
|
195
|
+
The hub creates attribution before every retained or terminal fan-out. This
|
|
196
|
+
includes protocol/output-limit termination, replacement, peer GOAWAY, orderly
|
|
197
|
+
hub shutdown, direct transport loss, and resume expiry. A syntactically safe
|
|
198
|
+
peer GOAWAY `code` may classify resulting terminal RESETs, but peer attribution
|
|
199
|
+
fields are never trusted and cannot replace the hub-observed physical cause.
|
|
200
|
+
|
|
201
|
+
These names are hub-reserved. Attribution, fingerprint, and epoch fields in a
|
|
202
|
+
gateway-forwarded RESET are stripped before forwarding; malformed or unknown
|
|
203
|
+
values never become trusted telemetry. No control carries raw connection IDs,
|
|
204
|
+
HMAC keys, targets, credentials, certificates, or payload data.
|
|
205
|
+
|
|
171
206
|
## Legacy protocol version 2
|
|
172
207
|
|
|
173
208
|
V2 starts every connection with the length-prefixed JSON envelope
|
|
@@ -503,10 +538,11 @@ it, because every frame declares its own encoding.
|
|
|
503
538
|
|
|
504
539
|
### Runtime support
|
|
505
540
|
|
|
506
|
-
zstd is provided by `node:zlib
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
541
|
+
zstd is provided by Node 24's `node:zlib`; the package requires Node 24 or
|
|
542
|
+
newer. Stream-data support is still negotiated, so an explicitly disabled
|
|
543
|
+
codec runs raw. Durable diagnostic storage separately requires checksummed
|
|
544
|
+
standard zstd frames and never falls back to plaintext. No runtime dependency
|
|
545
|
+
is added.
|
|
510
546
|
|
|
511
547
|
## Acknowledgements
|
|
512
548
|
|