Skip to main content

geektls

多语言 TLS/HTTP 全栈指纹伪造库:对 ClientHello 逐字节可控,覆盖 TLS(JA3/JA4)、HTTP/2(Akamai 指纹 + HPACK 编码策略)、HTTP/3 + QUIC、TCP 四层指纹,提供 Python / Node.js / Go 三种语言的一等 API。

当前状态:P0–P7 完成(一期收工)。其中 TLS/H2 已交付并经 E2 外部 oracle 验证;H3/QUIC:Chrome 149 H3 已完成 E1 真机采集(QUIC 内层 JA4 与真机逐字符一致,详见功能矩阵),Firefox/Safari H3 尚未实测;TCP 仅 TTL/MSS 可承诺(window/window_scale/options 仅 Linux 探测模式,不影响真实连接)。P1-T8(nginx 采集端 L2 终审)已通过(7 预设 × 35 断言全绿,tests/e2e/nginx-l2/);P6-T3(JA4TCP 验收)需真实二层网络,回环无意义仍待。精确现状见 docs/01-fingerprint-dimensions.md 与 docs/capability-matrix.yml。

pypi

目录

功能矩阵

维度 状态 验证
TLS ClientHello 逐字段(ciphers/扩展序/GREASE/key_share/MLKEM/ALPS/RSL/DC/padding/ECH) ✅ tls.peet.ws 实测 7 预设 JA3/JA4 全 MATCH;RFC 9001 Initial 嗅探字节级
真 ECH(config_list 注入) ✅ cloudflare-ech.com 实测 ECHAccepted=true
指纹入口:完整 profile / JA3 / JA4 / JA4R / ClientHello-hex + 自算 JA3/JA4 回读 ✅ FoxIO 官方向量 + 线上 round-trip;JA4 短哈希走内置预设反查(哈希不可逆)
HTTP/2 帧层(SETTINGS 序/WINDOW_UPDATE(三态:默认/指定/不发)/priority/伪头序/首流号) ✅ tls.peet.ws Akamai 四段全 MATCH;三态与首流号另有原始帧断言(TestH2FrameCaptureTriState:帧有无 + 流号 + SETTINGS 全序)
HTTP/3 + QUIC(SETTINGS/伪头序/GREASE 帧/内层 ClientHello 同 profile) ✅(机制) Chrome 149 H3 E1 真机采集完成(tests/e2e/e1_h3_test.go,证据 profiles/evidence/browsers/chrome_windows_h3.json);QUIC 内层 JA4 与真机逐字符相同(钉在 quic_sniff_test.go);transport params 顺序/非标参数经 T4-1 blob 直通可控;Initial 布局仍不可控、quic_grease_frames 未验证、Firefox/Safari H3 未实测(见能力矩阵)
HTTP/2 HPACK 编码策略(chrome/firefox/safari/generic 四档,vendor fork fhttp) ✅ 253/368 预设已带 逐字节块表示断言(索引/literal/Huffman/动表复用);chrome 档有 QUICHE 源码级证据,firefox 档有 Firefox 59 字节级证据,safari 档为保守近似待 E1;工具与无法归族的内嵌浏览器刻意留空(见 docs/p2-h2-capability.md §覆盖面)
H2/H3 racing + Alt-Svc ✅ H3 默认关(h3=True/protocols 显式开) 协议选择用例 + 线上 ALPN 断言(TestProtocolH1Only 等);force_http3 语义不变
请求头顺序可控(preserve / input / random) ✅(默认 preserve = 旧行为) 原始 TCP 抓包逐行核对线上头序(含集合不变、Host 恒最前)
身份自洽(自带 UA ↔ 客户端提示) ✅(identity_sync="auto") 冲突时 sec-ch-ua* 校正到调用方 UA + warnings 如实告警;TLS/H2 不伪造
连接池(H2 多路复用/H1 keep-alive/H3 共享 transport,并发安全) ✅ go test -race 零竞争;echo 侧连接数断言
流式上传(H1 chunked / H2 DATA,三语言绑定迭代器入参) ✅ 线上字节逐字节断言
响应自动解压(gzip/deflate/br/zstd + 多编码链,流式同步生效) ✅ 六路径矩阵 + 跨压缩块边界流式断言
WebSocket(wss / **ws,RFC 6455 + permessage-deflate,握手走指纹链路,自动 masking/ping-pong) ✅ 手写 RFC 服务端断言头序/帧/关闭码(wss 与明文 ws 各一组);独立 stdlib flate 对端断言压缩流
明文 http:// / ws:// ✅(H1;无 TLS ⇒ selfcheck 恒为零值) 引擎用例(基本请求/连接复用/重定向/流式上传/明文 WS/代理环境变量)+ pytest 一组;force_http3 与明文互斥(明确报错,不静默换路径)
命令行入口 geektls(version / presets / describe / check-profile / request) ✅ go test ./cmd/geektls 五个子命令冒烟 + 退出码口径(0/1/2)
Python asyncio(AsyncSession,线程池异步,docstring 如实标注) ✅ asyncio 并发 20 请求用例
TCP(TTL/MSS setsockopt 档,按平台分文件:linux/darwin/windows/其它 Unix) ✅(Windows 无 MSS;BSD 档 TTL+MSS,DF 如实告警) getsockopt 读回;CI cross-OS compile gate 保证每个 GOOS 恰好一份实现
Python 请求语义对齐(cookie jar / timeout 元组 / auth / 重定向开关 / 表单编码) ✅ pytest test_requests_parity.py(10 条,本地明文服务端,零外部依赖)
Python / Node / Go 绑定 ✅ pytest 34 passed / 2 skipped、node:test 18、go test 全绿;跨语言 JA4 三方全等

安装

pip install geektls          # Python(manylinux_2_28 x86_64/aarch64、macOS 11.0+ arm64/x86_64、Windows x64,动态库随 wheel 分发)
npm install geektls          # Node.js(⚠️ 尚未发布到 npm,当前请用仓库内 bindings/nodejs)
go get github.com/geektls/core   # Go(⚠️ 模块尚未推送到公网仓库,当前用本地 replace)

已发布到 PyPI 的 wheel(0.1.5 起)覆盖五平台:manylinux_2_28 x86_64 / aarch64、macosx_11_0 arm64 / x86_64、win_amd64 (构建矩阵见 .github/workflows/release-pypi.yml;其余平台安装会提示无匹配版本; 不提供 sdist——从源码构建需要 Go + C 工具链,本库只发 wheel)。

⚠️ 老版本的平台覆盖(实测 PyPI 索引):

版本 已上传的平台 说明
0.1.0 仅 manylinux_2_34_x86_64 没有 Windows;Linux 基线是 glibc 2.34
0.1.1 – 0.1.3 manylinux_2_34_x86_64 + win_amd64 同上
≥ 0.1.4 五平台:manylinux_2_28 x86_64/aarch64、macosx_11_0 arm64/x86_64、win_amd64 Linux 基线 2.34 → 2.28(兼容面更大);macOS 与 Linux aarch64 从这里才开始提供

⇒ macOS、Linux aarch64、或 glibc < 2.34 的系统上 pin 到 0.1.0–0.1.3 会直接 No matching distribution found;本库不发 sdist,没有源码安装这条退路。

Python 一行自检:

python -c "import geektls,json;print(json.dumps(geektls.version()));print(len(geektls.list_presets()),'presets')"
# {"abi": 1, "core": "0.1.8", "utls": "refraction-networking/utls v1.8.2; bogdanfinn/utls v1.7.8-barnius; ..."} / 368 presets

version()["utls"] 是指纹栈溯源:报告动态库里实际链接的 uTLS / fhttp / quic-go-utls 版本(由二进制 build info 推导,跟随 go.mod 自动更新,不会漂移),vendor fork 会显示成 bogdanfinn/fhttp v0.6.9 => ./third_party/fhttp。出问题时把这一串贴进 issue 即可定位栈版本; 极端裁剪的构建可能为空串,调用方需容忍。

快速上手

Python(pip install geektls,动态库随包):

from geektls import Session

with Session(impersonate="chrome_150") as s:
    r = s.get("https://example.com")
    print(r.status_code, r.ok, r.used_protocol)   # 200 True h2
    print(r.headers["content-type"])             # 大小写不敏感
    print(r.text[:200])                          # 直接当文本用
    print(r.selfcheck["ja4"])                    # 本次握手的 JA4 自算
    for line in r.iter_lines():                  # 需要流式时才用迭代
        ...

Node.js(npm install geektls):

const { Session } = require('geektls');

const s = new Session({ impersonate: 'chrome_150', timeout: 20 });
const r = await s.get('https://example.com', { params: { q: 1 } });
console.log(r.statusCode, r.ok, r.reason);     // 200 true OK
console.log(r.header('content-type'));         // 大小写不敏感
console.log(await r.text());                   // 直接当文本用
console.log((await r.json()).slideshow);       // 直接当 JSON 用
for await (const chunk of r.iterContent()) { ... }   // 需要流式时才迭代
s.close();

Go(import "github.com/geektls/golang"):

// 引擎直连(要用到响应便捷方法时走这条;只要 net/http 习惯可用上面的 RoundTripper 形态)
p, _ := profiles.Get("chrome_150")
sess, _ := engine.NewSession(p, engine.SessionOptions{})
defer sess.Close()

resp, _ := sess.Do(&engine.Request{Method: "GET", URL: "https://example.com"})
defer resp.Close()
fmt.Println(resp.StatusCode(), resp.OK(), resp.Reason()) // 200 true OK
fmt.Println(resp.Header("content-type"))                 // 大小写不敏感
text, _ := resp.Text()                                   // charset → utf-8 → gb18030 → latin-1
var v map[string]any
_ = resp.JSON(&v)

命令行(geektls CLI)

不想先写代码就想看效果时:

make cli                                       # 构建到 build/geektls(Windows 下 build/geektls.exe)
build/geektls version
build/geektls presets --grade self             # 只看自测集;--json 结构化;--name 过滤
build/geektls describe chrome_154_windows | jq .tls.detail.extensions
build/geektls check-profile chrome_133         # 离线自检:预设名 / ja3: / ja4: / ja4r: / hex
build/geektls request https://example.com -i --selfcheck
build/geektls request http://127.0.0.1:8000/ --ja4 t13d1516h2_8daaf6152771_02713d6af862
  • 指纹构造与绑定同一批入口(profiles.Get / FromJA3 / FromJA4R / FromClientHelloHex / ResolveJA4Profile),用户自带输入同样过一遍 NormalizeForReplay 自洽归一 —— CLI 看到的就是代码里跑的;
  • --selfcheck 把"本次握手实际发出的 JA3/JA4"打到 stderr ⇒ stdout 只放正文,可直接管道或重定向;
  • 退出码:0 = 传输成功(含 4xx/5xx,curl 口径;加 --fail 让 4xx/5xx 返回 1)、 1 = 配置或传输错误(原因在 stderr)、2 = 用法错误。

协议选择 / 请求头顺序 / 身份自洽(默认值都是"最保守的那个")

三个开关,默认行为与旧版完全一致,需要时才开:

关心的事 参数 默认 可选值
走哪个 HTTP 版本 protocols / h3 ["h1.1","h2"](不含 H3) ["h1.1"] 只 H1.1 | ["h2"] 只 H2 | ["h2","h3"] 允许 H3 | ["h3"] 强制 H3;便捷写法 h3=True
请求头顺序 header_order preserve(按预设声明序) input(按你给的顺序)| random(打乱)
自带 UA 与预设不一致 identity_sync auto(校正客户端提示 + 告警) off(不校正)

为什么 H3 默认关:同一个 URL 走 H2 还是 H3 是可观测差异(协议入口、QUIC 侧指纹都不同), 不该由"预设里带 http3 节"替用户决定;curl_cffi / requests 生态的默认也是 H2,H3 要显式开。 以前只要预设带 http3.enabled + h2_race_ms 就自动竞速 H3,现在必须 h3=True 或 protocols 含 "h3";请求级 force_http3=True 语义不变(失败不回落)。

协议选择:Python

from geektls import Session

# 1) 默认(什么都不给):h1.1 + h2,**不碰 H3** —— 与 curl_cffi / requests 同口径
with Session(impersonate="chrome_154_windows") as s:
    r = s.get("https://example.com/")
    print(r.used_protocol, r.selfcheck["negotiated"]["alpn"])   # 例:h2 h2

# 2) 只要 H1.1:ALPN 里不再 offer h2(对端偏好 h2 也只能给 http/1.1)
with Session(impersonate="chrome_154_windows", protocols=["h1.1"]) as s:
    print(s.get("https://example.com/").used_protocol)          # http/1.1

# 3) 只要 H2:对端不支持 h2 时**直接失败**,不静默回落 H1(要的就是这个确定性)
with Session(impersonate="chrome_154_windows", protocols=["h2"]) as s:
    print(s.get("https://example.com/").used_protocol)          # h2

# 4) 允许 H3(竞速/Alt-Svc 优先,失败落回 TCP);h3=True 是便捷写法
with Session(impersonate="chrome_154_windows", h3=True) as s:
    print(s.get("https://example.com/").used_protocol)          # h3 或 h2(看谁先拿到响应头)

# 5) 只走 H3(会话级强制,失败即报错,不回落)
with Session(impersonate="chrome_154_windows", protocols=["h3"]) as s:
    print(s.get("https://example.com/").used_protocol)          # h3

# 6) 请求级强制(0.1.6 起语义不变:仍是"必须 H3",但会话 protocols 显式排除 h3 时冲突报错)
with Session(impersonate="chrome_154_windows") as s:
    r = s.request("GET", "https://example.com/", force_http3=True)

要点:h3=True 与 protocols=[...] 不能同时给(两种意图打架,直接报错,不静默取其一); h3=False 与 protocols 同给不报错(常见写法,无额外意图)。开了 h3 但预设没有 http3 声明 会在建会话时报错,而不是悄悄退回默认 QUIC 参数。

协议选择:Node

const { Session } = require('geektls');

const s = new Session({ impersonate: 'chrome_154_windows' });                       // 默认 h1.1+h2
const s1 = new Session({ impersonate: 'chrome_154_windows', protocols: ['h1.1'] }); // 只 H1.1
const s2 = new Session({ impersonate: 'chrome_154_windows', h3: true });            // 允许 H3
const s3 = new Session({ impersonate: 'chrome_154_windows', protocols: ['h3'] });   // 只 H3
const r = await s.get('https://example.com/');
console.log(r.usedProtocol, r.selfcheck?.negotiated?.alpn);

协议选择:CLI

build/geektls request https://example.com --protocols h1.1 --selfcheck   # 只 H1.1(ALPN 打 stderr)
build/geektls request https://example.com --protocols h2                 # 只 H2
build/geektls request https://example.com --h3                           # 允许 H3
build/geektls request https://example.com --protocols h3                 # 只 H3
build/geektls request https://example.com --http3                        # 请求级强制

请求头顺序:三档

with Session(impersonate="chrome_154_windows") as s:
    # 默认 preserve:按 profile.http1.header_order 归位(= 与 0.1.6 之前逐字节相同)
    s.get(url)

with Session(impersonate="chrome_154_windows", header_order="input") as s:
    # 按**你传入字典的顺序**发(Host 仍在最前);requests 用户自己排好序时用这个
    s.get(url, headers={"accept-language": "en-US,en;q=0.9", "accept": "*/*"})

with Session(impersonate="chrome_154_windows", header_order="random") as s:
    # 每次请求打乱普通头顺序(Host 恒最前);绕"顺序即信号"的检测用
    s.get(url)

CLI 同理:--header-order preserve|input|random。

头序三档的取舍:真浏览器的头序是固定的,random 是"对抗顺序检测"的手段而不是更真—— 所以默认 preserve,且它会破坏可复现性(回归/对拍时别开)。

自带 UA 时的身份自洽

WIN_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
          "(KHTML, like Gecko) Chrome/161.0.0.0 Safari/537.36")

# 预设是 macOS 形态,但你写 Windows UA ⇒ 默认 auto:sec-ch-ua*/platform/mobile 跟着校正
with Session(impersonate="chrome_154_macos") as s:
    r = s.get(url, headers={"user-agent": WIN_UA})
    print(r.warnings)      # ["identity_sync: … TLS/JA3/JA4/H2 仍为该预设 …"]

# 想要连 TLS 也一致:换同平台变体预设(它的 ClientHello 本来就不同),而不是靠改头
with Session(impersonate="chrome_154_windows") as s:
    s.get(url, headers={"user-agent": WIN_UA})   # TLS 与 UA 都是 Windows 形态

# 不想要任何校正/告警:off(= 旧行为)
with Session(impersonate="chrome_154_macos", identity_sync="off") as s:
    s.get(url, headers={"user-agent": WIN_UA})

CLI:--identity-sync auto|off。

身份自洽的边界(如实说明):你自带 user-agent 时,identity_sync="auto" 会把 sec-ch-ua(版本号)/sec-ch-ua-platform/sec-ch-ua-mobile 校正到该 UA,因为这几项与 UA 同源、不一致是明显破绽。但 TLS/JA3/JA4/H2 不会跟着变——凭空改出来的指纹不属于任何真实 浏览器,比不一致更糟。想要"UA 与 TLS 也一致",正确做法是换同平台变体预设 (chrome_154 的 _windows / _macos / _android 变体的 TLS 真的不同),库会把这句写进 r.warnings。查变体:geektls.list_presets() 或 geektls check-profile <名>。

指纹怎么传:整体与局部

Session(...) 的指纹入参六选一(互斥,同时给以靠前的为准,优先级按下列顺序):

入参 语义 保真度 备注
impersonate="chrome_150_windows" 用内置预设(368 条) 高(多数有 E1/E2 证据) 名字是全名:chrome_154_macos、chrome_154_windows、okhttp_3_12_12…(没有裸 chrome_154)
profile={...} 自带完整指纹(profile JSON schema) 完全按你给的来 也接受 JSON 文本(profile='{"name":...}'),免去 json.loads
ja3="771,4865-...-23,4588-29-23-24,0" 只给 JA3 有损:扩展只有 type,负载全缺 缺失部分按引擎默认补齐,check_profile 会逐条列出 warnings
ja4r="t13d1516h2_002f,..._..._..." 只给 JA4R(含 cipher/扩展/sig_algs 列表) 中:列表有序化丢失(extensions_sorted 告警) 想"逐字节复刻"用这个或完整 profile;带 ECH(65037) 时按 GREASE 近似并告警(ech_assumed_grease),因为 raw 串不含负载
ja4="t13d1516h2_8daaf6152771_d8a2da3f94cd" 只给 JA4 短哈希 哈希不可逆 引擎在内置预设里反查同一 JA4 并采用其参数(ja4_resolved_to_preset 告警);找不到会明确报错,不会编近似指纹
clienthello_hex="160301..." 直接给原始 ClientHello 字节 最高(等于抓包重放的结构) 与生成器/采集端同一条解析路径

没传的部分如何保持自洽(这是"局部指纹"能用的关键):

  1. 重放自洽归一(core/profiles/replay.go):带 pre_shared_key(41) 的双向约束——声明 TLS 1.3 就必须有 41 空占位且位于扩展末尾(RFC 8446),不声明就必须没有(否则 ClientHello 结构非法)。补的是空占位:无票据时线上省略、也不计入 JA3/JA4,所以首次握手的指纹不变;不补的话,一旦会话复用命中票据,uTLS 会直接 panic。
  2. 如实告警:JA3 入口会报 grease_lost / extension_payloads_lost;JA4R 会报 extensions_sorted;JA4 反查会报 ja4_resolved_to_preset。宁可说清损失,也不假装保真。
  3. 离线验证:geektls.check_profile(...) 不发包,直接告诉你"这么传会发出什么"(JA3/JA4/wire 长度/warnings):
import geektls

# 整体:完整 profile 文本
print(geektls.check_profile('{"name":"mine","tls":{"detail":{"ciphers":["grease","0x1301"],"extensions":[{"type":43,"versions":["grease","0x0304","0x0303"]}]}}}')["ja4"])

# 局部:只给 JA3(会看到补占位 + 负载丢失的告警)
print(geektls.check_profile("771,4865-4866-4867-49195-49199,43-10-11-13-16-23-5-0-65281,4588-29-23-24,0"))

# 只给 JA4 短哈希(反查内置预设)
print(geektls.check_profile("t13d1516h2_8daaf6152771_d8a2da3f94cd"))

Python API 参考

模块级函数

函数 签名 说明
version() -> dict {"abi":1,"core":"0.1.8","utls":"<指纹栈版本串>"};启动时可用它断言 ABI 匹配,utls 用于溯源(见上文)
init(options=None) -> None 幂等初始化钩子(当前无全局状态,留作后续)
last_error() -> dict 最近一次失败的结构化错误(code/message/op)
list_presets() -> list[str] 全部内置预设名(排序,368 条;返回规范名,旧名是别名)
describe_preset(name) -> dict 预设展开后的规范 JSON(ciphers/扩展/H2/H3/身份头全展开)
check_profile(spec) -> dict 离线自检:profile JSON / JA3 / JA4 / JA4R / hex → {ja3,ja3_hash,ja4,wire_len,warnings}
GeekTLSError 异常类 所有失败都是它;.code 结构化错误码(invalid_config/request_failed/…)
default_session(**options) -> Session 模块级共享会话;首次调用可传 Session(...) 参数配置(之后只读,再传报错)
get/head/post/put/patch/delete/options(url, **kwargs)、request(method, url, **kwargs) -> Response requests 风格快捷 API,走 default_session()(连接池与 Cookie jar 随之共享;与 requests 的模块级 API 同语义)

Session

Session(impersonate=None, *, profile=None, ja3=None, ja4=None, ja4r=None,
        clienthello_hex=None, headers=None, proxies=None, timeout=None,
        read_timeout=None, resolve=None, local_address=None, ip_version=None,
        verify=None, cert=None, cert_key=None, allow_redirects=None,
        max_redirects=None, cookies=None, auth=None, trust_env=None, **options)
参数 类型 说明
impersonate / profile / ja3 / ja4 / ja4r / clienthello_hex str / dict / str 六选一(见上一节)
headers dict / list[tuple] 会话默认请求头(按给定顺序发出)
proxies dict / str requests 风格;引擎一次只用一个代理 URL({"https": ...} / {"all": ...})。scheme:http/https(CONNECT 隧道)、socks5(本地先解析域名)、socks5h(域名交给代理解析)、socks4/socks4a(仅 IPv4 目标;只有 user id 无口令位,URL 给口令即报错)
trust_env bool requests 同名,映射到引擎 proxy_from_env:False ⇒ 不读 HTTPS_PROXY/ALL_PROXY,只认显式 proxies;默认 True 会读,并按 NO_PROXY 与回环目标(localhost/127.0.0.1/::1)豁免
timeout float 秒 / (connect, read) 单值映射到引擎 timeout_ms(覆盖"dial+TLS+响应头");元组时 connect 进 timeout_ms、read 进 read_timeout_ms(任一项可为 None),请求级可用 timeout_ms= 覆盖
read_timeout float 秒 映射到引擎 read_timeout_ms:每次读 body 的空闲上限(默认 0=不限,与旧行为一致);请求级可用 read_timeout_ms= 覆盖。超时是结构化错误 code="read_timeout",调用线程毫秒级返回,底层连接作废
resolve dict[str, str] curl --resolve:{"host": "1.2.3.4"} 或 {"host:8443": "..."}(键精确匹配,不做通配/子域;值必须是 IP 字面量)。只改"连到哪":SNI / Host / 伪头 / ClientHello 仍用 URL 里的原域名,指纹字节不变。只在引擎本地解析的那几档生效(直连 / netstack / socks5 / socks4);socks5h/socks4a/CONNECT 由代理解析,钉位不参与(不静默改道)
local_address str curl --interface 的 IP 形态 / httpx local_address:绑出网源地址,只收 IP 字面量(网卡名不支持)。隐含协议族,与 ip_version、resolve 取值冲突建会话即 ValueError;tcp.mode=netstack 下报错(用户态栈源地址固定)而不是忽略
ip_version str / int curl -4 / -6:"4" / "6"(4、"v4"、"ipv4" 同义),限定解析与拨号族;命中的地址全被过滤时是明确的解析错误
verify bool / str / bytes / Path True(默认,系统信任库)/ False(跳过校验)/ CA bundle 路径、含 .pem/.crt/.cer 的目录、或内联 PEM(自持信任库,替换系统根)。其他类型直接 ValueError,不会静默退回系统根
cert str / bytes / Path / tuple mTLS 客户端证书:路径或 PEM(证书+私钥可同文件)、(证书, 私钥);三元组的 password 必须为 None(不支持加密私钥)
cert_key str / bytes / Path 单独给私钥(覆盖 cert 元组第二项)
allow_redirects bool False ⇒ 不跟随重定向(会话级;请求级也可传,请求级覆盖会话级)
max_redirects int 重定向上限(默认 10;会话级与请求级都可用)
cookies dict / list[tuple] / str / Cookies / bool dict 等形态 ⇒ 会话级 jar(每请求渲染成 Cookie 头,响应里的 Set-Cookie 自动并入);True/False ⇒ 只开关引擎 jar
auth tuple / 对象 (user, password) 或带 username/user + password 属性的对象 ⇒ HTTP Basic;自己传了 Authorization 头时以你的为准
protocols list[str] 允许的 HTTP 协议集合(顺序无关):"h1.1" / "h2" / "h3"。默认 ["h1.1","h2"],不含 H3(与 curl_cffi 同口径)。只要 ["h1.1"] ⇒ ALPN 收窄到 http/1.1;只要 ["h2"] ⇒ 对端不支持就失败(不静默回落);只要 ["h3"] ⇒ 会话级强制 H3
h3 bool protocols 的便捷写法:True = 默认集合加 "h3"(预设需带 http3 声明)。h3=False 与 protocols 同给不报错,h3=True 与 protocols 同给报错
header_order str 请求头顺序:"preserve"(默认,按 profile.http1.header_order)/ "input"(按你传入的顺序)/ "random"(打乱,Host 仍最前)。⚠️ random 与真浏览器不符且破坏可复现性
identity_sync str "auto"(默认):你自带 user-agent 与预设身份不一致时,自动把 sec-ch-ua / sec-ch-ua-platform / sec-ch-ua-mobile 校正到该 UA,并在 r.warnings 里如实说明"TLS/H2 仍是该预设";"off" = 不校正
**options — 其余按引擎会话原名透传(proxy/proxy_from_env/timeout_ms/read_timeout_ms/redirect_max/insecure_skip_verify/auto_decompress/cookie_jar/ca_bundle/client_cert/client_key/resolve/local_address/ip_version);不在名单里的键绑定层直接 ValueError
方法 说明
request(method, url, *, params=None, data=None, json=None, headers=None, body=None, timeout=None, timeout_ms=None, read_timeout=None, read_timeout_ms=None, proxy=None, proxies=None, cookies=None, auth=None, allow_redirects=None, max_redirects=None, stream=None, force_http3=None, **kwargs) 通用请求;头到达即返回,body 按需读取。proxies / cookies / auth / allow_redirects / max_redirects 请求级覆盖会话级
get / post / put / patch / delete / head / options(url, **kwargs) 便捷动词,等价于 request
websocket(url, headers=None, timeout=None, compress=False) 升级 WebSocket(见下节),返回拉模型 WebSocket;compress=True 握手 offer permessage-deflate
close() 释放会话(挂连接池/transport)
with Session(...) as s: 自动 close()

⚠️ verify / cert 是会话级:传给 request() 会明确报错而不是静默失效(要按请求切换就新建 Session)。allow_redirects / max_redirects / proxies / cookies / auth 请求级也能用。

⚠️ 拼错的关键字不会静默吞掉:core 的两处 JSON 解码都不严格(client config 只查指纹入参那六个键,SessionOptions 忽略未知字段),所以名单校验做在绑定层——Session(...) 收到未知会话选项、request() 收到未知请求级 kwargs 都立即报错(Node 侧同理:new Session({insecure_skip_verfy: true}) 抛 TypeError)。这类选项决定"校验开不开、超时走不走、连接钉到哪",绝不能配了却没生效。

Response

成员 类型 说明
status_code / status int HTTP 状态码(status 是 0.1.0 起的旧名,等价)
ok bool < 400 为 True(requests 语义);对象真值即 ok
reason str 本地按 RFC 9110 映射的短语(非服务端原文)
headers CaseInsensitiveDict 大小写不敏感;保留插入原始大小写
raw_headers list[tuple] 原始 (name, value) 顺序视图
used_protocol str 实际协商到的协议:h2 / h3 / http/1.1
selfcheck dict 本次握手的自算指纹与比对(见「自校验」)
url / method / elapsed / encoding / apparent_encoding — 请求元信息与编码推断
content / read() / bytes() bytes 读完并缓存 body
text str 按响应编码解码(未声明 charset 时自动探测)
json(**kwargs) Any 按声明 charset(缺省 utf-8,剥 BOM)解 content;不走 text 的探测链(省掉一次"猜编码再解码")
iter_content(chunk_size=65536, decode_unicode=False) / iter_bytes(chunk_size) / iter_lines(chunk_size, decode_unicode=False) 生成器 边收边处理大响应(iter_bytes 是旧名;decode_unicode=True 跨块多字节不会切坏)
header(name, default=None) str 取单个响应头
cookies Cookies 本响应 Set-Cookie 的解析结果(requests 同名);session.cookies.update(r.cookies) 可让它参与后续请求
raise_for_status() None >=400 抛 HTTPError(GeekTLSError 子类,异常上挂 .response;code="http_error")
close() None 释放响应句柄(未读完 body 时更要显式调用)
content_encoding str 线上 Content-Encoding 原值(不篡改,与 requests 一致)
decoded bool 本次响应是否被引擎透明解压过
warnings list[str] 解压层非致命提示(未知编码原样透传等)

WebSocket(wss:// 与 ws://,RFC 6455)

wss:// 握手走同一条拨号 + TLS 指纹链路;ws://(明文)只做 TCP —— 帧层与 TLS 无关, 两条路径共用同一套帧实现,差别只在拨号那一步(与 http:// 同一条路)。 wss 的 ALPN 收窄到 http/1.1(Upgrade 在 h2 上无效;收窄的是 detail 副本里的 ALPN 扩展内容, 不只改本地偏好;明文 ws 没有 ALPN)。 Upgrade 请求头序默认对齐真 Chrome(host → connection → upgrade → origin → 身份头 → sec-websocket-version → sec-websocket-key),profile.http1.header_order 非空时整体再过排序器。 帧层为自行实现(stdlib,无新依赖):text/binary/ping/pong/close、客户端 masking(RFC 强制)、 分片重组、控制帧 125 字节上限、ping 自动回 pong。 permessage-deflate(RFC 7692)已实现:compress=True 让握手 offer permessage-deflate, 只有对端在 101 里按我方 offer 真的接受才启用——之后 send/recv 收发的仍是明文 (RSV1 位、跨消息压缩上下文、分片续接都在库里管理)。默认 False 不发这个头: 没有逐浏览器 WS 握手的字节级证据,就不承诺 Chrome 那个 client_max_window_bits 参数 (stdlib 的压缩窗固定 15 bit)。要精确控制 offer 参数(server_no_context_takeover 等) 直接把它写进 headers。

with Session(impersonate="chrome_133") as s:
    ws = s.websocket("wss://ws.postman-echo.com/raw")
    ws.send("hello")                          # text 帧
    opcode, payload = ws.recv(timeout=8)      # (1, b'hello');1=text 2=binary
    ws.close()                                # 或交给 Session 的 with 统一释放

    # 想要服务端按 RFC 7692 压缩载荷(收发仍是明文):
    ws = s.websocket("wss://example.com/socket", compress=True)
  • 拉模型:send(data)(str ⇒ text 帧,bytes ⇒ binary;Python 可 send(data, opcode) 显式指定)/ recv(timeout=None, buf_size=1<<20)(秒)/ close(code=1000),不做回调式。
  • Node:const ws = await session.websocket(url, { compress: true }) → await ws.send(data) / ws.recvText(timeoutMs) / ws.close();asyncio 同款(AsyncSession.websocket)。
  • 协商严格按 RFC,不做"看起来能用再说":对端回声我方没请求的参数、协商了不认识的扩展、 把我方压缩窗限到 <15 bit,握手直接失败;没协商却收到 RSV1(以及 RSV2/RSV3、带 RSV1 的 控制帧)也直接报错——"把压缩字节当文本返回"是绝不允许的失败模式。
  • 不做 RFC 8441(WebSocket over H2):Chrome 实际仍以 H1 Upgrade 为主(capability-matrix ws.rfc8441_h2 登记)。
  • 已验证:本地 echo-server 双向收发/分片/控制帧(pytest test_websocket、node websocket)+ 公网 wss://ws.postman-echo.com/raw 真实握手与回显;permessage-deflate 用 core/engine/ws_deflate_test.go 里独立写的 stdlib flate 对端(真 TLS 监听)双向验证——发方向的压缩块拼起来交给普通 flate.NewReader 整体解开,证明是合法连续 DEFLATE 流而非自家私有格式。

响应透明解压(默认开)

引擎按响应 Content-Encoding 自动解压 body:gzip / deflate(zlib 头 + raw flate 容错)/ br / zstd,多重编码链(如 gzip, br)按声明顺序逆序解开;全链路流式生效—— r.content / r.text / r.json() / iter_* 拿到的都是解压后的字节(charset 探测在解压之后)。

  • 关闭:Session(auto_decompress=False),或请求级 request(..., auto_decompress=False)(请求级覆盖会话级)。
  • headers 保留线上原值:Content-Encoding / Content-Length 不篡改;解压状态看 r.decoded / r.content_encoding。
  • 未知编码值原样透传不报错,记入 r.warnings。
  • H3 路径同样生效(解压统一收归引擎;vendor fhttp 的内置单层解压已用 SkipResponseDecompress patch 关闭,杜绝双重解压)。

Node.js API 参考

const { Session, version, listPresets, describePreset, checkProfile,
        lastError, errorOf, GeekTLSError,
        request, get, post, del, options, defaultSession } = require('geektls');  // 后五个:requests 风格模块级快捷 API
成员 说明
new Session(options) options 同 Python:指纹入参六选一 + proxy/proxies、proxyFromEnv/trustEnv(false = 不读 HTTPS_PROXY/ALL_PROXY)、timeoutMs/timeout、readTimeoutMs/readTimeout(秒,body 读取空闲上限)、redirectMax/allowRedirects、cookieJar、insecureSkipVerify/verify(true/false/CA bundle 路径或 PEM)、cert(路径 / [证书, 私钥] / {cert, key})+ certKey、headers、地址控制 resolve({'host[:port]': ip})/localAddress/ipVersion。也认引擎原名(timeout_ms/insecure_skip_verify/resolve…);两个名单都不在的键抛 TypeError,不会静默消失
s.request({method, url, ...}) 返回 Promise<Response>(不阻塞事件循环);请求级可给 timeoutMs/readTimeoutMs/proxy,以及 allowRedirects:false(⇒ 引擎 redirect_max=-1)
s.get/post/put/patch/delete/head/options(url, options) 便捷动词
await s.websocket(url, { headers, timeoutMs, compress }) WebSocket(拉模型:send / recv / recvText / close);compress:true 握手 offer permessage-deflate
s.close() 释放会话
r.statusCode / r.ok / r.reason / r.headers / r.usedProtocol / r.selfcheck 同 Python 语义
r.contentEncoding / r.decoded / r.warnings 解压元信息(同 Python)
await r.read() / r.bytes() / r.text() / r.json() / r.header(name) body 读取
r.iterContent(size) / r.iterLines(size) 异步迭代器(for await)
r.close() 释放
version() / init() / lastError() / listPresets() / describePreset(name) / checkProfile(input) 与 Python 同名同语义
errorOf(handle) 按对象取最近一次错误(gtls_error_of;handle 查不到再回落 lastError())。异步绑定在 worker 线程报的错靠它取回
cookie Node 侧没有 Python 那样的显式 jar API:线上 Set-Cookie 的存取与重发由引擎 jar 自动完成(含重定向中间跳);要带指定 cookie 就写 Cookie 头

Go API 参考

两条路:绑定层(github.com/geektls/golang,http.Client/RoundTripper 形态)与引擎直连(core/engine,能力最全)。

包 关键 API 说明
core/profiles List() / Get(name) / Describe(name) / FromJA3 / FromJA4R / FromClientHelloHex / NormalizeForReplay / Parse 预设与指纹入口(含上面说的自洽归一)
core/tls CompileDetail / ComputeJA3 / ComputeJA4 / JA3Hash / CheckProfile / ResolveJA4Preset 编译、自算、离线自检、JA4 反查
core/engine NewSession(profile, SessionOptions) → Do(*Request) → *Response(StatusCode/OK/Reason/Header/Text/JSON/IterBytes/Close);DialWS(*WSRequest) → *WSConn(Send/Recv/Close,WSRequest.Compress 开 permessage-deflate) 引擎直连;支持 H2/H3 racing、Alt-Svc、代理、流式、wss
github.com/geektls/golang NewSession(preset, *Options) → Do/Get / DialWS(*WSRequest) → *WSConn / Close();NewRoundTripper(preset, *Options) 绑定层(不走 FFI,直接 import core);WSRequest/WSConn 是 engine 类型的别名,不再包一层以免漂移
core/ffi gtls_* C ABI(见 core/ffi/geektls.h) Python/Node 绑定的底座;gtls_client_new 的 config_json 支持 impersonate/profile/ja3/ja4/ja4r/clienthello_hex

使用场景

1) 单请求(最常用)

with Session(impersonate="chrome_154_windows") as s:
    print(s.get("https://tls.peet.ws/api/all").json()["tls"]["ja4"])

2) 并发抓取(会话复用 + 连接池)

import concurrent.futures as cf
with Session(impersonate="chrome_150") as s:
    with cf.ThreadPoolExecutor(16) as ex:
        for r in ex.map(lambda u: s.get(u), urls):
            print(r.status_code, r.used_protocol)

3) 会话复用/票据(第二次握手带 PSK,selfcheck 里能看到)

with Session(impersonate="firefox_156_windows") as s:
    s.get("https://example.com").close()
    r = s.get("https://example.com")     # resumed(同 origin 连接复用)

4) 强制 HTTP/3 / H3 竞速

with Session(profile=geektls.describe_preset("chrome_154_macos")) as s:
    r = s.get("https://cloudflare.com", force_http3=True, timeout_ms=8000)
    print(r.used_protocol)               # h3(不支持则按策略回落 h2)

5) 走代理 / 关校验 / 不跟随重定向

with Session(impersonate="edge_153_windows", proxies={"https": "http://127.0.0.1:8080"},
             verify=False, allow_redirects=False) as s:
    print(s.get("https://example.com").status_code)   # 3xx 原样返回

代理支持六种 scheme:http/https(CONNECT 隧道,带 user:pass 即发 Proxy-Authorization)、socks5(本地先解析域名再交代 IP)、socks5h (域名原样交给代理解析)、socks4/socks4a(老协议:只有 IPv4 目标、 鉴位只有 user id 没有口令;4a 的域名由代理侧解析)。 socks5 遇到解析不出的域名会直接报错,不会静默降级成 socks5h。

不给 proxies 时读标准环境变量 HTTPS_PROXY → ALL_PROXY(大小写都认),并按 NO_PROXY 豁免;trust_env=False 可整条关掉(指纹调试要可复计时建议关)。 域名是字面量匹配,不做 DNS——真 Chrome 会先解析再判 NO_PROXY,geektls 故意不这么 做:为的是"同一个预设、同一个 URL 每次走同一条路"。 回环目标(localhost / 127.0.0.1 / ::1)不吃环境变量派生的代理,避免开发机 上挂了 ALL_PROXY 就连不上本地测试服务;显式给的 proxies 对回环仍然生效。

⚠️ 有代理时 H3/QUIC 一律不参与(含 racing 与 Alt-Svc 升级):QUIC 过 HTTP 代理 需要 CONNECT-UDP(RFC 9298),本库未实现。force_http3=True + 代理会明确报 invalid 而不是偷偷直发漏指纹——这是"要么按你说的路走、要么报错"的一般原则。

5b) 自持信任库 / mTLS(requests 的 verify=<path>、cert=(crt, key))

with Session(impersonate="chrome_150",
             verify="/etc/ssl/certs/internal-ca.pem",          # 路径 / 目录 / PEM 文本皆可
             cert=("/etc/client/tls.crt", "/etc/client/tls.key")) as s:
    print(s.get("https://internal.example.com/api").status_code)

CA 解析不出来、或证书与私钥配不上对时,建会话就报错(GeekTLSError, code=invalid_config),不会退化成"用系统根继续"。TCP(uTLS)与 QUIC/H3 内层 握手共用同一份证书材料。

5c) 不走系统 DNS:把域名钉到固定 IP / 绑源地址 / 限协议族

with Session(impersonate="chrome_150",
             resolve={"api.example.com": "93.184.216.34",   # 键也可以是 "host:443"
                      "cdn.example.com": "104.16.132.249"},
             local_address="192.168.1.23",                  # 出网走这张网卡/这个 IP
             ip_version="4") as s:                          # 只要 A 记录
    print(s.get("https://api.example.com/v1").selfcheck["ja4"])

这三项只改"连到哪、从哪连",一律不改线上指纹字节:SNI、Host、伪头、 ClientHello 用的还是 URL 里的原域名,所以钉位前后 JA3/JA4 完全一致(有测试守着)。 local_address 必须是本机持有的 IP——绑一个不属于它的地址会在拨号时明确报错, 不会"静默换一张网卡"。 和 netstack 组合时它是正向能力:用户态栈要求目标是 IPv4 字面量,resolve 正好 供给;源 IP 由 ip_version/resolve 的族隐含。

生效范围按"谁做解析"划分,不做静默改道:直连、tcp.mode=netstack、socks5、 socks4 由引擎本地解析 ⇒ 钉位/族收窄都生效;socks5h、socks4a、CONNECT 隧道把 域名原样交给代理 ⇒ resolve/ip_version 不参与(那两档生效就等于绕过代理语义)。 local_address 在 netstack 下报错(用户态栈源地址固定为 TUN 侧地址)而不是忽略。 取值互相冲突(ip_version="4" 配 IPv6 源地址、resolve 值不是 IP、键写成网卡名) 建会话即 invalid_config;force_http3 与三者同给也是明确报错——QUIC 拨号没接地址 控制,宁可失败也不"以为钉住了其实在乱走"。

6) 对比两个预设的指纹差异(不发包)

import json
a = geektls.check_profile(json.dumps(geektls.describe_preset("chrome_152_macos")))
b = geektls.check_profile(json.dumps(geektls.describe_preset("chrome_154_macos")))
print(a["ja4"], b["ja4"], a["wire_len"], b["wire_len"])

7) 自定义完整指纹(自己的 profile JSON)

mine = {
    "name": "mine_chrome_like",
    "tls": {"detail": {
        "legacy_version": "0x0303",
        "ciphers": ["grease", "0x1301", "0x1302", "0x1303", "0xc02b", "0xc02f"],
        "extensions": [
            {"type": 0, "sni": "auto"},
            {"type": 16, "alpn": ["h2", "http/1.1"]},
            {"type": 43, "versions": ["grease", "0x0304", "0x0303"]},
            {"type": 10, "groups": ["grease", "X25519MLKEM768", "X25519"]},
            {"type": 13, "sig_algs": ["0x0403", "0x0804", "0x0401"]},
            {"type": 41},                       # 空占位:会话复用要求它在末尾
        ],
    }},
    "http2": {"hpack_strategy": "chrome", "settings": [[1, 65536], [2, 0], [4, 6291456], [6, 262144]],
              "window_update": 15663105,              # 三态:不写=补默认 15663105 / 0=不发这帧 / N=发 N
              "first_stream_id": 1,                   # 客户端首流号(正奇数);不写=1,真 Firefox 用 3
              "pseudo_header_order": ["m", "a", "s", "p"]},
}
geektls.check_profile(json.dumps(mine))          # 先离线自检:看 warnings 与 JA3/JA4
with Session(profile=mine) as s:
    print(s.get("https://example.com").selfcheck["ja4"])

8) 从抓包迁移:JA3 / JA4R / 原始 hex

with Session(ja3="771,4865-4866-4867,43-10-11-13-16-23-5-0-65281,4588-29-23-24,0") as s:
    r = s.get("https://example.com")             # 其余字段按引擎默认补齐
r = geektls.check_profile("t13d1516h2_8daaf6152771_d8a2da3f94cd")   # 反查内置预设

9) 抓包 → 预设(把线上真实指纹固化成文件)

import json, pathlib
r = Session(impersonate="chrome_150").get("https://tls.peet.ws/api/all")
cap = r.json()
spec = {  # 只保留指纹相关字段(tcp 节按宿主平台另配)
    "name": "my_captured_profile",
    "tls": {"detail": {"legacy_version": "0x0303", "ciphers": ["grease", "0x1301"], "extensions": []}},
    "http2": {"akamai": cap["http2"]["akamai_fingerprint"]},
}
pathlib.Path("my_profile.json").write_text(json.dumps(spec, ensure_ascii=False, indent=2))

更规范的做法是走仓库的 tests/e2e/cmd/gen-profiles(从原始 ClientHello 字节生成,落 core/profiles/builtin/ 并带证据等级)。

10) 回归/自校验(把指纹钉死在测试里)

JA4 = "t13d1517h2_8daaf6152771_cb7bf5808d99"
with Session(impersonate="chrome_154_windows") as s:
    assert s.get("https://example.com").selfcheck["ja4"] == JA4

预设体系

  • 数量与命名:368 条,core/profiles/builtin/<name>.json,文件名即预设名,规范是 <家族>_<版本>[_<变体>][_<平台>]:chrome_154_windows、firefox_156_android、okhttp_3_12_12。
    • 平台后缀能判定就必须写(windows/macos/linux/android/ios),判不定的留空(工具/App 族);
    • Safari 也带 _macos——它同时有 macOS 与 iOS 形态(safari_18_6_ios),后缀只允许 _macos / _ios(已按此把 safari_16 → safari_16_macos、safari_18 → safari_18_legacy_macos);
    • 旧名是别名:改名过的预设(chrome_131/133/150、firefox_120/135 → *_windows;Safari 两条同上)旧名继续可用, 取到同一份形态;映射表只增不删,新代码用规范名。
    • 仍然没有族级短名/前缀匹配:impersonate="chrome_154" 会报 preset not found,要写全名。 完整规范与理由见 docs/03-profile-format.md §3.1(守门测试 core/profiles/naming_test.go)。
  • 字段:tls.detail(ciphers/扩展与负载/GREASE 策略)、http2(SETTINGS/window_update(三态)/伪头序/first_stream_id/hpack_strategy/headers_priority)、http3(transport params/inner hello 规约)、identity(导航头集合与顺序)、可选 tcp、grade、source。
  • 证据分级(grade 字段,详见 docs/09-alignment-and-superiority.md):
    • 留空 = 本项目自测(E1 真机抓包 / E1r 字段级实测 / E2 本机可复现)→ 参与 E1 级断言;
    • E2i / E2i-u = 谱系内插(实测锚点 + 逐字段稳定性规则,-u 表示区间内有变化且边界未知);
    • E3 = 外部指纹集导入(只补覆盖,不做保真承诺)。
    • 4 个 E1 级测试(fp oracle / Chromium GT / Firefox GT / TLS1.2 回退)一律跳过 grade != "" 的预设——没实测的不参与"实测"断言。
  • 怎么挑:要稳就挑 grade 为空、且与目标族/版本/平台都对的(describe_preset() 看全貌);只求覆盖面可用 E3。
  • 怎么加:见 docs/maintenance.md 的"预设入库流程"。由 gen-profiles 生成的预设禁止手改(CI 逐字节比对守门)。

自校验

每个响应带 selfcheck:实际发出 ClientHello 的自算 JA3/JA4 + JA3 fullstring、线上扩展序(含/剔 GREASE 双份)、GREASE 值与位置、协商 cipher/版本/ALPN、SNI 是否上链(IP 目标自动报 i);JA3/JA4R 入口还含期望值比对;gtls_check_profile 支持 profile JSON / JA3 / JA4 / JA4R / hex 五入参离线自检。

身份层也有守门(core/profiles/builtin_identity_test.go):任何预设都不得带 headless 令牌,且 UA 与 sec-ch-ua 的平台/版本必须自洽(Windows NT ↔ "Windows"、Macintosh ↔ "macOS"、Chrome/Edg 主版本 ↔ "Chromium";v="N")。

性能摘要(docs/benchmarks.md)

  • FFI 调用开销:gtls_version 6–7µs、gtls_check_profile 26–30µs(<50µs 目标达成)
  • 回环吞吐(连接池开启,并发 100,10s):共享 Session(同 origin 单连接,浏览器真实形态)≈5–9k req/s;每 worker 会话模型 Go 12.7k / Python 9.4k / Node 8.9k req/s
  • 冷启动:Session 初始化 0.2ms、首请求 ~3.5ms

与同类项目对比

对比依据(2026-09-29 复核):逐仓拉取下列项目 README 原文核对能力声明,结合 tls-spoofing-library-matrix 与 docs/10-ecosystem-comparison.md。 三点口径:① akamai/uls 实为 Akamai 的日志流 SIEM 工具,不是指纹库,已从对比剔除 (此前表格误列);② cyCronet 的地址是 2833844911/cyCronet (Python + Chromium Cronet 真栈)——此前按名字猜 slug(cycronet/cycronet)查不到就写"仓库不存在", 是错的:查不到 ≠ 不存在,已补回对比行;③ bogdanfinn/tls-client 的 README 本次拉取失败 (raw 与镜像均 404),其能力按既有文档与本项目 E3 导入记录描述。单元格 — 表示公开材料未说明 该维度(不等于没有)。

完整客户端(TLS / H2 / H3 伪装客户端)

项目 栈 / 语言 TLS 指纹 H2 帧 + 头序 H2 HPACK 策略 H3 / QUIC 四层 TCP WebSocket 预设与证据 响应内自校验
geektls Go(c-shared)+ Python / Node / Go 绑定 ✅ uTLS fork + E1 真机采集链路 ✅ ✅ 四档(generic/chrome/firefox/safari),253/368 预设带值 ✅ 内层 ClientHello 同 profile + transport params blob 直通;Initial 布局不可控(SC-3 解锁) ✅ TTL/MSS/DF/window/wscale:setsockopt 三平台 + netstack 档(Linux root,gVisor 栈) ✅ RFC 6455 + permessage-deflate(握手走指纹链路) ✅ 368 条 grade/source 分级:29 自测 / 6 E2i / 8 E2i-u / 325 E3 ✅ selfcheck + check_profile 五入参
bogdanfinn/tls-client(hrequests、noble-tls、tls-client-sharp 等绑定) Go fhttp + utls ✅ ✅ — ✅ — — 自带 profile 集;与本项目 E3 覆盖(320 条)无直接来源关系——E3 的 source 逐条指向第三方快照 profiles/evidence/thirdparty/tls_config-0.0.2(TestE3SourceTraceable 守门),profile 组织形态是同类参照 —
lexiforest/curl_cffi(活跃)/ lwthiker/curl-impersonate(原始) libcurl 补丁 + BoringSSL / NSS ✅ ✅ — ✅(curl_cffi 新版起) ❌(libcurl 无 TCP 指纹面) ✅ 内置画像 + 自定义指纹;社区节奏最快 —
Danny-Dasilva/CycleTLS、cycletls_python Go utls + fhttp + quic-go ✅(JA3 可配置) ✅(fhttp 头序) — ✅ — ✅ profile 清单;socks4/5/5h —
sardanioss/httpcloak Rust 栈,Python/JS/C# 绑定 ✅ ✅(WINDOW_UPDATE/priority 可调) 部分(priority 侧) ✅(H3 GREASE 帧) ✅(TTL/MSS/Window,README 声明) — 内置 preset + "抓一次用到底"(JA3+Akamai 粘贴导入) —
jaredboynton/specter Rust + 自研 H2(RFC 9113)/ H3 ✅ Chrome 142–148、Firefox 133–151+ESR ✅(记录分帧/帧时序/池行为) — ✅ — ✅ 内置 profile + UA-CH 算法文档 —
zhkl0228/impersonator Java / kwik(QUIC) ✅ ✅ OkHttp 形态 — ✅ 含 QUIC Initial 布局(分片/乱序/PADDING 按浏览器拟真) — — — —
sqdshguy/wreq-js Node(Rust wreq 内核) ✅ ✅ Akamai — — — ✅(WHATWG 风格 + 会话复用) Chrome 至 149 / Firefox 151,声明"live capture 验证" —
daijro/hrequests Python(tls-client 内核) ✅ ✅ — — — — requests 风格 + BrowserForge 头生成 + 浏览器自动化 —
2833844911/cyCronet Python + Chromium Cronet 真栈(libcronet.so) ✅ 真栈(默认 chrome_144,tls_profiles.json 可自定义) ✅ 真栈 —(真栈内置,策略不可控) —(README 未见 H3/QUIC) ❌ ✅(WSS,TLS 指纹一致) chrome_144 + 自定义 json;SOCKS5 带鉴权 ❌(无自校验回读)
deedy5/primp、wangluozhe/requests-go、z402166914/pyhttpx、tocha688/curl-cffi-node 各语言封装 ✅ 依赖上游 ✅ 依赖上游(requests-go 另有 JA3 扩展随机化) — 部分 — 部分 — —
thesatellite-ai/fetchr Go + CLI + MCP server ✅(JA3/JA4) ✅ — ✅ — — 浏览器画像 —

采集 / 检测 / 代理(对照面,不是竞品)

项目 作用 与 geektls 的关系
gospider007/fp 独立 ClientHello 解析器 E1 采集链路直接使用(tests/e2e/cmd/e1-browser)
FoxIO-LLC/ja4、Crank-Git/ja4plus-go、XOR-op/ja-tools JA3 / JA4 / JA4+ 参考实现 JA4 规范与测试向量来源;只依赖 JA4 本体(JA4+ 部分方法有许可证限制)
wi1dcard/fingerproxy、LyleMi/ja3proxy、O-X-L/haproxy-ja4-fingerprint 反向代理 / 网关算指纹并透传 输出形态对照;selfcheck 做在响应里,不依赖网关
jaeles-project/gospider、gitee baixudong/gospider、Ecalose/gospider 爬虫 / 工具链(同名不同项目) 爬虫侧用法参照
tlsmask/tlsmask、Easonliuliang/helloprint 指纹伪装 / 一致性工具 UA ↔ TLS 一致性思路对照;落成身份层守门测试

上表里 geektls 的差异点

  1. 四层一起:TLS + H2(含 HPACK 编码策略四档)+ H3 / QUIC(内层 ClientHello 同 profile)+ TCP(TTL/MSS/DF/window/wscale:setsockopt 三平台 + Linux netstack 档)。上表其他客户端多数止步 TLS / H2;四层 TCP 只有 httpcloak 声明了同档能力。
  2. 证据分级:grade(30 自测 / 6 E2i / 8 E2i-u / 320 E3)+ source 守门 + E1 真机采集链路 + 语料回归 + 外部 oracle 周检;同类普遍只给一份清单,不区分实测与转写。
  3. 响应内自校验:本次握手实际发出的 JA3 / JA4 / 扩展序 / GREASE 值直接从响应取,可当回归断言;check_profile 支持五种入参离线自检。
  4. 三语言同引擎:Python / Node / Go 共用同一 C ABI,跨语言 JA4 三方全等,不需要为每种语言重写指纹栈。
  5. 预设结构:368 条按 grade 分层(29 自测可参与严格断言 + 6 E2i / 8 E2i-u 谱系内插 + 325 E3 导入),覆盖浏览器 / App / 工具 / 代理等 29 族,含跨平台同版本一致性断言(chrome_152 / chrome_154 在 macOS / Android / Windows 上 JA4 逐字符相同)。
  6. 部署形态:Go 实现、CGO 只用于构建动态库,产物是单文件动态库 + 平台 wheel,无需 libcurl 补丁链。
  7. 自主化路线:指纹相关代码路径正向 100% 自有推进(uTLS / fhttp / quic-go-utls 内化裁枝四阶段,见 docs/plans/2026-09-29-self-contained-roadmap.md);密码学原语(circl / brotli / zstd)按行业共识保留成熟实现,不自写。
  8. 能力面补齐:WebSocket(wss / ws,握手走指纹链路)+ 明文 http:// + Python asyncio + 四编码自动解压 + 命令行入口 + requests 语义面(cookie jar / timeout 元组 / auth / 重定向开关 / 表单编码),与 CycleTLS / curl_cffi / noble-tls 的能力清单逐项对齐(逐库对照见 docs/10-ecosystem-comparison.md)。

T-HPACK 四档的证据来源:chrome 档对齐 Chromium QUICHE HpackEncoder 的默认策略(源码级), firefox 档对齐 Firefox 59 抓包(字节级),safari 档为保守近似(待 E1 校验);上游 x/net 编码器 "一切皆可入动表"的行为与前三者都不同。

相对短板(同一张表下的客观差距)

维度 差距
生态与熟悉度 远不如 curl_cffi / curl-impersonate:Python 迁移成本、示例与社区最少;没有 hrequests 那种浏览器自动化集成
真栈行为级 curl_cffi(libcurl + BoringSSL)与 cyCronet(Chromium Cronet 真栈)的行为级细节天然全真(含握手重试、协议栈内部实现);这里靠逐维度对齐逼近,理论上存在未发现的行为差异
预设更新 依赖人工采样(SLA 2 周);大量预设是 E3 转写而非实测,curl_cffi / curl-impersonate 靠社区补丁更快
H3 可控面 QUIC 首飞全控:首 datagram 填充下限(initial_packet_size,1200–1452,不设=1200)、SCID 长度(connection_id_length,0 = Chrome 形态空 SCID)、PADDING 位置、CRYPTO 分片表、scrambling 开关、coalesce 阈值(initial_layout,vendor patch #8/#10);transport params 顺序靠 blob 直通;Firefox / Safari H3 未实测
Safari 侧 HPACK 的 safari 档是全 literal 保守近似;padding 用实测字节数表达;无 CFNetwork 字节级证据
四层 Windows 仅 setsockopt 档(TTL/MSS/DF,MSS 项缺失);netstack 档(window/wscale 精确控制)仅 Linux root;IP ID / TSval 不可控
发布面 npm 与 Go module 尚未发布;wheel 只覆盖 5 平台(无 musllinux / Windows ARM64)
依赖 fork fhttp、quic-go-utls 是 vendor fork,升级上游要重打 patch 并跑回归(流程已登记,仍是维护成本)
有损入口 JA3 / JA4 入参天然丢负载,此处选择"如实告警"而不是"看起来像",在只求"JA3 过检测"的场景不是最省事的路径

自主化进度(docs/plans/2026-09-29-self-contained-roadmap.md): 边界声明与依赖清单已完成;gvisor.dev/gvisor(netstack 档)已引入;core/internal/ 目前只有 registry ⇒ SC-1(uTLS 内化改写)/ SC-2(fhttp 裁枝内化)/ SC-3(quic-go-utls 内化)均未开始, "指纹路径 100% 自有"的终态尚未达成。不变量:ABI 签名只增不改、368 预设指纹输出逐比特不变、 每阶段全量回归 + L2 nginx 终审。

能力对齐现状与追赶排期(差距清单 + 关闭判据 + 明确不做的事)见 docs/plans/2026-09-29-parity-and-improvement-plan.md: 指纹能力面已对等或领先。形态面已补齐(明文 http:// / ws://、CLI、cookie 手感、 requests 语义面);剩余差距集中在发布面(Node/Go 未发布、平台不全)与两条结构性差距 (QUIC Initial 的 coalesce / 分片表 = SC-3、真栈行为级 = 不可追平)。

已知边界与不足(如实)

边界 说明
QUIC 内层 ECH 与 bogdanfinn 服务端 其 QUIC server 不处理 ECH-in-QUIC 会静默失败(客户端侧已合成 Chrome 等效 payload;真服务器按规范忽略 GREASE ECH)
QUIC Initial 布局 已全控(vendor patch #8/#10):PADDING 位置、CRYPTO 分片表、scrambling 开关、coalesce 阈值(initial_layout)、首包填充下限(initial_packet_size,不设=1200,不是"精确尺寸")、首飞 SCID 长度(connection_id_length,0 = Chrome 形态空 SCID;此时 initial_source_connection_id 才允许空值)。逐字节复刻真机首包 1230B 还需 CH 尺寸与分片表一致(属 profile 侧保真,不在布局旋钮内)
明文 http:// 的边界 只走 H1(不做 h2c,无 prior knowledge / Upgrade 路径);经 HTTP 代理时走 CONNECT 隧道(不发 absolute-form GET http://…);明文没有 TLS 层 ⇒ selfcheck 恒为零值、force_http3 + http:// 直接报错
代理环境变量按 scheme 取 https/wss 读 HTTPS_PROXY、http/ws 读 HTTP_PROXY,都不跨 scheme 取(curl/requests 同语义);都没命中再退 ALL_PROXY
QUIC transport params 顺序/非标参数 已经 T4-1 blob 直通可控(顺序/值硬断言全绿);个别残余值与 blob 整块替换冲突,待 fork 决策(能力矩阵 H3-7)
Windows TCP MSS 不支持(WSAENOPROTOOPT 实测),跳过+warning;raw socket 档仅 Linux 探测模式
uTLS PSK 复用 vs Go std 服务端 binder 校验失败(上游 interop,发出侧已证正确)
JA4 短哈希入参 哈希不可逆 ⇒ 只能反查内置预设;想精确复刻必须给 JA4R 或完整 profile
nginx L2 终审 ✅ 已通过(WSL2,35/35);JA4TCP 验收仍需真实二层网络

开发与发布

make build                     # 构建动态库到 build/
make cli                       # 构建命令行入口到 build/geektls
make smoke                     # Go/Python/Node 三语言冒烟
cd core && go test ./...       # 引擎全量(含 E1/E2 门禁、语料回归)
cd tests/e2e && go test ./...  # e2e:生成器守门、HPACK/H2 回放、loopback、跨语言
GEEKTLS_ORACLE_PRESETS=chrome_154_* GEEKTLS_ORACLE_ASSERT=1 \
  go test -tags external -run Oracle -v   # 外部 oracle 对拍(子集 + 不一致即失败)
wsl -d Ubuntu -- bash scripts/wsl-test.sh   # Linux 侧验证(WSL,Go 自动装到 ~/.local)
  • 发布:docs/versioning.md(语义化 + ABI 规则 + 三处版本同步);PyPI 流水线 .github/workflows/release-pypi.yml(五平台 wheel)、CI .github/workflows/ci.yml(含"预设必须等于生成器产物"逐字节守门)。
  • nightly 实测:.github/workflows/oracle-nightly.yml — 外部 oracle(JA3/JA4 + Akamai H2)以断言档跑选定子集、ECH 与真机会话恢复(resumed=true 是硬门禁)、Go/Python/Node 三语言一致性与两套绑定 e2e(Linux + Windows 真构建动态库)。子集由 GEEKTLS_ORACLE_PRESETS 控制,手动 dispatch 可临时加新预设;每周另有 .github/workflows/preset-freshness.yml 比对内置预设与 Chrome/Firefox 官方渠道版本,落后即开 issue。
  • 预设维护:docs/maintenance.md(Chrome 2 周 SLA、入库五步、oracle 周检、fork rebase)。

许可证

本项目自身按 MIT 授权(见 LICENSE,2026-09-29 定)。

发布物(wheel / npm 包 / Go module / 编译进动态库的依赖)内含第三方代码,各依赖的许可证与 归属声明登记在 LICENSES.md 并随 wheel 一起分发:全量闭包审计无 GPL/LGPL; 两个如实登记的注意项是上游 fhttp 无显式 LICENSE(仅"无授权使用"风险,行业生态同此依赖) 与 JA4+ 的许可证/商标边界(只依赖 JA4 本体,见上文对比表)。

文档索引

文档 内容
CHANGELOG.md 各版本更新内容(0.1.5 起)
docs/00-architecture.md 技术选型、模块划分、FFI 模型
docs/01-fingerprint-dimensions.md 四层指纹维度清单
docs/02-ffi-abi.md C ABI 契约
docs/03-profile-format.md profile JSON schema + 预设体系
docs/06-task-list.md 任务计划与完成状态
docs/07-capability-gaps.md 能力缺口与实测记录(含族横向对照)
docs/08-plan-pending-samples.md 待补采样与后期扩充计划
docs/09-alignment-and-superiority.md 证据分级与同类对比(内部版)
docs/capability-matrix.yml 机器可读能力矩阵(CI 断言/变量映射共用)
docs/CONTRACT-FREEZE.md 已验证面冻结契约(防"改而改弱")
docs/plans/2026-09-24-geektls-hardening-and-h3-plan.md 硬化与 H3 补齐方案(2026-09-24)
docs/p1-utls-capability.md / p2-h2 / p4-h3 各层能力摸底矩阵
docs/11-lowlevel-forgery.md TLS 以下的指纹伪造:分层可行性、一致性红线、参考仓库
docs/12-pcap-import.md pcap 抓包 → 装载:分层可见性、五个坑、import-pcap 设计与端到端实测
docs/tcp-platform-matrix.md TCP 指纹平台边界
docs/benchmarks.md 性能基准
docs/versioning.md / docs/maintenance.md 版本与运营机制
profiles/evidence/README.md 预设证据登记
LICENSES.md 依赖许可证审计
docs/10-ecosystem-comparison.md 生态对比与差距分析(33 库逐维对照)
docs/plans/2026-09-29-self-contained-roadmap.md 自主化路线图(脱离第三方指纹栈)

非目标

  • 不做服务端指纹采集。
  • 不做浏览器级 JS 环境模拟(Canvas/WebGL 等)。
  • 不内置任何绕过具体站点防护的"开箱即用"策略——只提供精确的指纹原语。

Metadata

Release files for geektls 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for geektls 0.1.8
File
geektls-0.1.8-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
geektls-0.1.8-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
geektls-0.1.8-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
geektls-0.1.8-py3-none-macosx_11_0_x86_64.whl Python 3 none macOS 11.0+ x86-64 Details
geektls-0.1.8-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 25.3 MB

Release files / geektls-0.1.8-py3-none-win_amd64.whl

Download URL geektls-0.1.8-py3-none-win_amd64.whl
Size 4.8 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
8b95d1478e9ea8164f42d1a12bb03019f35062918c27e14393de9c463e55fbca
BLAKE2b-256 checksum
How to use checksums
67a9485ef322d00b90a8c787158184fa2ebe2415d269cb84deceaa3b29484bac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / geektls-0.1.8-py3-none-manylinux_2_28_x86_64.whl

Download URL geektls-0.1.8-py3-none-manylinux_2_28_x86_64.whl
Size 5.9 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
98bf0a507990b10f86d8ea3a287036d342ee541618706a024e7527e8c016bbe7
BLAKE2b-256 checksum
How to use checksums
757453738acf84dfe589fbbda1e6665ad23d52944507681eb84efffddc694a71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / geektls-0.1.8-py3-none-manylinux_2_28_aarch64.whl

Download URL geektls-0.1.8-py3-none-manylinux_2_28_aarch64.whl
Size 5.4 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b6eaa15293ecc79bfe89a45d528fdae87ab6b8c91f45bca8f9536a2e378ffd86
BLAKE2b-256 checksum
How to use checksums
229760c3970f0b0ff770a92aa393bda0cbafc764ddf33be366caaceabac223a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / geektls-0.1.8-py3-none-macosx_11_0_x86_64.whl

Download URL geektls-0.1.8-py3-none-macosx_11_0_x86_64.whl
Size 4.8 MB
Tags Python 3 macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
3c3ce732094e55ae0a1eeba75ef145ad9ff4cbbc1429437d2598e70225fe2058
BLAKE2b-256 checksum
How to use checksums
675e77c9eed3f0af71dba9c9c93952478648bb6eb99fb4a51e692b35f99d0ed8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / geektls-0.1.8-py3-none-macosx_11_0_arm64.whl

Download URL geektls-0.1.8-py3-none-macosx_11_0_arm64.whl
Size 4.4 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e54ac309e34fb6bb1def9f0f0f854fac7c2610458570658c5efe280df730a013
BLAKE2b-256 checksum
How to use checksums
8bc553bd74d6f46e3f56fbe189591eb70a82f6fa4d8b7b1a5c3de15cd28f6d24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.2.1

5 release files

0.2.0

5 release files

0.1.9

5 release files

This release

0.1.8 This release

5 release files

0.1.7

5 release files

0.1.6

5 release files

0.1.5

5 release files

0.1.4

6 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page