Linux策略路由和iptables OUTPUT链的一个细节

十一长假第一天,清晨我放飞一群白鸽

范式

如果想实现哪个网口进来的流量从哪个网口返回这么一个需求,有一个范式,我先贴出来:

iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT
iptables -t mangle -A PREROUTING -i XXX .... -j MARK --set-mark YYY
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j CONNMARK --save-mark
iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark
ip rule add fwmark YYY table YYY
ip route add $net/$mask via $gw dev $device table YYY

问题

但是这里面有一个细节,对于本机流量而言,上述的OUTPUT链上的那条规则不一定能匹配成功,其实并不是没有匹配成功,而是说数据包在匹配这条规则之前就已经被丢弃了,根本就没有机会去匹配它!这是为什么呢?

  因为Netfilter的OUTPUT HOOK点是发生在标准IP路由之后的,如果在标准的路由查找(而不是策略路由查找)中没有成功找到路由,那么数据包在进入OUTPUT链之前就会被丢掉。

  也许你会问,为什么不直接查找策略路由表呢?

  很显然,这里发生了循环依赖,策略路由依赖fwmark,而要想把fwmark打给一个数据包,就必然要让数据包经过OUTPUT链,要想让数据包经过OUTPUT链,则必然要成功找到一条常规的路由。如果你的服务器没有配置这种恰好能被数据包使用的常规路由,那么上述的范式相当于就废掉了!不过值得注意的是,这条常规路由并不是真的给感兴趣数据包路由的,它存在的目的仅仅是为了让数据包有机会进入OUTPUT链去打mark。注意到这点,有助于我们设想出一个优雅的解决方案。

  那么我们怎么解决这个问题呢?

解法1:使用socket来设置mark

既然是本地流量,那么应用层一定会有一个服务来接纳这些流量并将本地始发流量注入协议栈,做这件事的无一例外就是socket,而socket可以通过下面的方式为协议栈的数据包提前打上mark:

int mark = 100;  
setsockopt(client_socket, SOL_SOCKET, SO_MARK, &mark, sizeof(mark));  

详情请看我的另一篇文章《Linux socket设置mark的必要性》
反正数据包上的mark就是为了策略路由,因此,协议栈里的数据包就不需要下面OUTPUT链上的规则了:

iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark

这是一个好的方案,但却并不一定可行,为什么呢?因为这要涉及到编程,超出了系统管理的范畴,而且服务程序代码不是说动就动的,即便是改一行代码,也要经过无穷尽的回归测试再测试…

  因此,我们来看看解法2。

解法2:使用dummy网卡

我觉得这是一个更加优雅的方案,不需要改代码,且不会造成流量泄漏!

  不是说在OUTPUT前必须找到一条路由吗?好吧,我给!然而这条路由真的就不是让你发包的,而只是一个Dummy路由!值得注意的是,Dummy路由并不是unreachable路由,也不是Linux上的blackhole路由,它是确确实实通过网卡发数据的路由,只是该网卡发数据的方式比较怪异,它只是默默丢弃数据包。

  很好,这并不会带来任何流量的泄漏。

  这一招依赖于你对Linux系统的足够熟悉,它依赖于一个特殊设备,叫做dummy网卡,你可以通过下面的命令加载它:

modprobe dummy
ifconfig dummy0 up

然后设置一条默认路由:

ip route add 0.0.0.0/0 dev dummy0

OK,就这样!

  如果你的系统配置中已经有了默认路由,就不必添加dummy默认路由了,因为彼种情况下数据包会被既有默认路由引导到OUTPUT链,待成功restore-mark后再重新路由。因此,为了不让默认路由冲突,可能需要封装一个脚本,添加基于fwmark的策略路由或者默认路由的时候,相互check一下,逻辑比较复杂,这里就不说了。

后记

其实这个问题早在几年前就遇到并思考过,近期有位朋友在工作中也遇到了同样的问题,所以就再次追忆了往事。

  当时第一次遇到这个问题的时候,我发誓一定要解决它,这并不是一个很难解决的问题,只需要把OUPUT移到路由前面即可,类似PREROUTING那样,但是这样又产生了循环依赖,比如OUTPUT链可以匹配-o这个match,如果OUTPUT在路由前,那么-o参数哪里来呢?…

  也难怪OUTPUT的特殊性了,虽然它和PREROUTING看上去差不多,但是方向却截然相反,只能作罢!后来,我想到了修改路由查找的代码,如果没有找到路由,就撸一遍OUPUT HOOK,然后重新路由,但依然太复杂了,不优雅…抑或说,在应用程序没有绑定网卡设备的时候再撸OUTPUT?…

  再往后,各种繁乱的工作任务交织,就把这事遗忘了。

  其实,iptables相关的还有别的问题,比如下面这个:
《关于Netfilter NF_HOOK宏的outdev参数bug》

你可能感兴趣的:(Linux策略路由和iptables OUTPUT链的一个细节)