You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The edge proxy operates in TLS passthrough mode (Cloudflare-less demo architecture: external client → edge SNI router → Tier-2 proxy that terminates TLS). The edge sees the raw TLS bytes and can extract JA4/JA4T fingerprints from the ClientHello. The Tier-2 proxy sees the cleartext but loses the L4/TLS fingerprint signal.
PR #350 wired PROXY v2 end-to-end so the Tier-2 proxy can recover the real client IP. The natural extension is to ride per-connection fingerprints in the same v2 header — populating the existing fp_lookup cache on the Tier-2 proxy without an out-of-band channel.
Design
In-band, via a PROXY v2 Custom TLV in the application-defined 0xE0..=0xEF range.
Edge side (encoder — passthrough.rs)
After the existing SNI peek, also call synapse_core::utils::tls_fingerprint::fingerprint_client_hello(&buf[..n]) to extract a Fingerprint (JA4 / JA4 unsorted / TLS version / cipher suite / SNI / ALPN). Serialize compact JSON (~250 bytes), wrap in ExtensionTlv::Custom { type_id: 0xE0, value: bytes }, and append to the v2 header extensions list before encode. Only fires for routes with proxy_protocol_v2: true — same opt-in as the existing source-addr emission.
Tier-2 side (receiver — start.rs)
At startup, register a callback via pingora_core::set_proxy_v2_tlv_callback:
pingora_core::set_proxy_v2_tlv_callback(Some(|tlvs, real_addr| {for tlv in tlvs {ifletExtensionTlv::Custom{type_id:0xE0, value } = tlv {ifletOk(fp) = serde_json::from_slice::<Fingerprint>(value){ifletSocketAddr::Inet(addr) = real_addr {
fp_lookup::put_self(addr.ip(), addr.port(),Arc::new(fp));}}}}}));
Once the entry is in fp_lookup keyed by the PROXY v2-recovered (client_ip, client_port), the existing proxyhttp.rs::request_filter lookup at line ~580 (crate::fp_lookup::get(peer_addr.ip(), peer_addr.port())) picks it up automatically — the WAF, access log, and rate limiter all see JA4 even though the TLS bytes never reach the Tier-2 proxy in cleartext.
Cross-repo dependencies
This work depends on two upstream fork PRs that need to land first:
In synapse: repoint [patch.crates-io] for the 16 pingora-* crates from branch = "main" (which is where it sits today, getting the source-addr-only PROXY v2 path) — no change needed since feat(pingora-core): expose PROXY v2 extension-TLV callback pingora#22 just adds to that same branch on merge. Bump Cargo.lock.
Add synapse_proxy::passthrough::encode_fingerprint_tlv and call it from handle_conn after extract_sni.
Add synapse_proxy::proxy_protocol::tlv_callback and register it in synapse_proxy::start::run next to set_proxy_protocol_enabled.
Configure proxy_protocol_v2: true on the demo passthrough routes (already set) and verify Axiom's synapse.access.fingerprints.ja4* populates for HTTPS traffic to test-a.
Goal
The edge proxy operates in TLS passthrough mode (Cloudflare-less demo architecture: external client → edge SNI router → Tier-2 proxy that terminates TLS). The edge sees the raw TLS bytes and can extract JA4/JA4T fingerprints from the ClientHello. The Tier-2 proxy sees the cleartext but loses the L4/TLS fingerprint signal.
PR #350 wired PROXY v2 end-to-end so the Tier-2 proxy can recover the real client IP. The natural extension is to ride per-connection fingerprints in the same v2 header — populating the existing
fp_lookupcache on the Tier-2 proxy without an out-of-band channel.Design
In-band, via a PROXY v2 Custom TLV in the application-defined
0xE0..=0xEFrange.Edge side (encoder —
passthrough.rs)After the existing SNI peek, also call
synapse_core::utils::tls_fingerprint::fingerprint_client_hello(&buf[..n])to extract aFingerprint(JA4 / JA4 unsorted / TLS version / cipher suite / SNI / ALPN). Serialize compact JSON (~250 bytes), wrap inExtensionTlv::Custom { type_id: 0xE0, value: bytes }, and append to the v2 headerextensionslist before encode. Only fires for routes withproxy_protocol_v2: true— same opt-in as the existing source-addr emission.Tier-2 side (receiver —
start.rs)At startup, register a callback via
pingora_core::set_proxy_v2_tlv_callback:Once the entry is in
fp_lookupkeyed by the PROXY v2-recovered(client_ip, client_port), the existingproxyhttp.rs::request_filterlookup at line ~580 (crate::fp_lookup::get(peer_addr.ip(), peer_addr.port())) picks it up automatically — the WAF, access log, and rate limiter all see JA4 even though the TLS bytes never reach the Tier-2 proxy in cleartext.Cross-repo dependencies
This work depends on two upstream fork PRs that need to land first:
ExtensionTlv::Custom { type_id, value }for the0xE0..=0xEFapplication range. Without this, the crate's enum rejects custom TLV types asInvalidTlvTypeId.set_proxy_v2_tlv_callbackhook inUninitializedStream::handshake. Without this, parsed TLVs are silently dropped bymaybe_consume_proxy_headerafter the source addr is recovered.Both PRs are open and tested.
Suggested commit sequence
pingora-core/Cargo.tomlto point at proxy-protocolmain(currently pinned to the feature branch). Merge feat(pingora-core): expose PROXY v2 extension-TLV callback pingora#22.[patch.crates-io]for the 16 pingora-* crates frombranch = "main"(which is where it sits today, getting the source-addr-only PROXY v2 path) — no change needed since feat(pingora-core): expose PROXY v2 extension-TLV callback pingora#22 just adds to that same branch on merge. BumpCargo.lock.synapse_proxy::passthrough::encode_fingerprint_tlvand call it fromhandle_connafterextract_sni.synapse_proxy::proxy_protocol::tlv_callbackand register it insynapse_proxy::start::runnext toset_proxy_protocol_enabled.proxy_protocol_v2: trueon the demo passthrough routes (already set) and verify Axiom'ssynapse.access.fingerprints.ja4*populates for HTTPS traffic to test-a.Out of scope for this issue