笔者曾在网络上(用英文和中文)检索许久,未发现一个完整和完备的解决方案以供Docker容器使用透明代理。其后,笔者整合资料并通过iptables
配合策略路由实现此功能。故撰文记述方法。
透过iptables
设置connmark
与fwmark
标记连接
首先,我们需要为Docker容器内的程序发起的连接设置fwmark,以便稍后设置策略路由。iptables
的mangle
表中的MARK目标(-j MARK
)似乎可以用来达到此目的。
iptables -t mangle -A PREROUTING -i docker0 ! -d 198.18.0.0/15 -j MARK --set-mark 0x40
然而,这样做引入了一个问题,由于未有conntrack模块参与,任何由docker0
发出的,目标不是198.18.0.0/15
的包,都将被设置0x40
作为fwmark
,这包括Docker容器内的应用程序打开监听端口,接受客户端连接后返回的包,这些包也将被策略路由到透明代理所在的Interface,从而无法到达客户端。这样做的后果,便是任何Docker容器映射的端口,无法被本机(Docker Daemon所在的主机)以外的主机访问到。
我们需要将Docker容器内的程序所发起的连接所对应的封包打上fwmark
,而不是标注任何由容器发出的封包。因此,我们需要conntrack
来实现这种有状态的标注。
# 如果连接是由docker0上新发起的,且目标不是docker的IP池,将这个连接标注0x40
iptables -t mangle -A PREROUTING -i docker0 ! -d 198.18.0.0/15 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x40
# 将被标注的连接的connmark (0x40)设置为封包的fwmark
iptables -t mangle -A PREROUTING -i docker0 -j CONNMARK --restore-mark
配置策略路由
iptables已经帮我们标注了哪些封包要被透明代理,接下来,我们需要配置策略路由。
# 将透明代理的Interface作为40号路由表的默认路由
ip route add 192.168.240.1 dev tun0 table 40
# 将fwmark为0x40的封包交由40号路由表处理
ip rule add fwmark 0x40 lookup 40
此时,运转一个Docker容器,使用默认的容器网络配置,令它加入Docker的bridge网络(docker0
),该容器所发出的连接已经被路由到透明代理的Interface了。
配置的持久化
欲将iptables
的设置持久化,可使用iptables-save
与iptables-restore
命令。可在/etc/iptables/iptables.rules
中加入如下内容,并启用iptables.service(systemctl enable iptables.service
)
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -i docker0 ! -d 198.18.0.0/15 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x40
-A PREROUTING -i docker0 -j CONNMARK --restore-mark
COMMIT
欲将路由表和路由策略持久化,可用网络管理程序实现。以笔者使用的systemd-networkd
为例,为透明代理的Interface建立一个配置文件(/etc/systemd/network/50-tun0.network
):
[Match]
Name=tun0
[Network]
Address=192.168.240.1/32
[Route]
Gateway=192.168.240.1
Table=40
[RoutingPolicyRule]
FirewallMark=0x40
Table=40
docker-compose
默认地,Docker Compose为每一个栈(stack)配置一个网络,其名称则以br-
为前缀后接随机的十六进制字符,例如br-fbc1e9a448af
或br-b74d3dfcaa73
。这对iptables
的匹配造成了一些困难——如果使用br-+
(最后的+
是通配符)来匹配,不排除有其他的Interface使用br-
作为前缀。
然而,我们可以在docker-compose.yml
中追加如下内容,来修改其创建的网络接口的名称。这个例子将其名称设置为br-compose1
。
networks:
default:
driver_opts:
com.docker.network.bridge.name: br-compose1
如果你有更多Docker Compose栈,不妨在它们的docker-compose.yml中追加同样内容,而将名称设为br-compose2
,br-compose3
…
如此,将iptables
中的docker0
,替换为br-compose+
,便可对特定的Docker Compose栈实现透明代理。
注释
本文中存在如下假设,你需要根据自己的实际情况,修改本文中的命令和代码。
- Docker的
bridge
网络名称为docker0
- Docker的IP池为
198.18.0.0/15
;默认地,这个值是172.17.0.0/16
- 要被代理的连接被附加
0x40
作为connmark
,要被代理的封包被附加0x40
作为fwmark
。以上两个值不应与任何已有的iptables
规则冲突。 - 为Docker透明代理所建立的路由表编号为
40
,这不能与任何已有的路由表冲突。 - 透明代理Interface的名称为
tun0
,IP为192.168.240.1/32
后记
- 本文所述内容是为Docker容器提供透明代理,而非令Docker CLI或Docker Daemon (
dockerd
)透过代理连接网络。如果要为Docker CLI配置代理服务器,请参见https://docs.docker.com/network/proxy/;如为Docker Daemon配置代理服务器,请参见https://docs.docker.com/config/daemon/proxy/。 - 本文假设你拥有一个Interface,例如WireGuard等。如果你没有,而是拥有一个SOCKS代理服务器,可以使用tun2socks或类似软件。