Architecture
A top-down map of the library. Pull this up when you're trying to figure out which package owns a behaviour, or when you want to trace a request from Session.Do all the way down to a TLS ClientHello hitting the wire.
The layering
+---------------------------------+
user code ---> | httpcloak (root package) |
| - Client / Session |
| - WithX option constructors |
| - Multipart helpers |
+-----------------+---------------+
|
v
+---------------------------------+
| session/ |
| - cookie jar (own + sardanioss)|
| - per-host transport routing |
| - Refresh / Fork / Warmup |
| - state save / load |
+-----------------+---------------+
|
+-----------------------+-----------------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| HTTP1Transport| | HTTP2Transport| | HTTP3Transport|
| (raw TCP+TLS) | | (uTLS + h2cc) | | (quic-go h3) |
+---------------+ +---------------+ +---------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| proxy (HTTP | | proxy (HTTP | | proxy (UDP |
| CONNECT, | | CONNECT, | | SOCKS5, |
| SOCKS5) | | SOCKS5) | | MASQUE) |
+---------------+ +---------------+ +---------------+
\ | /
\ v /
\ +-----+-----+ /
+------------> | network | <-----------+
+-----------+
+-------------------------------+
built once, + fingerprint registry |
shared: | - Go presets (presets.go) |
| - JSON presets (embedded/) |
| - JA3 / Akamai / TCP fp data |
+---------------+---------------+
|
v
+-------------------------------+
shared: | dns/ |
| - SNI resolution |
| - HTTPS RR / ECH lookup |
+-------------------------------+
Three transports, one session layer, one fingerprint registry, one DNS layer. The session picks which transport handles a request based on what the host advertises (Alt-Svc for H3) and whatever you forced via options.
Packages
httpcloak (root)
The public Go API. Client, Session, all the WithX option constructors, multipart helpers. Deliberately thin. Everything interesting lives in subpackages. Most options just set fields on a protocol.SessionConfig that gets handed to session.NewSession.
Files: httpcloak.go, local_proxy.go.
session/
Stateful session. Owns the cookie jar, the per-host transport map, and the lifecycle of Refresh / Fork / Warmup / state persistence.
The interesting bits:
- One transport per host, lazily created. First request to
example.combuilds the transport. Every request after reuses it. Refresh()closes connections but keeps the cookie jar and the TLS resumption ticket cache. Can switch protocols on the way.Fork(n)returnsnchild sessions sharing the cookie jar and TLS cache but with their own connections. Like opening multiple browser tabs.Warmup(ctx, url)simulates a real page load: fetches the HTML, parses it, then pulls CSS/JS/image subresources with realistic per-resource priorities and timing.
transport/
The three transports plus the unifying Transport wrapper. One transport per protocol family.
HTTP1Transport: raw TCP, TLS via uTLS, HTTP/1.1 framing through thesardanioss/httpfork. Kicks in when a host doesn't speak H2 or H1's been forced.HTTP2Transport: uTLS for the ClientHello, then a customhttp2.ClientConnfrom thesardanioss/httpfork. That custom conn is what lets us send Chrome's exactSETTINGSorder,PRIORITYframes,WINDOW_UPDATEvalue, and HPACK indexing policy.HTTP3Transport:quic-gofromsardanioss/quic-go(with thePRIORITY_UPDATEfix and Chrome-style initial packet shaping) plushttp3.Transport.
The other stuff this package owns:
- Speculative TLS for proxy CONNECT: sends the CONNECT request and the TLS ClientHello in one TCP write. Saves a round-trip. Off by default because some proxies choke on it. Toggle via
WithEnableSpeculativeTLS. - Happy Eyeballs: IPv4/IPv6 racing inside the H3 dial functions. First address that completes the QUIC handshake wins. The loser gets closed.
raceH3H2: protocol racing. When a host advertises H3 via Alt-Svc and you haven't forced a protocol, an H3 dial and an H2 dial fire off in parallel. First successful handshake wins.- TLS session ticket cache: in-memory by default, pluggable via
WithSessionCache(backend, errCb)for distributed setups (Redis, etc.). - Connection pool: per-transport with idle timeout. H3 idle is set via
WithQuicIdleTimeout.
fingerprint/
The preset registry and the data structures behind it.
presets.go: Go-defined presets (Chrome 133-146, Firefox 133, Safari 18, iOS, Android variants). Each preset is afunc() *Preset.embedded/*.json: JSON-defined presets (Chrome 147, 148 across every platform). Loaded at package init and registered alongside the Go presets.custom_preset.go: JSON parsing plusBuildPreset. Powers both the embedded JSONs and user-supplied presets throughLoadPresetFromFile/LoadPresetFromJSON.describe.go: the inverse ofBuildPreset. Spits canonical JSON from a*Preset. Round-trip stable.client_hello_ids.go: string-to-uTLS ClientHelloID resolution.akamai.go: Akamai H2 fingerprint string parser. The shorthand formatSETTINGS|WINDOW_UPDATE|PRIORITY|PSEUDOis parsed here.ja3.go: JA3 string parser and TLS spec builder.headers.go: header order and value handling.preset_pool.go: round-robin or random rotation across multiple presets.priority_table_test.go(paired with the runtime piece in transport): RFC 9218 priority handling forsec-fetch-destdriven priorities.
proxy/
Proxy implementations. Three protocols:
- HTTP CONNECT: for
http://andhttps://proxy URLs. Used by H1 and H2. - SOCKS5: for
socks5://URLs. Used by H1, H2, and (if the proxy supports UDP-associate) H3. - MASQUE: UDP-tunnelled-over-HTTP/3 proxying. Used for H3 through an HTTP-aware UDP proxy. Implements the CONNECT-UDP method.
Proxy chains work (proxy through a proxy). WithSessionProxy sets one URL for all protocols. WithSessionTCPProxy + WithSessionUDPProxy lets you split it (say, SOCKS5 for H1/H2, MASQUE for H3).
dns/
DNS resolution layer.
- A/AAAA lookup with caching.
- HTTPS RR lookup for pulling ECH config. Used by
WithECHFromand the default ECH-when-available path. - Per-resolver overrides (system, DoH, custom).
protocol/
The IPC layer for the daemon binary (httpcloak-daemon). Languages other than Go (Python, Node.js, .NET) talk to the daemon over stdin/stdout JSON. Not relevant if you're using the Go API directly. But protocol/types.go defines SessionConfig, which is what the root package's NewSession builds internally.
client/
A second, lower-level client surface that predates the unified root API. Most folks should stick with the root httpcloak.Client / Session. The client/ package still ships its own WithX options for backwards compatibility (client.WithPreset, client.WithECHConfig, certificate pinning via PinCertificate).
Other directories
bindings/: language SDKs (Python, Node.js, .NET) that wrap the daemon or link against the C library.pool/: connection pooling primitives.streaming/: streaming-body request helpers.extensions/: uTLS extension shims.
Forked dependencies
httpcloak rides on four forks. Each one patches upstream to expose fingerprint-relevant knobs.
| Fork | Upstream | What we changed |
|---|---|---|
sardanioss/http | golang.org/x/net/http2 | Custom http2.ClientConn that lets us control SETTINGS order, WINDOW_UPDATE, PRIORITY frames, HPACK indexing policy, pseudo-header order, cookie splitting. |
sardanioss/utls | refraction-networking/utls | Additional ClientHello presets for newer Chrome / iOS / Safari versions, QUIC ClientHello variants, PSK variants, key share count control. |
sardanioss/quic-go | quic-go/quic-go | Chrome-style initial packet shaping, transport parameter order control, PRIORITY_UPDATE frame fix, GREASE frame emission. |
sardanioss/net | golang.org/x/net | Various small fixes for the http2 interaction. |
These are pinned in go.mod. For local dev you can use go.work to point at a local checkout of the fork.
Request flow
A normal Session.Get(ctx, "https://example.com/") walks this path:
- Cookie injection:
session/looks up cookies for the URL and tacks them on as aCookie:header (unlessWithoutCookieJaris set or the caller already passed their own). - Header building: preset's header order + values + your request-level overrides get merged into the final ordered list.
User-Agent,sec-ch-ua,accept-language, etc. all come from the preset. - Transport selection:
session/looks up the per-host transport. If none yet, it builds one based on whatever the host has advertised (H3 via Alt-Svc cache) or what you forced. - Connect / dial: the transport opens a connection if needed. For H1/H2: TCP, then TLS via uTLS with the preset's ClientHello. For H3: UDP, then QUIC handshake with the preset's QUIC ClientHello plus transport parameters.
- Happy Eyeballs races IPv4 and IPv6.
- Protocol racing fires H3 and H2 in parallel if the host advertises both.
- Through a proxy: CONNECT / SOCKS5 / MASQUE first, then handshake.
- DNS / ECH: handled by
dns/. ECH HTTPS RR is fetched unless disabled. - Wire send: the transport writes request frames (HTTP/2
HEADERS+DATA, or HTTP/3 equivalent). Frame ordering, settings, priorities all come from the preset. - Response: read back, parsed, body delivered as
io.ReadCloser. - Cookie storage:
Set-Cookieheaders from the response go into the jar (unlessWithoutCookieJar). - Redirect: if the response is 3xx and follow is on, jump back to step 1 with the new URL.
Threading and concurrency
Sessionis goroutine-safe for concurrent requests. Internal state (cookie jar, transport map, TLS cache) is mutex-protected.Fork(n)returns sessions sharing the cookie jar mutex with the parent. Concurrent requests across forks contend on the jar but ride independent connections.- Transport
Close()paths are wrapped incloseWithTimeoutbecause QUICClosecan hang forever on a half-closed UDP socket. One of the timeout patterns we keep a close eye on. - Per-request context propagates down to the transport. Cancel the context, you cancel the in-flight read, handshake, or dial.
Where to look when something's wrong
| Symptom | Look in |
|---|---|
| Wrong JA3 / cipher order | fingerprint/presets.go or fingerprint/embedded/*.json for the preset's TLS section. Cross-check uTLS ClientHello ID. |
| Wrong HTTP/2 SETTINGS / pseudo order | fingerprint/presets.go H2 settings + chromeH2Config / firefoxH2Config / safariH2Config. |
| Wrong header order | Preset's HeaderOrder field. Verify against tls.peet.ws/api/all. |
| H3 not used | Check the host's Alt-Svc, the preset's SupportHTTP3, and whether WithDisableHTTP3 was called. |
| Proxy doesn't connect | proxy/. Check the proxy URL scheme matches what the proxy speaks. |
| Hangs on close | transport/ close paths. Check the QUIC close timeout wrapping. |
| Timeout not respected | session/ and transport/. Context propagation, deadline computation. |
For everything else, the source reads easier than this map. Start at httpcloak.NewSession and just follow the calls.