Go bindings for lkrequest — a Rust HTTP client with
byte-level TLS / HTTP2 / HTTP3(QUIC) fingerprint control, for browser emulation,
anti-detection research, and web scraping.
The package wraps the lkrequest-ffi C ABI behind a thin, idiomatic Go API:
Client— immutable, shareable client configuration (fingerprint, TLS, timeouts)Session— connection pool, proxy, redirect, retry, and cookie stateRequest— single-owner, consume-on-send request builderResponse/StreamingResponse— buffered and streaming (io.ReadCloser) reads
- Browser fingerprint presets — Chrome / Firefox / Safari TLS+HTTP2 fingerprints by name
- Fingerprint randomization — per-connection TLS extension-order jitter (drifts JA3, stays a real browser); synthetic cross-layer identities with the
synthetic-fplibrary - HTTP/2 & HTTP/3 (QUIC) — Alt-Svc discovery, per-request protocol preference, H3 header order
- Dual engine — default
purego(no C compiler, embedded library) orlkcgo(static CGo link) - Streaming responses —
SendStreaming()returns anio.ReadCloser - Cookie management — jar, attribute-aware
SetCookieWithAttrs, per-request override - Multipart upload — text + file parts, with Chrome-accurate
----WebKitFormBoundaryboundaries - Retry strategies — fixed interval / exponential backoff (with jitter)
- Proxy — per-session / per-request / proxy pool / session pool, with rotation, health checks, and bad-proxy marking; static header credentials (
Preshared,PrivateToken,Bearer, …) viaProxyConfig - MASQUE proxies (experimental) — QUIC tunneled through an RFC 9298 CONNECT-UDP proxy, with a private CA for the proxy hop; needs a
masque-enabled library - Connection prewarming —
Preconnect/PreconnectAsync - Automatic decompression — gzip / br / deflate / zstd, controllable via
AcceptEncoding - Certificate management — custom CA, verify toggle, native certs, ECH, TLS keylog
- Header / cookie ordering — configurable at client / session / request levels; cookies you do not order explicitly follow Chrome's own order
- Custom DNS — built-in DoH resolvers (Google / Cloudflare / Quad9) or a custom resolver callback
- SOCKS5 UDP relay probing — detect UDP ASSOCIATE support for QUIC-over-SOCKS5
- Request diagnostics — per-phase timings (DNS / TCP / TLS / TTFB) via
DiagnosticsJSON() - Context support — cancelable (
SendWithContext) and async (SendAsync) sends - Typed errors — error code, phase, retryable flag, and diagnostics on
LkError
go get github.com/EZXLabs/lkrequest-go/lkrequest@latestimport "github.com/EZXLabs/lkrequest-go/lkrequest"Top-level helpers use a process-wide default client:
resp, err := lkrequest.Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
defer resp.Close()
fmt.Println(resp.StatusCode())
fmt.Println(resp.String())The full Client → Session → Request → Response flow (all handles are explicitly closed):
client, err := lkrequest.NewClient("chrome_150") // or NewDefaultClient()
if err != nil {
log.Fatal(err)
}
defer client.Close()
session, err := lkrequest.NewSession(client)
if err != nil {
log.Fatal(err)
}
defer session.Close()
resp, err := session.Get("https://httpbin.org/get").Send()
if err != nil {
log.Fatal(err)
}
defer resp.Close()
fmt.Println(resp.StatusCode(), resp.Header("content-type"))Session has one helper per HTTP verb. Each returns a *Request to chain
setters onto, so a construction failure travels with the request and is
reported by Send() rather than needing its own error check.
resp, err := session.Get("https://httpbin.org/get").Send()
// Get / Post / Put / Delete / Head / Patch / Options,
// plus Request(method, url) for anything else.
resp, err = session.Request("PROPFIND", "https://example.com/dav").Send()NewRequest(session, method, url) is the same construction with the error
returned up front. SessionPoolGuard carries the same set of helpers.
Request is consume-on-send — chain setters, then send. Reuse via Clone().
resp, err := session.Post("https://httpbin.org/post").
AddHeader("accept", "application/json").
AddQuery("page", "1").
SetBearerAuth("my-token"). // or SetBasicAuth(user, pass)
SetJSON(map[string]string{"hello": "world"}). // marshals with encoding/json
Send() // SetJSONBody takes a pre-encoded documentCancelable and async sends:
resp, err := req.SendWithContext(ctx) // honors context cancellation
respCh, errCh := req.SendAsync(ctx) // runs in a goroutine, returns channelsClientBuilder is a chained builder; setter errors surface at Build().
client, err := lkrequest.NewClientBuilder().
SetPreset("chrome_144").
SetVerify(true).
SetUseNativeCerts(true).
AddDefaultHeader("Accept-Language", "en-US").
SetTimeoutTotal(30000). // milliseconds
SetMaxConnectionsPerSession(16).
SetFallbackH2ToH1(true).
Build()session, err := lkrequest.NewSessionBuilder(client).
SetProxy("socks5://user:pass@host:1080").
SetMaxRedirects(5).
SetHTTP2Only().
SetDefaultAcceptEncoding(lkrequest.AcceptEncodingGzip | lkrequest.AcceptEncodingBr).
SetRetryExponential(3, 50, 500, true). // maxRetries, baseMs, maxMs, jitter
Build()
// Fixed interval instead: SetRetryFixed(2, 100) // maxRetries, intervalMssession.SetCookie("https://example.com", "token", "abc123")
session.SetCookieWithAttrs("https://example.com", "sid", "xyz", lkrequest.CookieAttrs{
Path: "/api", Domain: "example.com", Secure: true, HTTPOnly: true,
})
val, _ := session.GetCookie("https://example.com", "token")
jar, _ := session.GetCookies("https://example.com") // []Cookie; GetCookiesJSON returns the raw payload
req.SetCookieOverride("token", "fresh-value") // per-request override of the jarmp := lkrequest.NewMultipart().
AddText("title", "report").
AddFile("document", "report.pdf", "application/pdf", pdfBytes)
req.SetMultipart(mp) // transfers ownership; the request can no longer be clonedstream, err := req.SendStreaming()
if err != nil {
log.Fatal(err)
}
defer stream.Close()
body, err := io.ReadAll(stream) // StreamingResponse implements io.ReadCloser// Proxy pool — rotates URLs, marks bad proxies
pool, err := lkrequest.NewProxyPoolBuilder().
AddProxies([]string{"socks5://p1:1080", "http://p2:8080"}).
SetRotation(lkrequest.RotationRoundRobin).
Build()
guard, _ := pool.Acquire()
defer guard.Close()
proxyURL := guard.URL()
// Session pool — each session bound to a different proxy
sp, err := lkrequest.NewSessionPoolBuilder(client).
AddProxy("http://p1:8080").
SetMaxSessions(8).
Build()
g, _ := sp.Acquire()
defer g.Close()
poolReq, _ := g.NewRequest("GET", "https://example.com")A proxy URL only carries a username and password. ProxyConfig also takes a
static HTTP credential and the header it travels in:
proxy, err := lkrequest.ParseProxyConfig("https://proxy.example:443")
if err != nil {
log.Fatal(err)
}
defer proxy.Close() // builders copy the config
proxy.SetHTTPAuth("Preshared", "<psk>"). // sent as "Preshared <psk>"
SetAuthHeader(lkrequest.ProxyAuthHeaderAuthorization) // default: Proxy-Authorization
session, err := lkrequest.NewSessionBuilder(client).SetProxyConfig(proxy).Build()
// or pool it: lkrequest.NewProxyPoolBuilder().AddProxyConfig(proxy)proxy.Identity() is the key ProxyPool.MarkBad expects — a proxy URL is not.
The mark is applied asynchronously, shortly after MarkBad returns.
A masque:// proxy carries the QUIC connection to the origin inside a
CONNECT-UDP tunnel. MasqueConfig configures the hop to the proxy; its trust
settings are independent of the client's SetVerify and CA certificates.
masque, err := lkrequest.NewMasqueConfig() // errors if the library lacks MASQUE
if err != nil {
log.Fatal(err)
}
defer masque.Close()
masque.AddCACertsPEM(caPEM). // a private CA for the proxy hop
SetUseNativeCerts(false)
client, _ := lkrequest.NewClientBuilder().SetMasqueConfig(masque).Build()
proxy, _ := lkrequest.ParseProxyConfig("masque://proxy.example") // port defaults to 443
session, _ := lkrequest.NewSessionBuilder(client).SetHTTP3Only().SetProxyConfig(proxy).Build()- A MASQUE route carries HTTP/3 only. HTTP/1.1 and HTTP/2 requests through it
fail instead of connecting directly, even with
SetFallbackProxyToDirect(true). - Pools skip
masque://proxies in health checks unless givenSetMasqueTunnelProbe(masque)together withSetHealthCheck. - The embedded libraries are built without the
masquefeature, so with themNewMasqueConfigreturns an error andmasque://URLs are rejected.
presets, _ := lkrequest.ListPresetsJSON() // enumerate available presets
detail, _ := lkrequest.GetPresetDetailJSON("chrome_150")
// Tier 1 — per-connection JA3 jitter, always available, still a real browser
client, _ := lkrequest.NewClientBuilder().
SetRandomize(lkrequest.RandomizeExtensionOrder, lkrequest.FingerprintLayersAll).
Build()RandomizeRecombine / RandomizeFull synthesize non-browser fingerprints (for blocklist targets)
and require a library built with the synthetic-fp feature; otherwise SetRandomize returns an error.
if lkrequest.FeatureSupported("quic-h3") {
session, _ := lkrequest.NewSessionBuilder(client).
SetHTTP3WithFallback(). // prefer H3, fall back to H2
Build()
}client, _ := lkrequest.NewClientBuilder().
SetDNS(lkrequest.DnsCloudflareHTTPS). // built-in DoH; or SetDNSCustom("1.1.1.1:53")
Build()lkrequest.InitLog("info", "/tmp/lkrequest.log") // level string, log file
lkrequest.InitLogCallback(func(level lkrequest.LogLevel, target, msg string) {
log.Printf("[%v] %s: %s", level, target, msg)
}, lkrequest.LogLevelInfo)resp, err := req.Send()
if err != nil {
var lkErr *lkrequest.LkError
if errors.As(err, &lkErr) {
fmt.Println(lkErr.Code(), lkErr.Phase(), lkErr.IsRetryable())
}
log.Fatal(err)
}
defer resp.Close()
if err := resp.ErrorForStatus(); err != nil {
// 4xx / 5xx
}Two interchangeable binding engines fill the same ffi_lk_* function pointers:
| Engine | Build tag | How it loads the library | Needs a C compiler |
|---|---|---|---|
purego (default) |
!lkcgo |
extracts the embedded shared library to a temp dir and dlopens it |
No |
cgo |
lkcgo |
#include "lkrequest.h" and links lib/<platform>/ statically |
Yes |
go build ./lkrequest/... # purego (default)
go build -tags lkcgo ./lkrequest/... # cgo| Platform | Status |
|---|---|
| linux / amd64 | ✅ shipped (embedded library) |
| windows / amd64 | ✅ shipped |
| darwin / arm64 | ✅ shipped (since 0.2.1) |
- Go 1.21+.
| Group | Symbols |
|---|---|
| Top-level helpers | Get, PostJSON, NewClient, NewDefaultClient, NewSession, NewRequest, NewMultipart, ParseProxyConfig, NewMasqueConfig |
| HTTP verbs | Session.Get / Post / Put / Delete / Head / Patch / Options / Request — same set on SessionPoolGuard |
| Builders | NewClientBuilder, NewSessionBuilder, NewProxyPoolBuilder, NewSessionPoolBuilder |
| Core types | Client, Session, Request, Response, StreamingResponse, ProxyPool, SessionPool, ProxyConfig, MasqueConfig, Multipart, Cookie, Redirect |
| Introspection | ABIVersion, LibraryVersion, FeatureSupported, ListPresetsJSON, GetPresetDetailJSON |
| Logging | InitLog, InitLogCallback |
| Enums | RandomizeMode, FingerprintLayers, AcceptEncoding, RotationStrategy, HttpVersion, PreferredHTTPVersion, DnsConfig, LogLevel, ProxyAuthHeader |
Full API docs: go doc github.com/EZXLabs/lkrequest-go/lkrequest.
ClientandSessionare safe to share across goroutines;Requestis consume-on-send (reuse viaClone()).StreamingResponseimplementsio.ReadCloser.ClientBuilder.Build()does not apply an implicit Chrome header-order preset; set a fingerprint or callAddHeaderOrder/AddCookieOrderwhen you need a specific order.- QUIC/H3, custom DNS resolver, SOCKS5 UDP probe, preferred/negotiated HTTP version,
ProxyConfig/MasqueConfig, and syntheticSetRandomizetiers require a library that exports the corresponding symbols. Against an older embedded library these return anlk: ... not supported by the loaded lkrequest libraryerror (builders defer it toBuild()); the package still loads normally.