传统的代理,常见的有 HTTP 代理、SOCKS 代理,比如 Windows
系统你可以在 网络和 Internet
- 代理
中进行设置,对浏览器的访问生效。如果是其他的程序,就要看支不支持系统代理的配置了,如果不支持的话,还需要类似 SocksCap
之类的程序去劫持应用程序的请求来使用代理。相对于传统的代理,透明代理则无需配置系统代理,而是通过配置路由规则实现代理,这对几乎所有的应用程序都有效。这里介绍一个 Linux
系统基于 Xray
配置透明代理的方案,这样除了本机可以使用代理,Docker
等服务也可以使用代理,还可以支持代理局域网中其他机器的请求。
安装 Xray
首先,我们需要先安装 Xray
,当然你也可以选择使用 V2ray
,这里我们采用官方脚本进行安装。
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install -u root
安装完毕后,默认的文件位置如下:
installed: /etc/systemd/system/xray.service
installed: /etc/systemd/system/[email protected]
installed: /usr/local/bin/xray
installed: /usr/local/etc/xray/*.json
installed: /usr/local/share/xray/geoip.dat
installed: /usr/local/share/xray/geosite.dat
installed: /var/log/xray/access.log
installed: /var/log/xray/error.log
国内对github的访问向来是时断时续的,如果安装失败可以等一等,或者使用科学上网工具,这里推荐可以 注册 一个,目前有免费流量可以薅羊毛。
接下来,我们需要修改 /usr/local/etc/xray/config.json
文件,在 inbounds
中开启透明代理入口,下面是一个简单的例子。
{
"log": {
"loglevel": "warning",
"error": "/var/log/xray/error.log",
"access": "/var/log/xray/access.log"
},
"inbounds": [
{
"tag": "all-in",
"port": 12345,
"protocol": "dokodemo-door",
"settings": {
"network": "tcp,udp",
"followRedirect": true
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls"]
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
}
}
],
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
},
"streamSettings": {
"sockopt": {
"mark": 2
}
}
},
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "服务端域名",
"port": 443,
"users": [
{
"id": "UUID",
"flow": "xtls-rprx-vision",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "xtls",
"sockopt": {
"mark": 2
}
}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
},
{
"tag": "dns-out",
"protocol": "dns",
"settings": {
"address": "8.8.8.8"
},
"proxySettings": {
"tag": "proxy"
},
"streamSettings": {
"sockopt": {
"mark": 2
}
}
}
],
"dns": {
"hosts": {
"服务端域名": "服务端 IP"
},
"servers": [
{
"address": "119.29.29.29",
"port": 53,
"domains": ["geosite:cn"],
"expectIPs": ["geoip:cn"]
},
{
"address": "223.5.5.5",
"port": 53,
"domains": ["geosite:cn"],
"expectIPs": ["geoip:cn"]
},
"8.8.8.8",
"1.1.1.1",
"https+local://doh.dns.sb/dns-query"
]
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["all-in"],
"port": 53,
"outboundTag": "dns-out"
},
{
"type": "field",
"ip": ["8.8.8.8", "1.1.1.1"],
"outboundTag": "proxy"
},
{
"type": "field",
"domain": ["geosite:category-ads-all"],
"outboundTag": "block"
},
{
"type": "field",
"domain": ["geosite:geolocation-!cn"],
"outboundTag": "proxy"
},
{
"type": "field",
"ip": ["geoip:telegram"],
"outboundTag": "proxy"
}
]
}
}
上面配置中,端口 12345
就是用来接收透明代理请求的入口,从该入口进入的请求都会根据路由规则,凡是解析为国外 IP 的请求都会走 proxy
的出口代理,你需要按你的实际配置进行修改。详细的配置可以参考 官方文档。
配置透明代理
透明代理的配置比较复杂,这里推荐使用上面的开源工具 ss-tproxy
进行自动配置。首先,需要安装依赖项 chinadns-ng
,用来实现 DNS 分流。
首先,我们从 chinadns-ng 下载最新版本,选择 chinadns-ng+wolfssl
开头的,然后根据你的 CPU 架构进行选择。
# x86_64架构
curl -o chinadns-ng https://github.com/zfl9/chinadns-ng/releases/download/2024.12.22/chinadns-ng+wolfssl@x86_64-linux-musl@x86_64@fast+lto
# arm64架构
curl -o chinadns-ng https://github.com/zfl9/chinadns-ng/releases/download/2024.12.22/chinadns-ng+wolfssl@aarch64-linux-musl@generic+v8a@fast+lto
# 安装
chmod +x chinadns-ng
mv chinadns-ng /usr/local/bin
接下来我们安装 ss-tproxy
。
git clone https://github.com/zfl9/ss-tproxy
cd ss-tproxy
chmod +x ss-tproxy
install ss-tproxy /usr/local/bin
install -d /etc/ss-tproxy
install -m 644 *.conf *.txt *.ext /etc/ss-tproxy
install -m 644 ss-tproxy.service /etc/systemd/system
systemctl daemon-reload
systemctl enable ss-tproxy
然后修改 /etc/ss-tproxy/ss-tproxy.conf
配置文件,每个配置项的作用配置文件中已经有详细的说明,这里只列出最关键的几项。
## mode
#mode='global' # 全局:{ignlist}走直连,其他走代理
#mode='gfwlist' # 黑名单:{gfwlist}走代理,其他走直连 (回国模式也是这个,后面有详细说明)
mode='chnroute' # 大陆白名单:{gfwlist}走代理,{ignlist,chnlist,chnroute}走直连,其他走代理
## ipv4/6
ipv4='true' # 是否对ipv4启用'透明代理': true启用 false不启用
ipv6='true' # 是否对ipv6启用'透明代理': true启用 false不启用
## tproxy
tproxy='true' # true: TPROXY(tcp) + TPROXY(udp) ## 纯 tproxy 模式 ##
# false: REDIRECT(tcp) + TPROXY(udp) ## redirect 模式 ##
## tcponly
tcponly='false' # true:仅代理TCP流量 | false:代理TCP和UDP流量
## selfonly
selfonly='true' # true: 只代理ss-tproxy主机(本机)传出的流量
## proxy
proxy_tcpport='12345' # ss/ssr/v2ray/ipt2socks 等本机进程的 TCP 监听端口,该端口支持"透明代理"
proxy_udpport='12345' # ss/ssr/v2ray/ipt2socks 等本机进程的 UDP 监听端口,该端口支持"透明代理"
这里端口
12345
要和前面Xray
的入站端口对应上。
配置完成后,第一次执行前还需要进行一些准备工作。
# 更新 gfwlist.txt
ss-tproxy update-gfwlist
# 更新 chnlist.txt
ss-tproxy update-chnlist
# 更新 chnroute.txt
ss-tproxy update-chnroute
# 将 Xray 运行的用户组设置为 proxy
ss-tproxy set-proxy-group xray
sed -i '/^User=/a Group=proxy' /etc/systemd/system/xray.service
systemctl daemon-reload
systemctl restart xray
# 将 chinadns-ng 运行的用户组设置为 proxy-dns
ss-tproxy set-dns-group chinadns-ng
现在,执行 ss-tproxy start
就可以启动透明代理了。
[root@localhost ~]# ss-tproxy start
mode: chnroute
proxy/tcp: [running]
proxy/udp: [running]
chinadns: [running]
启动成功后,本地的流量就会根据地域进行分流,需要转发的流量经由透明代理的 12345
端口转发给 Xray
处理。
代理局域网流量
代理局域网流量需要修改 /etc/ss-tproxy/ss-tproxy.conf
中的 selfonly
配置项为 false
,这样你前面安装的透明代理服务就可以作为旁路由来使用了。
然后需要代理的主机需要修改网关为透明代理服务器的 IP,即可实现流量代理,而无需设置系统代理了。
代理 Docker 流量
做完前面的步骤后,你会发现本机 Docker
容器里依然无法通过代理上网,当然最简单的方式是使用 --network=host
参数启动容器以使用本机网络代理上网。但是如果你想使用网桥模式时,会发现 Docker
发出的流量无法被代理,原因分析如下。
Docker
默认会加载 br_netfilter
内核模块,用于桥接网络,也就是 docker0
。br_netfilter
引入了几个内核参数,位于 /proc/sys/net/bridge/
目录,主要关心这两个:
- bridge-nf-call-iptables:在桥接层(2层)调用 iptables(3层)规则,默认为 1(启用)
- bridge-nf-call-ip6tables:在桥接层(2层)调用 ip6tables(3层)规则,默认为 1(启用)
bridge(网桥)是 Linux 内核在软件层面实现的一个虚拟“交换机”。
在 网桥/数据链路层/L2
上下文调用 网络层/L3
是有问题的,违反了网络分层模型;因为 iptables
相关代码都是假设它们在 网络层/L3
上下文被调用,这是完全合理的假设;在网桥中启用 call-iptables
功能后,这些 iptables
代码可能就会出现问题,因为调用这些函数时,所处的上下文是 网桥/数据链路层/L2
层。
回到透明代理,按照正常的顺序,会先调用 IP 层的 ip_rcv_core
函数,然后再调用 netfilter
的 PREROUTING
钩子,调用 xt_TPROXY
的代码,执行透明代理操作。
- ip_rcv_core 中,会将 skb->sk 置空,https://elixir.bootlin.com/linux/v6.3.8/source/net/ipv4/ip_input.c#L540
- tproxy_tg4 中,会给 skb->sk 赋值,https://elixir.bootlin.com/linux/v6.3.8/source/net/netfilter/xt_TPROXY.c#L77
call-iptables
的情况下,br_netfilter
做了很多特殊处理,保证一个数据包不会多次进入同一个netfilter
钩子。因此,调用ip_rcv_core
后,数据包不会进入 IP 层的PREROUTING
钩子,也就是说tproxy_tg4
不会调用第二次。
总结:我们需要将 call-iptables
改为 0
,禁止在 L2
调用 L3
代码,如下修改 /etc/ss-tproxy/ss-tproxy.conf
中的 pre_start
钩子函数:
pre_start() {
if is_tcp_tproxy || is_enabled_udp; then
modprobe br_netfilter # 确保模块已加载,否则 sysctl 会提示找不到条目
sysctl -wq net.bridge.bridge-nf-call-iptables=0
sysctl -wq net.bridge.bridge-nf-call-ip6tables=0
sysctl -wq net.bridge.bridge-nf-call-arptables=0 # 与透明代理无关,不关也可以
fi
}
现在我们重启 ss-tproxy restart
之后,就可以在网桥模式下代理 Docker
容器的流量了。但是如果你使用了 UFW
的话,会发现还是无法代理上网,经过分析发现在 iptables
的 filter
表 ufw-not-local
规则链中被 DROP
了,所以需要将来自于 docker0
的流量放行。
iptables -t filter -I ufw-not-local -i docker0 -j RETURN
你也可以将规则加入到 /usr/lib/systemd/system/docker.service
的 ExecStartPost
和 ExecStopPost
中,如下:
...
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStartPost=/usr/sbin/iptables -t filter -I ufw-not-local -i docker0 -j RETURN
ExecStopPost=/usr/sbin/iptables -t filter -D ufw-not-local -i docker0 -j RETURN
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
...