实操教程2026年10月8日阅读约 10 分钟

自己数字节:用 mitmproxy 核对代理用量表

自己动手核对代理用量表:把 mitmproxy 串在代理前面,逐条记录请求和响应字节数,再看清差距里哪些来自 TLS、gzip 压缩和重试。

想自己核对代理的用量表,办法是把 mitmproxy 放在客户端和代理之间,用一个很短的插件把每个请求、每个响应的大小写进 CSV 文件,再拿这份文件去对服务商的逐条请求日志。访问普通 http:// 网站时,两边的数字应该一个字节都不差;访问 https:// 网站时,两边不可能完全一致,但差多少、差在哪里,是可以事先算出来的。

怎么核对你的代理服务商用量表是否诚实讲的是道理:一个你没法还原的用量数字只是说法,不是账单。这一篇是动手的那一半:要敲的命令、我们实际跑出来的数字,以及这些数字能证明什么、不能证明什么。

需要准备什么

  • Docker,以及官方镜像 mitmproxy/mitmproxy:12.1.2,这是我们测试用的版本。
  • curl,或者你真正想核对的那个脚本。
  • 一套 HTTP 协议的代理登录信息(HOST、PORT、USERNAME、PASSWORD)。mitmproxy 的上游模式只会用 HTTP 和下一级代理通信:用 --mode upstream:socks5://… 启动会立刻报错 Invalid server scheme: socks5。住宅代理的端口本来就接受 HTTP;ISP 和数据中心服务请在测试期间到控制台把协议切换成 HTTP。

我们是怎么测的

为了同时看到用量表两边的数字,我们自己搭了一个代理坐在服务商的位置上:带登录的 Squid 6.13,它会记下每条连接的请求字节和响应字节。它后面是两个本地测试网站:一个 httpbin 的复刻(go-httpbin 2.25.0),一个开了 gzip 的 nginx 1.27。全部跑在同一台机器的容器里,没有用到任何外部服务商。下文说的“用量表”,指的就是这份 Squid 日志。真实服务商的数字在细节上会不一样,原因见后文。

第一步:计数插件

mitmproxy 会对经过它的每个请求运行 Python 插件。把下面的代码存为 count_bytes.py:

import csv

from mitmproxy import ctx, http
from mitmproxy.net.http.http1.assemble import assemble_request_head, assemble_response_head


class CountBytes:
    def __init__(self):
        self.file = open("bytes.csv", "w", newline="")
        self.out = csv.writer(self.file)
        self.out.writerow(["host", "path", "status", "sent", "received", "body_on_wire", "body_decoded"])
        self.sent = self.received = 0

    def write(self, row):
        self.out.writerow(row)
        self.file.flush()
        self.sent += row[3]
        self.received += row[4]

    def response(self, flow: http.HTTPFlow):
        sent = len(assemble_request_head(flow.request)) + len(flow.request.raw_content or b"")
        body = len(flow.response.raw_content or b"")
        decoded = len(flow.response.get_content(strict=False) or b"")
        received = len(assemble_response_head(flow.response)) + body
        self.write([flow.request.host, flow.request.path, flow.response.status_code, sent, received, body, decoded])

    def error(self, flow: http.HTTPFlow):
        self.write([flow.request.host, flow.request.path, "failed", 0, 0, 0, 0])

    def done(self):
        ctx.log.info(f"sent {self.sent} bytes, received {self.received} bytes")
        self.file.close()


addons = [CountBytes()]

received 是状态行、响应头,加上线路上实际传输的正文,压缩过就按压缩后的大小算。body_decoded 是同一份正文解压之后的大小。单独列出来,是因为把这两个数弄混,是自己核对时最常见的出错方式。没有收到任何响应的请求,会记成 failed,各列都是零。

第二步:把 mitmproxy 串到代理前面

mkdir -p ~/.mitmproxy
docker run --rm -it -p 127.0.0.1:8080:8080 \
  -v "$PWD":/work -w /work \
  -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
  mitmproxy/mitmproxy:12.1.2 \
  mitmdump --mode upstream:http://HOST:PORT --upstream-auth USERNAME:PASSWORD \
    -s count_bytes.py --set http2=false

--mode upstream: 让 mitmproxy 不自己去连网站,而是把所有流量转给你的代理;--upstream-auth 负责发送登录信息,所以客户端那边不用再填代理密码。--set http2=false 让所有交互都走 HTTP/1.1,头部就是可以直接数的纯文本。端口只绑定在 127.0.0.1 上,局域网里的其他设备用不了它。我们在实验环境里还加了 --set ssl_verify_upstream_trusted_ca=,指向测试网站的自签名证书;换成真实网站时,不要加这一项。

第一次启动时,mitmproxy 会在 ~/.mitmproxy 里生成自己的证书颁发机构(CA)。客户端必须信任它,mitmproxy 才看得到 https:// 流量的内容;否则客户端会拒绝连接,curl 报的是 curl: (60) SSL certificate problem: unable to get local issuer certificate。

第三步:让流量经过它

curl -x http://127.0.0.1:8080 --cacert ~/.mitmproxy/mitmproxy-ca-cert.pem \
  -o /dev/null https://httpbin.org/bytes/1000

用 requests 写的 Python 脚本,设两个环境变量就行,我们用 requests 2.32.5 验证过:

HTTPS_PROXY=http://127.0.0.1:8080 \
REQUESTS_CA_BUNDLE=~/.mitmproxy/mitmproxy-ca-cert.pem \
python3 your_script.py

按 Ctrl+C 停掉 mitmdump,插件会打印总数,我们那次是 sent 543 bytes, received 13729 bytes,旁边就是 bytes.csv。下面就是那一次的文件,实验里的 origin 是 httpbin 复刻,shop 是 nginx:

host,path,status,sent,received,body_on_wire,body_decoded
origin,/bytes/1000,200,84,1190,1000,1000
shop,/catalogue.html,200,123,11721,11470,182045
origin,/status/429,429,84,203,0,0
origin,/status/503,503,84,205,0,0
origin,/status/503,503,84,205,0,0
origin,/status/503,503,84,205,0,0
nowhere.invalid,/,failed,0,0,0,0

如果上游的登录信息不对,mitmdump 会记下 Upstream proxy HOST:PORT refused HTTP CONNECT request: 407 Proxy Authentication Required,curl 则从 mitmproxy 那里拿到一个 502。常见原因见解决 407 错误。

第四步:和服务商的日志逐行对照

导出同一时间段的逐条请求日志,按时间和主机把两边的行对上,把你的 received 列和对方的字节数列放在一起看。对照期间最好一个请求用一条连接:分开执行的每条 curl 命令都会新开连接,这样你的一行正好对着对方的一行。

这些数字能证明什么

普通 http:一个字节都不差

访问 http://plain:8080/bytes/1000 时,我们的 CSV 记下发送 179 字节、接收 1,294 字节,用量表记的也是 179 和 1,294。能读懂明文 HTTP 的代理,看到的和你看到的完全一样,连它自己加上的头部也算在内(我们的 Squid 加了 Via 和 Cache-Status,这两个头同样到达了 mitmproxy)。这里要是出现实打实的差距,就值得发邮件去问。

https:用量表数的是隧道

访问 https:// 网站时,客户端请代理打开一条 CONNECT 隧道,隧道里的一切都是加密的。代理处的用量表只能数穿过隧道的东西:TLS 握手、网站证书、每个加密记录上那几个字节的封装,再加上头部和正文。mitmproxy 会解密,所以它只数隧道里面的 HTTP。实际结果如下:

抓取内容 mitmproxy 的 received 用量表:隧道回传字节
1 个 1,000 字节的响应,新连接 1,190 3,858
同样 5 个,复用同一条长连接 5,950 8,706
10 个 100,000 字节的响应,同一条连接 1,001,920 1,006,216
同样 10 个,每个都新开连接 1,001,920 1,031,020

每条连接的握手大约多出 2.7 KB,而我们的测试网站只发了一张很小的自签名证书。真实网站发的是证书链,常常有好几 KB,所以实际会更多。很小的响应走新连接时,被计入的大头是握手;大响应走复用的连接,差距降到了 0.4%;每个 100 KB 的响应都新开连接,差距是 2.9%。还要留意,5 个请求在用量表里只占一行隧道记录:如果日志的一行代表一条连接,那一行里可能装着你好几个页面。

压缩:数线路上走过的字节

测试用的商品目录页是 182,045 字节的 HTML,gzip 压缩后是 11,470 字节。Python requests 默认就会请求 gzip,并把解压后的正文交给你,于是 len(r.content) 打印出 182,045,而用量表看到的这次传输是 13,732 字节。请数 body_on_wire,或者在测量期间设置 Accept-Encoding: identity。

重试也是流量

用 curl --retry 2 请求一个返回 503 的地址,一共发出 3 个请求,CSV 里是 3 行,而这 3 次都走同一条隧道,用量表记成一行 3,305 字节。网站每一次都回应了,所以每一次都花了流量。处理封锁和重试,别白白烧掉代理流量一文讲了怎样避免为同一个拒绝付两次钱。

网站报错不等于连接失败

那个 429 回来时,是 2,849 字节隧道里装着的 203 字节 HTTP:网站作了回应,所以算流量。一个根本不存在的主机,在 CSV 里留下一行全是零的 failed,用量表也记下了这次尝试,但没有从任何网站收到任何东西。

这些数字证明不了什么

  • mitmproxy 会改变它测量的连接。 它会自己和网站建立 TLS 会话,所以用量表数到的握手是 mitmproxy 的,不是你的客户端的,网站看到的也是 mitmproxy 的 TLS 指纹。请拿 httpbin.org 这类中立的地址来核对,不要拿你真正要抓取的网站。
  • HTTP/2 的头部更小。 我们关掉了 HTTP/2,头部按 HTTP/1.1 文本计算。走 HTTP/2 时头部在线路上是压缩过的,插件会把它们算多。
  • 它只看得到经过它的流量。 另一个程序用同一套代理登录信息产生的流量,会出现在服务商的日志里,却不会出现在你的文件里。
  • 它看不到服务商内部。 本地计数只能给出你应该预期的范围。值得提出来的,是普通 http 上的明显差距,或者 https 长连接大流量传输中的明显差距。

不想写脚本:Charles 和 Fiddler

更习惯图形界面的话,Charles 和 Fiddler Everywhere 都能逐条列出请求和大小,也都能把流量转给另一个代理。Charles 里这项功能叫 External Proxies,HTTP、HTTPS 和 SOCKS 可以分别设置,支持 Basic 登录。Fiddler Everywhere 放在 Gateway 下的 Manual proxy configuration;它的文档没有提到给这个上游填写登录信息,所以建议搭配 IP 白名单使用。上面的对照方法照样适用,证书那一步也一样:两者都要先信任各自的根证书,才能显示 https:// 流量。

快速问答

不靠服务商,能核对代理用量表吗? 你能量自己这一侧。要拿去对照,就需要对方的逐条请求日志,所以买之前就值得问一句有没有。

为什么 https 下服务商的数字比我的大? 对方的用量表数的是加密隧道,握手和封装都算在内;mitmproxy 数的是隧道里的 HTTP。响应越小、越是新连接,差距越大。

429 或封锁页算不算流量? 只要用量表数的是网站发来的字节,就算:那些字节确实是网站发的。

ProxyPanda 的用量表是怎么写明的

我们的实话实说页写得很清楚:计费的是响应正文加响应头,在我们的代理处测量,请求字节不收费。在 https 隧道里,我们的代理看不到网站的状态码,所以用量表数的是穿过隧道的字节。超时、连接重置和我们自己网关的错误计零,并照样留在日志里标为免费,整份日志可以导出成 CSV。如果你数的和我们数的相差超出那里写明的容差,把两个数字都发给我们,我们先按你的数字重算,再去查原因。用量表那篇文章列出了值得问任何服务商的几个问题,价格页写着这些字节要乘上的单价。

下一步

照上面的方法启动 mitmdump,在一条 curl 命令里连续抓取 10 次 https://httpbin.org/bytes/100000,这样会复用同一条连接;再分 10 条命令各抓一次。把两组总数和服务商同一时间段的日志放在一起:第一组应该很接近,第二组则告诉你在这个网络上一次握手要花多少。数字看不明白的话,把 CSV 删掉登录信息后发到 Discord。

还有后续问题?

去 Discord 问。答案还能帮到下一个读到这个帖子的人。

加入 Discorddiscord.gg/proxypanda
$5 起步去 Discord 问