广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何用Nginx配合mitmproxy搭建HTTPS中间人代理调试环境?

调试HTTPS接口时,最麻烦的是加密流量把请求和响应都藏了起来,抓包工具只能看到TLS握手。借助Nginx和mitmproxy的组合,可以搭建一条本地可控的代理链路:客户端把请求发往Nginx,Nginx不做HTTP解析,而是用stream模块把原始TCP流量转发给mitmproxy;mitmproxy作为中间人完成TLS终止,把明文请求展示出来,再转发到真实后端。这样既能观察移动端、小程序或第三方服务发来的加密数据,也能对请求做断点修改,而不会影响线上服务。

如何用Nginx配合mitmproxy搭建HTTPS中间人代理调试环境?

整个链路对客户端来说是透明的,客户端只需要信任mitmproxy生成的CA证书。文章会先对比两种常见的架构选择,再给出Nginx和mitmproxy的具体配置,最后介绍断点调试和常见问题。

一、两种代理链路的取舍

在开始配置之前,需要先确定让谁来终止TLS。第一种做法是让Nginx仅做四层转发,把443端口的原始TLS流量通过stream模块交给mitmproxy处理。mitmproxy负责与客户端完成TLS握手,因此必须让客户端安装并信任mitmproxy的CA证书。这种方式的好处是mitmproxy能看到完整的HTTPS明文,包括请求头、请求体、响应体和状态码,同时可以修改任何一个字段再发送给上游。对于调试移动App、小程序或者无法直接设置HTTP代理的IoT设备,这种模式最接近真正的中间人代理。

第二种做法是让Nginx直接终止TLS,使用自己的证书完成解密,然后把明文HTTP请求转发给mitmproxy。此时客户端不需要安装额外证书,因为Nginx使用的证书如果是公共信任链签发的,客户端就会正常完成握手。mitmproxy接收的是Nginx转发过来的明文,虽然看不到客户端与Nginx之间的TLS细节,但仍然可以修改请求并转发到后端。这种方案适合已有正式证书、不方便在客户端安装CA证书或者只想快速在服务端侧拦截流量的场景。缺点是无法观察到原始的TLS协商过程,而且Nginx解密的流量是以HTTP形式在本地传递,安全性完全依赖本机环境。

本文后续以第一种stream转发方案为主进行演示,因为它更能体现中间人代理的价值,配置上也更加简洁。第二种方案会在配置部分给出对照示例,方便按需选择。

二、证书准备与Nginx stream配置

先安装mitmproxy。macOS可以使用Homebrew安装,Linux可以通过pip安装,Windows也有对应的安装包。安装完成后执行一次mitmproxy命令,它会在用户目录下生成CA证书文件,通常位于~/.mitmproxy目录。运行下面的命令可以看到证书所在位置,并把PEM格式的CA证书导入客户端设备。手机端可以通过mitm.it页面下载证书,但需要先把设备的代理指向mitmproxy所在机器。

Nginx需要开启stream模块。大多数官方二进制包和源码编译版本都包含stream模块,可以通过nginx -V查看编译参数中是否有--with-stream。确认后,在nginx.conf中增加stream块,与http块平级。配置如下:

stream {
    upstream mitmproxy_backend {
        server 127.0.0.1:8080;
    }
    server {
        listen 443;
        proxy_pass mitmproxy_backend;
        proxy_connect_timeout 5s;
        proxy_timeout 60s;
    }
}

这个配置把本机443端口收到的所有TCP流量转发到mitmproxy监听的8080端口。注意stream块中的server不能与http块中的server混用,两者是独立的作用域。proxy_connect_timeout和proxy_timeout用来控制连接超时,避免长时间占用连接。修改配置后执行nginx -t检查语法,再执行nginx -s reload让配置生效。

如果采用Nginx终止TLS的方案,则不需要stream块,而是在http块中配置一个普通的HTTPS server,把请求反向代理到mitmproxy。配置示例如下:

server {
    listen 443 ssl;
    server_name debug.ipipp.com;
    ssl_certificate /etc/nginx/certs/debug.ipipp.com.pem;
    ssl_certificate_key /etc/nginx/certs/debug.ipipp.com.key;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host debug.ipipp.com;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

这种模式下,Nginx把解密后的HTTP请求发送到mitmproxy监听的8080端口,mitmproxy只需要以普通HTTP代理模式运行即可。

三、启动mitmproxy并配置上游

在stream方案中,Nginx只是把TLS流量原封不动转给mitmproxy,因此mitmproxy需要监听8080端口并负责TLS握手。最简单的启动方式是使用上游模式,让所有请求自动转发到指定后端,不需要每次手动确认。命令如下:

mitmproxy --listen-port 8080 --mode upstream:http://127.0.0.1:9000 --ssl-insecure

其中--listen-port 8080对应Nginx stream配置中的目标端口,--mode upstream:http://127.0.0.1:9000表示mitmproxy终止TLS后,把明文HTTP请求转发到本地9000端口,也就是真实后端或者本地调试服务监听的地址。--ssl-insecure的作用是允许mitmproxy在连接上游时忽略证书校验,适合后端使用自签名证书或者测试环境证书的情况。

如果希望根据请求的Host或路径动态选择不同的上游,可以写一个简单的mitmproxy addon。下面这个脚本会把不同的路径转发到不同的后端服务。

from mitmproxy import http
class Router:
    def request(self, flow: http.HTTPFlow) -> None:
        if flow.request.path.startswith("/api/"):
            flow.request.host = "127.0.0.1"
            flow.request.port = 9001
        else:
            flow.request.host = "127.0.0.1"
            flow.request.port = 9002
addons = [Router()]

使用方法是将脚本保存为route.py,然后用mitmproxy -s route.py启动。注意addon中的request函数会在每次请求到达时执行,可以通过修改flow.request.host和flow.request.port来改变上游地址,mitmproxy会根据修改后的值重新发起连接。

如果希望Nginx终止TLS后转发给mitmproxy,则mitmproxy不需要处理TLS,启动参数可以去掉--ssl-insecure,并监听8080端口作为普通HTTP代理,例如:mitmproxy --listen-port 8080 --mode upstream:http://127.0.0.1:9000。两种模式的区别只在于TLS终止的位置。

四、断点修改与请求重放

mitmproxy的交互式界面提供了方便的断点功能。在运行mitmproxy时,可以按b键调出断点规则输入框,例如输入~u /login表示只对路径包含/login的请求进行拦截。拦截后,请求会暂停在队列中,按回车进入详情页,可以编辑请求头、请求体、URL参数,修改完成后按a键放行。响应也可以被拦截,例如输入~s表示所有响应都暂停,或者输入~s 500只拦截状态码为500的响应。这样在调试后端异常时,可以先看到原始响应,再进行修改或重放。

如果觉得交互式操作不够自动化,可以使用Python脚本实现条件断点。下面这个addon会在请求路径以/login结尾时自动拦截请求,在响应状态码为500时自动拦截响应。

from mitmproxy import http
def request(flow: http.HTTPFlow) -> None:
    if flow.request.path.endswith("/login"):
        flow.intercept()
def response(flow: http.HTTPFlow) -> None:
    if flow.response.status_code == 500:
        flow.intercept()

脚本中flow.intercept()会让对应的请求或响应进入拦截队列,在mitmproxy界面中可以看到并手动处理。对于更复杂的场景,可以结合mitmproxy的flow.kill()直接丢弃请求,或者用flow.response = http.Response.make(...)直接返回伪造响应。例如在登录接口调试时,可以临时替换响应体,模拟不同角色登录。

重放请求同样简单。在mitmproxy界面中,选中历史记录里的某个请求,按r键会重新发送该请求,按r后可以编辑请求内容。结合断点修改,可以反复验证后端对特定参数的处理逻辑。对于需要登录态的接口,先放行一次登录请求拿到Cookie,后续重放会自动带上该Cookie,减少手动设置的成本。

五、常见问题排查

证书问题是最容易遇到的。如果客户端没有安装mitmproxy CA证书,TLS握手会失败,mitmproxy界面中会看到证书错误提示。解决方法是先在电脑上运行一次mitmproxy,然后在客户端访问mitm.it下载对应平台的证书,并安装到系统信任区。iOS和Android对证书的信任要求不同,iOS需要在设置中手动启用完全信任,Android则可以安装到用户证书区,部分高版本系统需要安装为系统证书才能被应用信任。

另一个常见问题是Nginx的stream配置没有生效。由于stream块与http块是平级关系,如果nginx.conf中已经有http块,需要确保stream块没有嵌套在http内部。同时检查nginx -t的输出,确认没有语法错误。如果启动时提示unknown directive stream,说明当前Nginx没有编译stream模块,需要重新安装或更换包含stream模块的包。

上游连接失败通常与mitmproxy的--mode upstream有关。如果上游服务返回502或连接超时,先检查mitmproxy终端中的错误日志,确认上游地址和端口是否正确,以及后端服务是否监听在对应端口。可以使用curl直接访问后端服务,排除mitmproxy本身的问题。对于使用域名作为上游的情况,可以在mitmproxy命令行中添加--set connection_strategy=lazy,让连接延迟到请求真正需要发送时再建立,减少DNS解析提前失败的影响。

最后需要留意Host头的变化。在Nginx stream转发模式下,客户端发送的Host头会原样保留,因此mitmproxy转发到上游时,后端看到的Host还是原始域名。如果后端依赖Host进行路由或签名校验,可能需要修改Host头。可以使用addon脚本统一设置Host,或者在后端网关层做兼容。通过这组工具和配置,就能比较顺畅地对HTTPS接口进行本地化调试。

广告位 · 文章底部(后台-模块管理替换此区域)