为Docker容器提供透明代理

笔者曾在网络上(用英文和中文)检索许久,未发现一个完整和完备的解决方案以供Docker容器使用透明代理。其后,笔者整合资料并通过iptables配合策略路由实现此功能。故撰文记述方法。

透过iptables设置connmarkfwmark标记连接

首先,我们需要为Docker容器内的程序发起的连接设置fwmark,以便稍后设置策略路由。iptablesmangle表中的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-saveiptables-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-fbc1e9a448afbr-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-compose2br-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

后记

  1. 本文所述内容是为Docker容器提供透明代理,而非令Docker CLI或Docker Daemon (dockerd)透过代理连接网络。如果要为Docker CLI配置代理服务器,请参见https://docs.docker.com/network/proxy/;如为Docker Daemon配置代理服务器,请参见https://docs.docker.com/config/daemon/proxy/
  2. 本文假设你拥有一个Interface,例如WireGuard等。如果你没有,而是拥有一个SOCKS代理服务器,可以使用tun2socks或类似软件。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注