trans_proxy

一款 macOS 和 Linux 透明代理工具,通过 pf/nftables 拦截 TCP 流量并转发至上游 HTTP CONNECT 或 SOCKS5 代理。

客户端设备 macOS pf rdr trans_proxy :8443 trans_proxy 内部 DIOCNATLOOK SNI 提取 DNS 表 CONNECT host:port 上游代理 (HTTP CONNECT / SOCKS5) 原始目标地址 DNS 转发器 :53 网关 rdr IP → 域名 /dev/pf

功能特性

macOS pf 集成

使用 /dev/pf 上的 DIOCNATLOOK ioctl 从 pf 的 NAT 状态表中恢复原始目标地址 — 与 mitmproxy 使用的技术相同。

Linux nftables 集成

使用 SO_ORIGINAL_DST getsockopt 从 nftables 重定向规则中恢复原始目标地址。

SOCKS5 上游支持

通过 socks5://host:port 使用 SOCKS5 代理作为上游,支持可选的用户名/密码认证(RFC 1928/1929)。

SNI 提取

窥探 TLS ClientHello 以提取主机名,发送正确的 CONNECT host:port 而非原始 IP。无需 TLS 终止或证书生成。

DNS 转发器

直接在网关接口(端口 53)上监听局域网客户端的 DNS 查询。构建 IP→域名查找表。支持 DoH 和 UDP 上游。

安全的防火墙规则

使用基于锚点的 pf 规则(macOS)或独立的 nftables 表(Linux),不会触碰现有防火墙配置。

守护进程模式

支持作为后台进程运行,带有 PID 文件和日志文件支持。使用 -d 启动,使用 kill 停止。

系统服务

安装为系统服务(macOS 使用 launchd,Linux 使用 systemd),开机自动启动。Linux 上自动管理 nftables NAT 规则。使用 --install--uninstall

异步 I/O

基于 tokio 构建,为每个连接生成独立任务。通过双向中继高效处理大量并发连接。

端到端测试

完整的端到端测试套件在 Linux 和 macOS 上运行真实的 nftables/pf + 代理流水线,覆盖 SOCKS5、HTTP CONNECT、DNS 转发和端口选择性重定向。

系统要求

构建

# 克隆仓库
git clone https://github.com/madeye/trans_proxy.git
cd trans_proxy

# 构建发布版二进制文件
cargo build --release

# 验证
cargo test
./target/release/trans_proxy --help

快速开始

本示例假设您的上游 HTTP 代理运行在 127.0.0.1:1082,局域网接口为 en0

启动透明代理

在网关接口上启用 DNS 运行(默认使用 Cloudflare DoH):

# 前台运行(自动检测 en0 IP,监听端口 53)
sudo ./target/release/trans_proxy \
  --upstream-proxy 127.0.0.1:1082 \
  --dns

# 或作为守护进程运行
sudo ./target/release/trans_proxy \
  --upstream-proxy 127.0.0.1:1082 \
  --dns -d

设置 pf 重定向

在另一个终端中安装 pf 规则(仅 HTTP/HTTPS — DNS 由程序直接处理):

sudo scripts/pf_setup.sh en0 8443

脚本会输出您的网关 IP 和设置摘要。

配置客户端设备

在每个客户端设备上将 Mac 的 IP 设置为默认网关(和 DNS 服务器)。

完成后清理

sudo scripts/pf_teardown.sh
# 如果以守护进程运行
sudo kill $(cat /var/run/trans_proxy.pid)

命令行参数

参数 默认值 说明
--listen-addr 0.0.0.0:8443 代理监听的地址和端口
--upstream-proxy 必填 上游代理:host:port 为 HTTP CONNECT,socks5://host:port 为 SOCKS5
--log-level info 日志详细级别:trace、debug、info、warn、error
--dns off 在网关接口上启用 DNS 转发器(端口 53)
--interface en0 用于 DNS 自动检测的网络接口
--dns-listen 自动 覆盖 DNS 监听地址(例如 192.168.1.42:53
--dns-upstream https://cloudflare-dns.com/dns-query 上游 DNS:UDP 使用 host:port,DoH 使用 https:// URL
-d / --daemon off 作为后台守护进程运行
--pid-file /var/run/trans_proxy.pid PID 文件路径(与 --daemon 一起使用)
--log-file /var/log/trans_proxy.log(守护进程) 日志文件路径(前台运行时默认输出到 stderr)
--install off 安装为系统服务(macOS 使用 launchd,Linux 使用 systemd)
--uninstall off 卸载系统服务

pf 脚本

# 设置:pf_setup.sh <interface> [proxy_port]
# DNS 不再需要 pf 重定向 — 直接监听端口 53
sudo scripts/pf_setup.sh en0 8443

# 清理:刷新锚点规则,禁用 IP 转发
sudo scripts/pf_teardown.sh

工作原理

流量流程

  1. 客户端发送数据包到 example.com:443(解析为 93.184.216.34
  2. 数据包到达网关的局域网接口
  3. NAT 重定向规则将目标地址重写为 127.0.0.1:8443(macOS 使用 pf,Linux 使用 nftables)
  4. trans_proxy 接受连接
  5. 通过 DIOCNATLOOK(macOS)或 SO_ORIGINAL_DST(Linux)恢复原始目标地址(93.184.216.34:443
  6. SNI 提取窥探 TLS ClientHello 以读取主机名(example.com
  7. 向上游代理发送 CONNECT example.com:443
  8. 在客户端和上游代理之间进行双向中继

主机名解析链

1. SNI

从 TLS ClientHello 中提取。适用于 HTTPS(端口 443)。无需 TLS 终止。

2. DNS 表

来自 DNS 响应(通过 DoH 或 UDP)的 IP→域名映射。适用于 HTTP 和 HTTPS。需要 --dns

3. 原始 IP

如果无法确定主机名,则回退到 IP 地址。始终可用。

原始目标地址恢复

NAT 重定向规则会在套接字层看到之前就重写目标地址。 trans_proxy 使用平台特定机制恢复原始目标地址: macOS 上使用 DIOCNATLOOK(查询 pf 的 NAT 状态表,与 mitmproxy 相同), Linux 上使用 SO_ORIGINAL_DST(从内核的连接跟踪中恢复重定向前的目标地址)。

客户端设置

在每个需要通过代理路由的设备上,将网关机器的 IP 设置为默认网关和 DNS 服务器。

macOS / iOS

设置 → Wi-Fi → (i) → 配置 IP → 手动 → 路由器和 DNS:网关 IP

Windows

设置 → 网络 → Wi-Fi → 属性 → 编辑 IP → 手动 → 网关和 DNS:网关 IP

Linux

sudo ip route replace default via <ip> 并更新 /etc/resolv.conf

Android

Wi-Fi → 长按 → 修改 → 高级 → 静态 IP → 网关和 DNS:网关 IP

故障排除

"Failed to open /dev/pf"
请使用 sudo 运行。代理需要 root 权限才能访问 /dev/pf
"No ALTQ support in kernel"
这是 pfctl 的无害警告。macOS 不包含 ALTQ。pf 重定向在没有它的情况下也能正常工作。
"DIOCNATLOOK failed"
确保 pf 规则已加载:sudo pfctl -a trans_proxy -s rules
确保 pf 已启用:sudo pfctl -s info | head -1
检查流量是否到达预期的接口。
连接挂起或超时
验证上游代理是否正在运行并接受 CONNECT 请求。
使用 --log-level debug 获取详细的逐连接日志。
确保已启用 IP 转发:sysctl net.inet.ip.forwarding(应为 1)。
客户端设备无法解析 DNS
确保已设置 --dns 且 DNS 转发器正在运行。
检查 trans_proxy 日志是否显示 DNS forwarder listening on <ip>:53
直接测试:dig @<gateway_ip> example.com