Heads-up for the maintainer: this issue was investigated and filed by Claude Code (Anthropic's coding agent), running on @moooyo's machine and posting from their account. Everything below was verified directly against the running image and the vendored @reclaimprotocol/tls, not inferred. Happy to adjust or close if I've misread something.
Summary
makeTLSClient(...) is called without a logger in two places, so @reclaimprotocol/tls falls back to its module-level default logger — whose info/debug/warn/error are bare console.* passthroughs with no level gating. Every userspace-TLS handshake therefore writes ~16 lines to the server's stdout.
Because an unset proxy fallback list resolves to DIRECT_CONNECT_ID (packages/gateway/src/dial/fetcher.ts), this is the default egress path — a stock self-hosted deployment gets this output without opting into anything.
Where
Call sites (neither passes logger):
packages/http/src/tls.ts — the tlsOptions object literal
packages/proxy/src/protocols/reality.ts — the makeTLSClient({ ... }) call
Library side, @reclaimprotocol/tls@0.1.2:
lib/types/tls.d.ts:102 — logger?: Logger is a supported option
lib/make-tls-client.js:20 — const logger = _logger || LOGGER
lib/utils/logger.js — the default LOGGER maps info/debug/warn/error to console.*; only trace is a no-op
DEBUG_USERSPACE_TLS doesn't gate this — it guards only Floway's own teardown line (logTlsTeardownError in packages/http/src/tls.ts).
Sample output
One handshake, verbatim from the container log (timestamps stripped):
{
cipherSuite: 'TLS_AES_128_GCM_SHA256',
connTlsVersion: 'TLS1_3',
selectedAlpn: undefined
} processed server hello
{ keyType: 'SECP256R1' } computed shared keys
received change cipher spec
{ len: 16, extData: {} } received encrypted extensions
{ len: 3 } parsed certificates
verified certificate chain
{ len: 516 } received certificate verify
{ alg: 'RSA_PSS_RSAE_SHA256' } parsed certificate verify
received server finish
server finish verified
{ len: 57 } received session ticket
{ len: 57 } received session ticket
These interleave with the request log, which makes docker logs hard to read for the ~17% of lines that are actually about requests.
Measured impact
From one instance serving Codex Desktop POST /azure-api.codex/responses:
| metric |
value |
| handshakes observed |
301 |
| total log lines |
5,796 |
| lines attributable to the TLS debug output |
4,866 (83%) |
| ≈ lines per handshake |
16 |
| container log growth at 7–10 req/min |
30 MB/day |
| extrapolated at 30–60 req/min |
90–180 MB/day |
Compounding it slightly: docker/docker-compose.yml sets no logging: options, so with Docker's default json-file driver the log grows unbounded. That's trivial for an operator to cap (and I have), but the default combination — verbose-by-default handshake logging plus unrotated json-file — can fill a small VM's disk over time.
Suggested fix
Pass an explicit logger at both call sites, e.g.
logger: { info() {}, debug() {}, trace() {}, warn: console.warn, error: console.error },
keeping warn/error so genuine failures still surface.
If the handshake detail is worth keeping for debugging, gating it behind the existing DEBUG_USERSPACE_TLS flag would match the pattern already used for teardown errors, and would keep the default quiet.
Happy to open a PR if that'd be useful — just let me know which shape you'd prefer.
Environment
- Floway
56dddc6d (image tag 20260816.31959732430, ghcr.io/menci/floway-server)
- Node v22.23.1, linux/arm64
@reclaimprotocol/tls 0.1.2 (in both packages/http and packages/proxy)
- Deployed with docker compose, direct egress (no fallback egress configured)
Summary
makeTLSClient(...)is called without aloggerin two places, so@reclaimprotocol/tlsfalls back to its module-level default logger — whoseinfo/debug/warn/errorare bareconsole.*passthroughs with no level gating. Every userspace-TLS handshake therefore writes ~16 lines to the server's stdout.Because an unset proxy fallback list resolves to
DIRECT_CONNECT_ID(packages/gateway/src/dial/fetcher.ts), this is the default egress path — a stock self-hosted deployment gets this output without opting into anything.Where
Call sites (neither passes
logger):packages/http/src/tls.ts— thetlsOptionsobject literalpackages/proxy/src/protocols/reality.ts— themakeTLSClient({ ... })callLibrary side,
@reclaimprotocol/tls@0.1.2:lib/types/tls.d.ts:102—logger?: Loggeris a supported optionlib/make-tls-client.js:20—const logger = _logger || LOGGERlib/utils/logger.js— the defaultLOGGERmapsinfo/debug/warn/errortoconsole.*; onlytraceis a no-opDEBUG_USERSPACE_TLSdoesn't gate this — it guards only Floway's own teardown line (logTlsTeardownErrorinpackages/http/src/tls.ts).Sample output
One handshake, verbatim from the container log (timestamps stripped):
These interleave with the request log, which makes
docker logshard to read for the ~17% of lines that are actually about requests.Measured impact
From one instance serving Codex Desktop
POST /azure-api.codex/responses:Compounding it slightly:
docker/docker-compose.ymlsets nologging:options, so with Docker's defaultjson-filedriver the log grows unbounded. That's trivial for an operator to cap (and I have), but the default combination — verbose-by-default handshake logging plus unrotatedjson-file— can fill a small VM's disk over time.Suggested fix
Pass an explicit logger at both call sites, e.g.
keeping
warn/errorso genuine failures still surface.If the handshake detail is worth keeping for debugging, gating it behind the existing
DEBUG_USERSPACE_TLSflag would match the pattern already used for teardown errors, and would keep the default quiet.Happy to open a PR if that'd be useful — just let me know which shape you'd prefer.
Environment
56dddc6d(image tag20260816.31959732430,ghcr.io/menci/floway-server)@reclaimprotocol/tls0.1.2 (in bothpackages/httpandpackages/proxy)