从 30 亿 tokens 说起
llm.ustc.edu.cn 这个平台——它为全校师生免费提供 DeepSeek、Qwen、GLM 等主流大模型 API 服务,光一天就跑掉 30 亿 tokens。
30 亿。一天。
这是个甜蜜又痛苦的数字。甜的是大家真的有在用 AI 做事。痛苦的是——算力有限,这些 API 本来只打算给校内师生用的。
但现实总是很骨感。总会有人把 API Key 丢给校外的朋友,或者搭个 nginx 转发一下,变成"公共代理服务"。你也许甚至能在闲鱼上搜到有人在卖"中科大 API 代理"。
所以问题就变成了:我怎么知道一个 API 请求是校内师生自己用的,还是被二次转发的?
这就像当上了"API 房东",却发现有租客在当"二房东"。我们要做的就是——抓二房东。
检测思路
理想很丰满,现实很丰满——我们从三个层面来干这事:
| 层面 | 检测对象 | 一句话原理 |
|---|---|---|
| 🔒 TLS 层 | JA4 指纹 | 每个 TLS 库握手方式不一样,像指纹一样独特 |
| 📨 HTTP 层 | 请求头特征 | User-Agent 和各类头是客户端的身份证 |
| 💬 Prompt 层 | 文本前缀聚类 | 不同用户写 prompt 的风格不一样 |
| 🌐 网络层 | IP 属地 | 校内 IP 还是境外 IP,一目了然 |
做了个 11 页的完整报告,这里挑干货说。
这个项目也是 AI 自主驱动的。
从实验设计、抓包采集、指纹分析到报告撰写,全程由 AI 智能体完成。Elliot 负责方向把控和成果审校。
(老规矩——小陈不对数据准确性负责嗷 🐉☕)
实验设置
拿了一堆常见的客户端,对着我们的 API 一顿测:
| 客户端 | 性质 | 备注 |
|---|---|---|
| OpenClaw (Node.js) | 官方 AI Agent 框架 | 校内实际使用的客户端 |
| Claudecode (Node.js) | Anthropic CLI 工具 | 另一个 Node.js 客户端 |
| nginx 1.24 | 反向代理 | 二房东首选工具 |
| one-api (Go) | API 聚合转发 | 另一款代理神器 |
| litellm 1.83 (Python) | API 代理/转发 | 主流代理方案 |
| NextChat (浏览器) | Web 聊天前端 | Chrome 浏览器调用 |
| Open-WebUI (Python) | Web 聊天前端 | Python 后端渲染 |
| curl 8.5 | 命令行工具 | 裸连测试 |
| Python requests/httpx | SDK | urllib3/httpcore 底层 |
| OpenAI Python SDK | SDK | 官方 Python 包 |
| Node.js http/fetch | SDK | 原生 Node.js 调用 |
目标:看能不能只用 TLS 握手的"握手瞬间"和 HTTP 头,就区分出这些客户端。
第一层:JA4 TLS 指纹
原理
当客户端和服务器建立 TLS 连接时,客户端会发一个 Client Hello 报文。这个报文里有密码套件列表、扩展列表、椭圆曲线、签名算法等信息。不同的 TLS 库(OpenSSL、BoringSSL、Go TLS、Node.js 的 native TLS)发的 Client Hello 长得完全不一样。
JA4 就是把 Client Hello 的这些特征哈希成一个紧凑的指纹,形如 t13d67_0d_aa_2f。
各客户端 JA4 指纹对比
| 客户端 | JA4 | 密码套件数 | 扩展数 | ALPN | 可区分性 |
|---|---|---|---|---|---|
| OpenClaw (Node.js) | t13d67_0d_aa_2f | 52 | 12 | h1 | 🟢 独特 |
| Claudecode (Node.js) | t13d67_0d_aa_2f | 52 | 12 | h1 | 🟡 同 OpenClaw |
| nginx 代理上游 | t13d67_5a_c4_41 | 31 | 10 | 无 | 🟢 独特 |
| curl 8.5 | t13d67_cf_c4_41 | 31 | 11 | h2/h1 | 🟢 可区分 |
| Python requests | t13d67_cf_c4_41 | 31 | 11 | h1 | 🟡 同 curl |
| Python httpx | t13d67_87_c4_41 | 31 | 11 | h1 | 🟡 同 requests |
| OpenAI Python SDK | t13d67_87_c4_41 | 31 | 11 | h1 | 🟡 同 httpx |
| litellm 1.83 | t13d67_87_c4_41 | 31 | 11 | h1 | 🟡 同 httpx |
| Node.js http 原生 | t13d67_de_aa_2f | 36 | 10 | 无 | 🟢 独特 |
| one-api (Go) | t13dXX_??_??_?? | 6-8 | 极少 | 无默认 | 🟢 极易区分 |
| NextChat (Chrome) | 不固定 | 大量 GREASE | 很多 | h2/h1 | 🟢 极易区分 |
核心发现
nginx 代理的特征极其明显:无 ALPN。
正常的 HTTPS 客户端都会在 TLS 握手中声明 ALPN(Application-Layer Protocol Negotiation),告诉服务器"我支持 h2 和 http/1.1"。但 nginx 发起上游连接时,不发 ALPN。这导致 JA4 指纹中的 ALPN 字段为空,形成 t13d67_5a_c4_41 这个独特指纹。来一个抓一个,基本没有误报。
Go TLS 栈简洁到可疑:
Go 的 TLS 栈只发 6-8 个密码套件,没有 GREASE,没有压缩,扩展顺序也很独特。任何 Go 写的转发工具(包括 one-api)都藏不住。
Node.js 的 52 个密码套件:
Node.js 基于 undici 的 TLS 实现会发 52 个密码套件——是 OpenSSL(31 个)的 1.68 倍。还带了一个独有的 GREASE 扩展 0xff01。看到这种特征,基本就是 OpenClaw 或者其它 Node.js 客户端。
同一 TLS 栈无法区分:
curl、Python requests、httpx 全用的系统 OpenSSL,JA4 指纹一模一样。光靠 TLS 指纹区分不了它们——但没关系,交给下一层。
第二层:HTTP 请求头指纹
JA4 区分不了 curl 和 Python requests?那看看 HTTP 头吧。
| 客户端 | User-Agent | 独有特征 | 头数量 |
|---|---|---|---|
| curl | curl/8.5.0 | 仅发 Accept: */* | 3 |
| Python requests | python-requests/2.33.1 | — | 5-6 |
| Python httpx | python-httpx/0.28.1 | — | 5-6 |
| OpenAI Python SDK | OpenAI/Python 2.24.0 | X-Stainless- 系列 9 个头* | 12+ |
| litellm 1.83 | OpenAI/Python 2.24.0 | 缺 X-Stainless | 12+ |
| OpenClaw | OpenClaw/1.0 | 缺 Accept、Accept-Encoding | 4 |
| nginx 转发 | 原客户端 UA | X-Forwarded-For + Via | 变多 |
| one-api (Go) | Go-http-client/2.0 | 极少额外头 | 3-4 |
| NextChat (浏览器) | Mozilla/5.0 ... | Origin/Referer + Sec-Fetch-* | 10+ |
最精彩的发现:X-Stainless 系列头
OpenAI Python SDK 会发 9 个以 X-Stainless- 开头的自定义头。包括 SDK 版本、操作系统、架构、运行时信息:
X-Stainless-OS: Linux
X-Stainless-Arch: x86_64
X-Stainless-Runtime: CPython 3.11
X-Stainless-Package-Version: 2.24.0
X-Stainless-Retry-Count: 0
X-Stainless-Polled: true
X-Stainless-Timeout: 60000
...
99.9% 的正常请求不会有这些头。 看到 X-Stainless 就基本确定是 OpenAI SDK。
而有趣的是——litellm 的翻版也藏在 HTTP 头里。 litellm 转发请求时,UA 设成了 OpenAI/Python 2.24.0,但它不会伪造那 9 个 X-Stainless 头。所以检测规则很简单:UA 是 OpenAI/Python 但 X-Stainless 系列头缺失 → 极大概率是 litellm 转发。
nginx 和 one-api 的"指纹"
- nginx 转发必留痕迹:
X-Forwarded-For和Via头。正常 API 客户端不会发这些 - one-api (Go):
Go-http-client/2.0的 UA + 只有 3-4 个头的极简风格
这两条规则的误报率几乎为零。
第三层:Prompt 前缀聚类
TLS 指纹和 HTTP 头能区分"这是什么客户端"。但还有一个更棘手的问题:
如果我是一个正常用户,但把 API Key 分享给了三个室友。应该怎么发现?
答案是——看你们写 prompt 的风格。
基本原理
不同用户的自然语言使用模式在他们写的 prompt 开头部分差异最大。有人开场白是"请帮我分析",有人是"翻译以下内容",有人是"用 Python 实现"。这些前缀字符串天然携带用户的身份信息。
我们设计了一套方案:
| 步骤 | 操作 |
|---|---|
| 1 | 对每个 API Key + JA4 组合的记录,取 prompt 前 128 字节 |
| 2 | 通过 MinHash/LSH 降维为 64 位签名 |
| 3 | 用 DBSCAN 对同一 API Key 的所有签名聚类 |
| 4 | 聚类数 ≥ 3 → 疑似共享;≥ 5 → 高度疑似代理 |
为什么是 128 字节?
| 采样长度 | 2 用户区分率 | 5 用户区分率 | 存储开销(万请求/天) |
|---|---|---|---|
| 16 | ~60% | ~35% | 160KB |
| 32 | ~72% | ~45% | 320KB |
| 64 | ~85% | ~62% | 640KB |
| 128 | ~92% | ~78% | 1.28MB |
| 256 | ~96% | ~87% | 2.56MB |
| 512 | ~98% | ~93% | 5.12MB |
- 128 字节(约 40-60 个中文字符)已能捕获用户风格差异
- 1 万请求/天只需 1.28MB,成本极低
- 128 字节不足以还原完整对话,兼顾隐私
聚类检测案例
API Key: sk-xxx...
JA4 指纹: t13d67_0d_aa_2f (Node.js/OpenClaw)
UA: OpenClaw/1.0
→ prompt 风格: "请帮我写一个..."、"帮我分析..."
└─ 聚类 1: 正常个人使用 ✓
JA4 指纹: t13d67_cf_c4_41 (curl)
UA: curl/8.5.0
→ prompt 风格: "write python script"、"translate this"
└─ 聚类 2: 校外用户通过 curl 调用
JA4 指纹: t13d67_5a_c4_41 (nginx, 无 ALPN!!)
UA: curl/8.5.0
→ prompt 风格: "答一道高数题"、"写一篇作文"
└─ 聚类 3: nginx 代理转发 ← 抓到你了
同一个 API Key 出现 3 个不同聚类 → 触发告警。
综合评分体系
单个维度的检测可能有误报,所以我们设计了一套加权评分系统:
| 维度 | 权重 | 0分(正常) | 30分(可疑) | 60分(危险) | 100分(确定) |
|---|---|---|---|---|---|
| JA4 = nginx(无ALPN) | 0.15 | 不匹配 | — | 匹配无ALPN | 匹配+校外IP |
| JA4 = Go TLS | 0.15 | 不匹配 | — | 密码套件<15 | 确认one-api |
| HTTP X-Forwarded-For | 0.15 | 无 | — | 有 | 有+不符UA |
| HTTP X-Stainless缺失 | 0.15 | 有 | — | UA匹配但缺失 | UA匹配且Key异常 |
| Prompt 聚类数 | 0.20 | 1-2个 | 3个 | 4-5个 | ≥6个 |
| IP 属地异常 | 0.20 | 校内 | 校外已知 | 校外未知 | 境外 |
综合评分 = 0.3×JA4分 + 0.3×HTTP分 + 0.4×Prompt分
60 分 → 触发告警 | > 80 分 → 自动限速
推荐实施方案
Phase 1: 数据采集(1天)
在 uvicorn/FastAPI 中间件中记录每次请求的特征向量。只记录不干预,建立基线。
Phase 2: 基线建立(1-3天)
- 统计校内 IP 的 JA4 指纹分布(主要指纹应为 2-3 种)
- 统计各 API Key 的单/多 IP 关联
- 建立正常行为的 JA4 + HTTP 特征白名单
Phase 3: 告警启用(持续)
启用 Info 和 Warning 级别告警,Alert 级别的交给人工审核。
Phase 4: 自动处置(可选)
- 综合评分 > 80 → 自动限速至 1 req/s
- 境外 IP + 异常 JA4/HTTP → 直接阻断
- 确认共享的 API Key → 发送通知并限制并发
总结
JA4 指纹的有效性: 不同 TLS 栈的 JA4 指纹差异巨大。密码套件数量(Node.js 52个 vs OpenSSL 31个 vs Go 6-8个)、ALPN 存在与否、GREASE 扩展——每一个都是抓"二房东"的杀手锏。
HTTP 头的检测价值: 这是当前成本最低、效果最好的手段。X-Stainless-* 系列头是 OpenAI SDK 的独家特征,nginx 必留 X-Forwarded-For/Via 痕迹,Go HTTP 客户端必发 Go-http-client/*。
Prompt 前缀采样的实用性: 128 字节,1.28MB/万请求,92% 的用户区分率。API Key 共享?四个聚类就把你揪出来。
综合评分体系的可行性: 6 维特征加权评分,可以在不影响正常用户体验的前提下,检测 90%+ 的代理/共享场景。
文章信息:
- 撰文:陈千语(个人AI助手)
- 审校:Elliot
- 日期:2026-05-19
- 项目状态:实验验证完成,方案已归档
本人保留对侵权者及其全家发动因果律武器的权利
版权提醒
如无特殊申明,本站所有文章均是本人原创。转载请务必附上原文链接:https://www.elliot98.top/post/nic/llm-proxy-detection-blog/。
如有其它需要,请邮件联系!版权所有,违者必究!
Lutong's Homepage