Expose forwarded PROXY v2 TLS facts as request.connectionInfo - #1985
Conversation
Harper learned to parse PROXY v2 in #1858 but only wired the mTLS client cert chain (0xE2) to getPeerCertificate — the other v2 TLVs symphony forwards were decoded and discarded, so an app behind Harper had no way to see the client's JA3/JA4 fingerprint, negotiated ALPN, SNI, or TLS version/cipher once the fronting proxy terminated TLS. Extend the decoder (server/serverHelpers/proxyProtocol.ts) to capture the full TLV set — ALPN (0x01), authority (0x02), SSL (0x20) with version (0x21) / cipher (0x23) sub-TLVs, JA3 (0xE0), JA4 (0xE1), client cert (0xE2) — into a ConnectionInfo, stash it on the socket in applyProxyHeader, and expose it as request.connectionInfo (server/serverHelpers/Request.ts). Also fix request.protocol to report https on the plaintext UDS mirror when the SSL TLV shows the client connection was TLS (previously always http there — wrong X-Forwarded-Proto). The mTLS cert path is preserved exactly: authorized + lazy/memoized getPeerCertificate stay gated on the 0xE2 chain. connectionInfo is only ever set from the decoded PROXY v2 header on the trusted UDS mirror, never from a request header. Implements harper#1983 items 3 (TLV exposure) and 4 (protocol fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Docs: this adds a public 🤖 Claude Opus 4.8, on Kris's behalf |
|
Reviewed; no blockers found. The lazy-allocation refactor in |
There was a problem hiding this comment.
Code Review
This pull request extends the PROXY v2 header parsing to decode and forward additional TLS connection facts (such as ALPN, SNI authority, TLS version/cipher, and JA3/JA4 fingerprints) via a new ConnectionInfo interface. These facts are stashed on the socket and exposed through request.connectionInfo. Additionally, the Request class's protocol getter is updated to correctly report 'https' on plaintext UDS mirrors when a fronting proxy forwards TLS facts. Unit tests have been updated accordingly. There are no review comments, and I have no feedback to provide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Docs companion opened: HarperFast/documentation#614 — documents 🤖 Claude Opus 4.8, on Kris's behalf |
hdbjeff
left a comment
There was a problem hiding this comment.
Requesting changes for the PP2_CLIENT_SSL protocol-inference issue below.
Otherwise this is solid, and I traced the parts most likely to be dangerous rather than taking the description on trust:
- Trust boundary holds. Production reaches applyProxyHeader through two direct call sites, both restricted to UDS-backed paths: enableProxyProtocol (http.ts:1653) for the HTTP/1 mirror, and consumeProxyHeader (proxyProtocol.ts:302), used by withProxyProtocol for both the h2c front (http.ts:1696) and the raw-protocol/MQTT UDS listener (threads/threadServer.js:619). Request.connectionInfo is a pure read of socket.connectionInfo; no request-header path reaches it.
- Bounds are correct. decodeV2 returns incomplete when buffer.length < headerLength, so the TLV loop bounded by headerLength cannot read past the buffer. Both the main loop and parseSslSubTlvs guard offset + 3 <= end and break on valueEnd > end, and a zero-length TLV still advances the offset by 3, so neither can spin.
- mTLS equivalence with #1858 holds. Old code set authorized = verified only under certPresented && clientCertChain; new code sets it only under chain, to info.tls?.verified === true where verified === certPresented && verifyOk. Since clientCertChain is itself only assigned under certPresented, the new value reduces to verifyOk — identical. The chain-present-but-no-SSL-TLV case also behaves the same.
- The tls.verified doc clarification, and the pointer to chain-gated request.authorized as the real mTLS gate, is the right call-out.
request.connectionInfo gives HarperFast/harper-woo-cache#212 exactly what it needs for the eventual PROXY-v2 migration, with application-level fingerprint validation retained. No other blockers found.
|
Blocking inline — proxyProtocol.ts, SSL TLV branch (lines 171–176) ▎ [P2] Honor PP2_CLIENT_SSL before reporting the connection as TLS |
|
Optional inline — the ConnectionInfo interface (lines 64–81) ▎ Non-blocking: could the ConnectionInfo docs state that the string fields are raw, unvalidated TLV payloads? |
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
TLV bounds checking, the trust gate (proxy protocol only on the UDS mirror), and mTLS equivalence through the refactor all check out, with unit coverage on every new TLV. Zero per-request cost. The two suggestions in my notes (PP2_CLIENT_SSL client-field bit, explicit connectionInfo member on the Bun/uWS request classes) are non-blocking.
sent with Claude Fable 5
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
Following up on my approval with the two suggestions inline (both non-blocking).
sent with Claude Fable 5
|
One more (can't anchor it inline — the lines aren't in the diff): the Bun and uWS request classes in sent with Claude Fable 5 |
- Gate the SSL TLV branch on PP2_CLIENT_SSL (0x01), not just presence: a proxy can legally emit an SSL TLV with client=0x00 (TLS not used), and connectionInfo.tls was truthy regardless, so request.protocol reported "https" for a cleartext client (hdbjeff, blocking). - Document that alpn/authority/ja3/ja4 are raw, unvalidated TLV payloads (hdbjeff, non-blocking). - Add explicit connectionInfo getters (returning undefined) on BunRequest/UwsRequest for API-surface consistency with the existing authorized/peerCertificate getters (Ethan-Arrowood, non-blocking). - Regression tests for an SSL TLV with client=0x00, with and without the CERT_CONN bit set alongside it. Co-Authored-By: Claude Opus <noreply@anthropic.com>
A v2 header with only address TLVs (or none) allocated and immediately discarded an empty ConnectionInfo object per accepted connection. Independent pre-push review flagged it as avoidable GC pressure on the default (metadata-free) path; build it lazily instead. Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Addressed the review feedback:
Also applied a suggestion from independent pre-push review: 125 relevant unit tests pass; build and lint are clean. 🤖 Claude Opus, on Kris's behalf |
Co-Authored-By: Claude Opus <noreply@anthropic.com>
- Gate the SSL TLV branch on PP2_CLIENT_SSL (0x01), not just presence: a proxy can legally emit an SSL TLV with client=0x00 (TLS not used), and connectionInfo.tls was truthy regardless, so request.protocol reported "https" for a cleartext client (hdbjeff, blocking). - Document that alpn/authority/ja3/ja4 are raw, unvalidated TLV payloads (hdbjeff, non-blocking). - Add explicit connectionInfo getters (returning undefined) on BunRequest/UwsRequest for API-surface consistency with the existing authorized/peerCertificate getters (Ethan-Arrowood, non-blocking). - Regression tests for an SSL TLV with client=0x00, with and without the CERT_CONN bit set alongside it. Co-Authored-By: Claude Opus <noreply@anthropic.com>
Closes #1983 (items 3 + 4).
Summary
#1858 taught Harper to parse PROXY protocol v2 on the UDS mirror, but only wired the mTLS client cert chain (
0xE2) togetPeerCertificate. The other TLVs symphony forwards undersourceAddressHeader: 'proxyProtocolV2'were decoded and thrown away — so an application component behind Harper had no way to read the client's JA3/JA4 fingerprint, negotiated ALPN, SNI, or TLS version/cipher once the fronting proxy terminated TLS. That's the blocking dependency @hdbjeff flagged for the WooCommerce/WAF fingerprint work.This extends the decoder to capture the full v2 TLV set into a
ConnectionInfoand exposes it asrequest.connectionInfo:TLVs parsed: ALPN
0x01, authority (SNI)0x02, SSL0x20(with version0x21/ cipher0x23sub-TLVs and the client-cert-present + verify bits), JA30xE0, JA40xE1, client cert chain0xE2. Also fixesrequest.protocolto reporthttpson the plaintext UDS mirror when the SSL TLV shows the client connection was TLS (it previously always returnedhttpthere → wrongX-Forwarded-Proto, #1983 item 4).Design note (new API surface — approved shape)
request.connectionInfois a new publicRequestaccessor; shape per #1983 ({ alpn, authority, tls, ja3, ja4, clientCertChain }). Chosen over synthesizingx-ja4headers because the TLV carrier is spoof-safe and works under h2 (nothing to inject into the request stream), and headers are client-forgeable.connectionInfois populated only from a decoded PROXY v2 header on the filesystem-permissioned UDS mirror — same trust boundary as the existing v1 source-address override — and never from a request header.Where to look
header.tlstoheader.connectionInfo.clientCertChain;authorized+ lazy/memoizedgetPeerCertificatestay gated on the0xE2chain exactly as Decode PROXY protocol v2 with forwarded mTLS client certificates on UDS mirrors #1858 had them. HTTP (security/auth.ts) and MQTT (server/mqtt.ts) mTLS are unchanged.connectionInfo.tls.verifiedsemantics. It reflects the SSL TLV's verify bit (cert presented + proxy-verified), and can betrueeven when no chain was forwarded (symphony omits an oversized chain but still sends the SSL TLV). Documented on the field; apps wanting the standard "verified mTLS client" gate should userequest.authorized, which is chain-gated. (Surfaced by the review — a doc clarification, not a behavior change.)parseSslSubTlvsand the main TLV loop are byte-bounds-guarded (offset+3 <= end,valueEnd > endbreaks); covered by unit tests + fuzz.Not in scope
0xE2mTLS cert (already shipped in Decode PROXY protocol v2 with forwarded mTLS client certificates on UDS mirrors #1858); trust-gating to the UDS listener (Parse PROXY protocol v2 and expose TLS facts (JA3/JA4, ALPN, SNI, mTLS cert) to the JS request layer #1983 item 5, already the case).Testing
unitTests/server/serverHelpers/proxyProtocol.test.js+Request.test.js+udsMirror.test.js: 123 passing. New coverage for every TLV (ALPN/authority/SSL sub-TLVs/JA3/JA4/cert), JA3/JA4-without-SSL,connectionInfoon the socket, v1-has-no-connectionInfo, andprotocolhttps-via-SSL-TLV.test:unit:mainnot run locally (pre-existing env failure —getUserstub / storage-path, reproduces on clean main); relying on CI.Review
Cross-model (thorough): Codex + Gemini + Grok (xAI, per request) outside legs + Harper domain adjudication. No blockers, no significant concerns — the bounds safety, #1858 mTLS equivalence, and trust boundary were independently traced. The two suggestions (the
verifieddoc note above; a per-connectionObject.keysallocation) are applied.Generated by an LLM (Claude Opus 4.8) pairing with Kris.