TPROXY与Docker的兼容性

plupiseo  于 2023-03-07  发布在  Docker
关注(0)|答案(3)|浏览(295)

我试图理解TPROXY是如何为Docker容器构建透明代理的。
经过大量的研究,我创建了一个网络名称空间,注入了一个veth接口,并添加了TPROXY规则。下面的脚本在一个干净的Ubuntu 18.04.3上运行:

ip netns add ns0
ip link add br1 type bridge
ip link add veth0 type veth peer name veth1
ip link set veth0 master br1
ip link set veth1 netns ns0
ip addr add 192.168.3.1/24 dev br1
ip link set br1 up
ip link set veth0 up
ip netns exec ns0 ip addr add 192.168.3.2/24 dev veth1
ip netns exec ns0 ip link set veth1 up
ip netns exec ns0 ip route add default via 192.168.3.1
iptables -t mangle -A PREROUTING -i br1 -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port 1234 --tproxy-mark 0x1/0x1
ip rule add fwmark 0x1 tab 30
ip route add local default dev lo tab 30

之后,我从Cloudflare blog启动了一个玩具Python服务器:

import socket

IP_TRANSPARENT = 19

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.IPPROTO_IP, IP_TRANSPARENT, 1)

s.bind(('127.0.0.1', 1234))
s.listen(32)
print("[+] Bound to tcp://127.0.0.1:1234")
while True:
    c, (r_ip, r_port) = s.accept()
    l_ip, l_port = c.getsockname()
    print("[ ] Connection from tcp://%s:%d to tcp://%s:%d" % (r_ip, r_port, l_ip, l_port))
    c.send(b"hello world\n")
    c.close()

最后,通过运行ip netns exec ns0 curl 1.2.4.8,我能够观察到从192.168.3.21.2.4.8的连接,并接收到“hello world”消息。
问题是它似乎与Docker有兼容性问题。在干净的环境中一切都运行得很好,但一旦我启动Docker,事情就开始出错。看起来TPROXY规则不再起作用。运行ip netns exec ns0 curl 192.168.3.1会导致“连接重置”,运行ip netns exec ns0 curl 1.2.4.8会超时(两者都应该产生了“Hello World”消息)我尝试恢复所有iptables规则,删除Docker生成的IP路由和规则并关闭Docker,但即使我没有配置任何网络或容器,也没有一个工作。
幕后发生了什么?如何让TPROXY正常工作?

r8xiu3jd

r8xiu3jd1#

我跟踪了Docker使用strace -f dockerd创建的所有进程,并查找了包含exec的行,大多数命令是iptables命令,我已经排除了这些命令,而包含modprobe的行看起来很有趣,我逐一加载了这些模块,并计算出导致问题的模块是br_netfilter
该模块通过iptablesip6tablesarptables过滤桥接数据包,可以通过执行echo "0" | sudo tee /proc/sys/net/bridge/bridge-nf-call-iptables禁用iptables部分,执行命令后脚本正常工作,不影响Docker容器。
我仍然很困惑,我不明白这样设置的后果,我启用了包跟踪,但是在启用bridge-nf-call-iptables之前和之后,包似乎符合完全相同的规则集,但是在前一种情况下,第一个TCP SYN包被发送到Python服务器,在后一种情况下,包由于未知原因被丢弃。

vkc1a9a2

vkc1a9a22#

尝试使用-p 1234运行Docker
默认情况下,创建容器时,它不会向外部发布其任何端口。要使端口可用于Docker外部的服务,或可用于未连接到容器网络的Docker容器,请使用--publish或-p标志。
https://docs.docker.com/config/containers/container-networking/

svdrlsy4

svdrlsy43#

我遇到了同样的问题,终于弄明白了为什么TProxy与Docker不兼容。
默认情况下,docker为container创建一个网桥网络。由于网桥是第二层设备,container之间交换的数据包不在iptables的作用域范围内。它们是交换的而不是路由的。因此docker依赖bridge-netfilter在桥接接口之间执行iptable规则。StackExchange上的两篇优秀文章详细总结了bridge-netfilter的历史。

像IP转发路径上的链一样,Netfilter在网桥的数据路径上添加钩子。bridge-netfilter在这些网桥钩子上调用iptable钩子,这些钩子通常由网桥代码中的IP层调用。一个博客(http://devel.aanet.ru/linux-bridge/)解释了网桥钩子和IP钩子如何混合使用,以及bridge-netfilter如何确保每个钩子只被调用一次。
与我们的问题相关的情况是,当一个包从桥接接口发送到本地进程时,即从对接器发送到主机中运行的代理程序时,它遍历钩子NF_BR_PREROUTING(调用NF_IENT_PRE_ROUTING)进入主机IP层,从主机IP层的Angular 来看,是从网桥接口进入的数据包,为了解决这个问题,bridge-netfilter插入了一个特殊的钩子,如果它检测到这个包是从桥上交换过来的,因此这些钩子已经被桥调用过,那么这个钩子就跳过所有的NF_IENT_PRE_ROUTING钩子。
PREROUTING链上的TPROXY规则在包进入IP层之前在桥代码内部被调用,但是IP层中的ip_rcv_core函数假设Netfilter代码在它之后被调用,因此它清除sk_buff中的TPROXY规则设置的sock

  • https://elixir.bootlin.com/linux/latest/source/net/ipv4/ip_input.c#L540

这是一个已知问题,已发送修补程序来解决此问题。

但是这个补丁并不受欢迎,因为bridge-netfilter并不是一个受欢迎的特性,一旦它的功能被nftables这样的工具取代,预计它将在未来被删除。

总之,我们很遗憾地不能将Docker与TPROXY结合使用,除非出现以下情况之一:

  • Docker不支持bridge-netfilter
  • 内核接受修复TPROXY与bridge-netfilter兼容性的补丁。(不太可能)。

但一个看似有前途的替代方案是纤毛社区提供的eBPF TPROXY支持:

由于eBPF允许将socket转发到设备层的TPROXY(TC Ingress),因此遇到了与bridge-netfilter相同的问题(ip_rcv_core重置了选中的sock),但它成功修复了上游,虽然没有经过验证,但我认为可以使用eBPF程序为Docker容器构建一个基于TPROXY的透明代理。
另一个选择是基于REDIRECT(DNAT)的透明代理,它经过了仔细的考虑,并得到了bridge-netfilter的支持。

相关问题