TPROXY与Wireguard

shixudong@163.com

网上关于Linux TPROXY的中文资料已经相当丰富,我还记得初次接触TPROXY时,由于相关文档缺失OUTPUT链打标记重路由到PREROUTING 链的规则,误以为TPROXY所在网关自身无法使用,只能用于局域网。但通过局域网测试始终无法成功,前不久才知道与TPROXY所在网关使用了br_netfilter有关。其实当时已有文档提到桥接模式下同时使用ebtables和br_netfilter,局域网能正常使用TPROXY,但我一向以为常规情况下没有必要同时使用ebtables和br_netfilter;还有一个原因就是新版ebtables依托于nft,尚不支持broute,需要使用ebtables-legacy才能操作broute表,就没有进一步去测试一下broute的强制路由功能(其实质就是跳过了br_netfilter),不过也再次证明了常规情况下确实没有必要同时使用ebtables和br_netfilter

近来偶尔重新测试了一下TPROXY功能,鉴于TPROXY的中文资料已较为完备,因此本次测试相当顺利,不仅一次性通过了TPROXY所在网关自身的测试,并在局域网测试过程中重现并确认了TPROXY和br_netfilter冲突的问题。文档相关iptables规则甚至还考虑到了规避OUTPUT链重路由引起的PREROUTING 链自发自收环路问题(mangle表PREROUTING 链允许自发自收,nat表PREROUTING 链不让自发自收)。然而,对文档中摘自内核官方来源的如下规则始终存疑:

iptables -t mangle -N DIVERT

iptables -t mangle -A DIVERT -j MARK --set-mark 1

iptables -t mangle -A DIVERT -j ACCEPT

iptables -t mangle -I PREROUTING -p tcp -m socket -j DIVERT

这些规则的用途是为了避免已有连接的包二次通过TPROXY,理论上有一定的性能提升,最后一条使用I而非A,表示要让该规则放在最前,第一时间发挥作用并跳过后边N多规则。这些规则的使用也说明了另一个事实,即mangle表不使用连接跟踪机制,每个包都需要遍历mangle表(不像nat表,只需要首包遍历)。
存疑之处在于-m socket匹配的socket范围似乎太广,包括了本人和网关之间的ssh连接。偶尔在网关上启用wireguard(由其提供默认路由,即AllowedIPs = 0.0.0.0/0)后,终于发现了诡异,此时除了正在操作的ssh,其他已存在的网关ssh马上断连,新发起的ssh也无法连到网关。有时正在操作的ssh也会断连,导致再也无法ssh到网关。
经过排查分析,就是因为-m socket波及范围太广,wireguard提供默认路由时强制启用了src_valid_mark参数,两者之间发生了冲突。src_valid_mark的作用,简单说来就是带上mark去执行rp_filter机制,并且不受rp_filter参数的影响。网上有资料提到使用TPROXY时,必须关掉rp_filter,实际上这是一个错误观点。设置rp_filter=1后,接收到的TPROXY相关数据包虽然经历了重路由,但并不改变其最初进入的网卡,如src_valid_mark为0(默认值),对数据包进行反向路由验证时,策略路由并不使用mark标记,所以能顺利通过验证,也就是说,rp_filter参数取值不会影响到TPROXY数据包。然而当src_valid_mark=1时,不管rp_filter参数取值如何,内核同样对数据包进行反向路由验证,并且验证时使用接收包刚打的mark去匹配策略路由,得到的结果显而易见,进出网卡不一致,内核直接丢弃该火星包。事先执行sudo sysctl -w net.ipv4.conf.all.log_martians=1,再模拟前述操作,待ssh被动断连,就可以用dmesg看到被丢弃火星包的来源。
前述-m socket规则将网关ssh连接也打上了mark,wireguard启用src_valid_mark后,其他ssh连接无法通过反向路由验证,所以断连。反向路由验证在ip_route_input_slow函数里调用,由于Linux已默认启用ip_early_demux参数,当前正在操作的ssh连接不需要调用ip_route_input_slow函数,所以不受src_valid_mark参数影响。但如后续执行了与路由相关的命令,仍会调用ip_route_input_slow函数,导致当前ssh连接断连,此后再也无法ssh到网关,除非直接到网关机上关掉src_valid_mark参数,方能恢复ssh到网关的正常连接。
通过查阅iptables官方资料,-m socket可以带条件:--transparent和--nowildcard。查阅源码(xt_socket.c)后进一步发现,-m socket不带条件和带--nowildcard条件本质上是一致的,即匹配所有socket,包括透明代理socket和全零绑定socket。这点和官方资料说法完全不一致(官方说法-m socket默认不匹配全零绑定socket,等同于带--transparent条件,即只匹配透明代理socket)。将先前的-m socket规则补上--transparent条件后,非透明代理socket(如网关ssh连接)不再打mark,就再也不会受src_valid_mark参数的影响了。
虽然网关自身发到本机TPROXY的数据包使用lookback路径,不受反向路由验证机制影响,但TPROXY最终外出的底层数据包的回包也属于非透明代理socket,同样要受到src_valid_mark=1的影响,导致网关自身无法正常使用透明代理功能。因此-m socket增加--transparent条件后,能保障网关ssh连接以及网关自身使用TPROXY功能不受src_valid_mark参数影响。
如前面分析,src_valid_mark反向路由验证机制和TPROXY依赖的重路由机制之间必然存在冲突,这一冲突导致,即使-m socket规则增加了--transparent条件,也无法保障局域网正常使用透明代理。这是因为局域网终端连到TPROXY的数据包总是要打mark的,所以src_valid_mark=1后,局域网终端由于反向路由验证失败,无法连接TPROXY透明代理。幸好内核提供了另一个参数accept_local,可以跳过前述反向路由验证机制。因此,为保障局域网正常使用TPROXY,不是关掉rp_filter参数,而是启用accept_local参数,之后局域网也能顺利连接TPROXY。
以上分析了wireguard(提供默认路由)强制启用src_valid_mark参数后引起TPROXY策略路由和不带条件-m socket之间发生冲突及其规避措施。然而,即使启用了accept_local参数,wireguard(提供默认路由)新增的策略路由也会和TPROXY依赖的策略路由发生冲突,导致网关自身或局域网仍然无法正常使用TPROXY透明代理。
如果先开启wireguard,再添加TPROXY所需的策略路由,由于后添加的策略路由优先级更高,网关自身应用程序发起的SYN数据包在OUTPUT链打mark后必然重路由到TPROXY。但由于应用程序发起SYN数据包时,无mark查找路由,只能获取wg网卡对应的IP作为源IP,TPROXY发回的SYN+ACK数据包也回送到该IP。然而,该数据包能够抵达wg网卡IP,并不意味着能够抵达发出数据包的应用程序。wireguard就是靠截获进出wg网卡IP的数据包进行VPN通信的,最终该SYN+ACK数据包被wireguard直接丢弃,TPROXY则因为迟迟等不到应用程序本应后续发来的ACK数据包导致无法成功建立连接,透明代理功能失效。

前述问题其实不光TPOXY模式存在,REDIRECT模式也存在,REDIRECT模式虽然不涉及策略路由,但REDIRECT后数据包使用local路由,优先级最高,能保障数据包准确到达REDIRECT位置。但网关自身应用程序需要REDIRECT的SYN数据包源IP,根据路由算法,也必然是wg网卡对应的IP,导致出现TPROXY模式下同样的问题,REDIRECT功能对网关自身应用程序失效。该问题目前看来似乎无解,除非应用程序可以选择外出网卡IP以避免使用wg网卡对应的IP。
至于局域网终端,发起的SYN数据包在网关PREROUTING链打mark后同样重路由到TPROXY,但由于其全程不用经过wg网卡,SYN+ACK数据包能够顺利返回,透明代理功能正常,只是TPROXY最终外出的底层数据包通过wireguard新增的默认路由而非网关原先的默认路由出去而已。
如果先添加TPROXY所需的策略路由,再开启wireguard,基于同样道理,无论是网关自身还是局域网终端,发出的数据包将通过wireguard新增的默认路由出去,永远到达不了TPROXY,间接导致了透明代理功能失效。不过如wireguard对端也提供了TPROXY所要实现的功效,表面上反而感觉不到透明代理功能失效这一事实,在此种情形下,因为数据包始终从wireguard新增的默认路由出去,即便不启用accept_local参数,数据包也能通过src_valid_mark反向路由验证,不受该参数影响。
Wireguard在不提供默认路由的情况下,既不启用src_valid_mark参数,也不新增策略路由,完全可以和TPROXY和平共处。
综上,为尽量保证TPROXY正常发挥作用,还需完善如下措施:
一是要给iptables规则-m socket增加--transparent条件:
sudo iptables -t mangle -I PREROUTING -p tcp -m socket --transparent -j DIVERT
二是还要启用accept_local参数:
echo 1 > /proc/sys/net/ipv4/conf/all/accept_local
sudo bash -c "echo 1 > /proc/sys/net/ipv4/conf/all/accept_local"
三是在TPROXY和wireguard同时使用时,配置wireguard不提供默认路由(即不要使用AllowedIPs = 0.0.0.0/0)。

第三个措施是最为彻底的方案,不仅能解决TPROXY和wireguard的共存问题,也能解决REDIRECT和wireguard的共存问题。

新增小插曲:前面分析过,在TPROXY策略路由优先时,网关自身应用程序仍然使用wg网卡IP作为源IP,这是由于网关自身应用程序在查找路由填充源IP时,并没有打mark之故。本人使用socat的setsockopt=1:36:1测试了一下网关自身应用程序打mark后的源IP选择情况,当使用TCP:<host>:<port>或UDP:<host>:<port>,源IP也是使用wg网卡IP作为源IP,而使用UDP-SENDTO:<host>:<port>或UDP-DATAGRAM:<host>:<port>,源IP则使用目标IP,然而在网关上使用sudo ping <host> -m1却报网络不可达错误。后使用strace对比跟踪三种命令区别,意外发现socat的TCP:<host>:<port>或UDP:<host>:<port>在connect之前居然不打mark,只能无mark查找路由填充源IP,结果可想而知。经与socat官方联系,获取了临时补丁,此时socat使用TCP:<host>:<port>或UDP:<host>:<port>加setsockopt=1:36:1,结果就和ping <host> -m1保持了一致:网络不可达。
经查询内核源码,以上结果在于socket采用connect还是unconnect方式。采用connect方式,需要两次路由查找(带mark),第一次查找时没有源IP,由于路由类型查询结果是local,第一次源IP就是目标IP;第二次查找时已有源IP,同样由于路由类型是local,但local路由表无法匹配源IP,返回网络不可达错误。unconnect方式,只需查找一次路由(带mark),源IP为目标IP。ping -m1也使用了connect方式,所以在使用TPROXY策略路由后,只能返回网络不可达错误。
UDP-SENDTO或UDP-DATAGRAM使用unconnect方式,加setsockopt=1:36:1后,虽然能获取目标IP作为源IP,并且发送数据包打mark后能通过TPROXY出去,但无法接收回包。因为按照TPROXY的配套iptables规则,回包目标地址符合iptables打mark规则,仍会送到TPROXY,形成一个小环路。
以上测试和分析说明,在使用TPROXY策略路由时,应用程序自身打mark只能用于避开策略路由(比如透明代理程序自身发包需要避开策略路由),无法用于使用策略路由的情形,后者只能通过iptables规则打mark的方式实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值