AI自主科研案例————LLM API 代理检测:当网络管理员开始抓"API 二房东"

2026-05-09 research network LLM API security TLS JA4 proxy-detection

从 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/httpxSDKurllib3/httpcore 底层
OpenAI Python SDKSDK官方 Python 包
Node.js http/fetchSDK原生 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_2f5212h1🟢 独特
Claudecode (Node.js)t13d67_0d_aa_2f5212h1🟡 同 OpenClaw
nginx 代理上游t13d67_5a_c4_413110🟢 独特
curl 8.5t13d67_cf_c4_413111h2/h1🟢 可区分
Python requestst13d67_cf_c4_413111h1🟡 同 curl
Python httpxt13d67_87_c4_413111h1🟡 同 requests
OpenAI Python SDKt13d67_87_c4_413111h1🟡 同 httpx
litellm 1.83t13d67_87_c4_413111h1🟡 同 httpx
Node.js http 原生t13d67_de_aa_2f3610🟢 独特
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独有特征头数量
curlcurl/8.5.0仅发 Accept: */*3
Python requestspython-requests/2.33.15-6
Python httpxpython-httpx/0.28.15-6
OpenAI Python SDKOpenAI/Python 2.24.0X-Stainless- 系列 9 个头*12+
litellm 1.83OpenAI/Python 2.24.0缺 X-Stainless12+
OpenClawOpenClaw/1.0缺 Accept、Accept-Encoding4
nginx 转发原客户端 UAX-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-ForVia 头。正常 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 TLS0.15不匹配密码套件<15确认one-api
HTTP X-Forwarded-For0.15有+不符UA
HTTP X-Stainless缺失0.15UA匹配但缺失UA匹配且Key异常
Prompt 聚类数0.201-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/

如有其它需要,请邮件联系!版权所有,违者必究!