内容简介:版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/86706256
版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/86706256
以下的描述仅仅针对于 Linux 内核实现的TCP/IP协议栈。
首先,让我们明确一个事实,即:
- 1. iptables的OUTPUT链在标准IP路由之后起作用
其次,让我们再明确另一个 关于IP路由 的事实,即:
- 2. 对于本地始发的流量,IP路由除了确定下一跳之外,对于没有指定源IP的数据包,还将会为其选择源IP地址
我们把上述经过iptables OUTPUT之前的标准IP路由行为简单称为 第一次路由 。
当数据包经过了iptables OUTPUT链,某条rule为其打上了fwmark或者改变了其目标地址后,由于数据包属性已经改变,需要重新路由,我们将其称作 第二次路由 。
Linux内核协议栈在实现第一次路由和第二次路由时,其逻辑是一样的。
结合上述的第1点和第2点事实,将会出现一个问题:
- 由于第一次路由时会为skb选择source地址,那么第二次路由时的命中路由条目的source属性将永远不会生效
这里的问题在于, 最终数据包被发送出去时,其源地址可能并不是期望的源地址 ,以至于 不得不在出网卡上做一个masquerading才可以 ,而我们知道,这个masquerading是饱受诟病的,因为它依赖于nf_conntrack,而nf_conntrack多年以来被人云亦云地喷了个无地自容!
总结一下,数据包的源地址取决于第一次路由的查询结果!这问题在多运营商线路接入的主机上非常显而易见:
请看上图,我们的配置如下:
# 默认路由走电信 ip route add 0.0.0.0/0 via 10.0.0.254 src 10.0.0.1 # 为特殊的数据包打标签,走联通策略路由 iptables -t mangle -A OUTPUT XXXX -j MARK --set-mark 100 ip rule add fwmark 100 table vtab # 联通的默认路由 ip route add 0.0.0.0/0 via 10.1.0.254 src 10.1.0.1 table vtab
很遗憾,由于所有的数据包在第一次路由时均匹配到了到电信的默认路由,从而获得了10.0.0.1这个源IP地址,那么即便策略路由将其导向了联通的线路,其源地址由于已经存在了,就不会再使用联通的源地址了。
这就会导致:
- 运营商的Reverse Route Filter策略会丢弃这个不属于自家AS的数据包
- 即便不会被RP丢弃,也可能会被热土豆策略乱扔( 标准正常的数据包都是冷土豆策略 )
那么怎么办?必须加上masquerading才可以:
iptable -t nat -A POSTROUTING -o $联通网卡 -j MASQUERADE
然而不是大家都不喜欢nf_conntrack吗?所以这并不是一个完美的方案!
所以说,我把上面的问题看作是一个Linux内核协议栈实现的问题!它并不完美!
不完美就改呗,于是我想做一个不依赖nf_conntrack的NAT。找到reroute那一段,即重新第二次路由的那段,在net/ipv4/netfilter/iptable_mangle.c中:
/* Reroute for ANY change. */ if (ret != NF_DROP && ret != NF_STOLEN) { iph = ip_hdr(skb); if (iph->saddr != saddr || iph->daddr != daddr || skb->mark != mark || iph->tos != tos) { err = ip_route_me_harder(skb, RTN_UNSPEC); if (err < 0) ret = NF_DROP_ERR(err); } }
简单至极,几行代码搞定:
... /* Reroute for ANY change. */ if (ret != NF_DROP && ret != NF_STOLEN) { iph = ip_hdr(skb); if (iph->saddr != saddr || iph->daddr != daddr || skb->mark != mark || iph->tos != tos) { if (sk) { inet = inet_sk(sk); if (inet && !inet->inet_saddr) { struct flowi4 fl4 = {}; // 为了重新选择源IP地址,所以flowi4的saddr清零! fl4.saddr = 0; fl4.daddr = iph->daddr; fl4.flowi4_tos = RT_TOS(iph->tos); fl4.flowi4_oif = sk->sk_bound_dev_if; fl4.flowi4_mark = skb->mark; fl4.flowi4_flags = inet_sk_flowi_flags(sk); // 新函数,改自ip_route_me_harder,接受flowi4结构体参数 err = ip_route_reroute(skb, &fl4); if (err < 0) ret = NF_DROP_ERR(err); else recheck = 1; if (saddr != fl4.saddr) { iph->saddr = fl4.saddr; inet->inet_saddr = fl4.saddr; ip_send_check(iph);// 重新计算校验码 } } } if (!recheck) { err = ip_route_me_harder(skb, RTN_UNSPEC); if (err < 0) ret = NF_DROP_ERR(err); } } }
用ping测试,结果OK,不需要NAT的masquerading规则也是可以在第二次路由的时候重新选择源IP地址。
当我用TCP测试时,没有达到预期,它没有在上述的修改后的reroute逻辑中将源IP地址改掉,依然使用的是第一次路由时确定的源IP…
Why?!
这是TCP的连接特性所决定的。
TCP在发送第一个SYN连接包之前,必须完全确定四元组,这四个元素一个也不能少,所以在connect调用发SYN包之前,必须查一遍路由,以确定源IP地址以及获取一个路由属性。
这里有点特殊的是,这次连接前的路由查找并不属于上述的 第一次路由 或者 第二次路由 中的任何一个,而只是一个纯粹的路由查找,查找过程全程是没有数据包skb参与的!所以,即便修改了OUTPUT链上的reroute逻辑,也根本无法起作用,数据包根本就不过Netfilter,甚至根本就没有数据包!
那么怎么办?
也不是没有办法,我依然在OUTPUT链的reroute处拦截数据包。在拦截到第一个SYN包后,此时它已经经过了第一次路由,在第二次路由前,按照上面的patch将其源IP在必要的时候清零。
完成以上这些步骤后,我必须将TCP socket层面的元数据也一并修改,以将新的四元组体现在这个TCP连接里保持住。
代码如下:
int ip_route_reroute(struct sk_buff *skb, struct flowi4 *fl4) { struct net *net = dev_net(skb_dst(skb)->dev); struct rtable *rt; unsigned int hh_len; rt = ip_route_output_key(net, fl4); if (IS_ERR(rt)) return PTR_ERR(rt); /* Drop old route. */ skb_dst_drop(skb); skb_dst_set(skb, &rt->dst); if (skb_dst(skb)->error) return skb_dst(skb)->error; hh_len = skb_dst(skb)->dev->hard_header_len; if (skb_headroom(skb) < hh_len && pskb_expand_head(skb, HH_DATA_ALIGN(hh_len - skb_headroom(skb)), 0, GFP_ATOMIC)) return -ENOMEM; return 0; } static unsigned int ipt_mangle_out(struct sk_buff *skb, const struct nf_hook_state *state) { struct net_device *out = state->out; unsigned int ret; struct sock *sk = skb->sk; struct inet_sock *inet; struct iphdr *iph; u_int8_t tos; __be32 saddr, daddr; u_int32_t mark; int err; /* root is playing with raw sockets. */ if (skb->len < sizeof(struct iphdr) || ip_hdrlen(skb) < sizeof(struct iphdr)) return NF_ACCEPT; /* Save things which could affect route */ mark = skb->mark; iph = ip_hdr(skb); saddr = iph->saddr; daddr = iph->daddr; tos = iph->tos; ret = ipt_do_table(skb, NF_INET_LOCAL_OUT, state, dev_net(out)->ipv4.iptable_mangle); /* Reroute for ANY change. */ if (ret != NF_DROP && ret != NF_STOLEN) { int recheck = 0; iph = ip_hdr(skb); if (iph->saddr != saddr || iph->daddr != daddr || skb->mark != mark || iph->tos != tos) { struct tcphdr *th = NULL; if (sk) { inet = inet_sk(sk); if (inet && iph->protocol == IPPROTO_TCP) { struct tcp_sock *tp = tcp_sk(sk); th = tcp_hdr(skb); // 只NAT第一个SYN包 if ((tcp_flag_word (th) & TCP_FLAG_SYN) && !(tcp_flag_word (th) & TCP_FLAG_ACK) && // 这里的本意是想过滤FastOpen的,但没有成功... 1/*tp->tcp_header_len == skb->len*/) { goto doit; } } if (inet && !inet->inet_saddr) { struct flowi4 fl4 = {}; doit: fl4.saddr = 0; fl4.daddr = iph->daddr; fl4.flowi4_tos = RT_TOS(iph->tos); fl4.flowi4_oif = sk->sk_bound_dev_if; fl4.flowi4_mark = skb->mark; fl4.flowi4_flags = inet_sk_flowi_flags(sk); err = ip_route_reroute(skb, &fl4); if (err < 0) ret = NF_DROP_ERR(err); if (saddr != fl4.saddr) { iph->saddr = fl4.saddr; inet->inet_saddr = fl4.saddr; ip_send_check(iph); // 此以上对应三层的NAT修正 if (th) { // 下面为TCP的NAT修正 __be16 oldport = th->source; // 转换源IP地址 inet->inet_rcv_saddr = inet->inet_saddr; // 为保证四元组的唯一性,必要时,需要重新选择sport,重新hash inet_unhash(sk); inet_put_port(sk); err = inet_hash_connect(&tcp_death_row, sk); // 转换源端口 th->source = inet->inet_sport = htons(inet->inet_num); if (err) { ret = -err; goto out; } // 重新计算校验码! inet_proto_csum_replace2(&th->check, skb, oldport, th->source, 0); inet_proto_csum_replace4(&th->check, skb, saddr, fl4.saddr, 1); } } recheck = 1; } } if (!recheck) { err = ip_route_me_harder(skb, RTN_UNSPEC); if (err < 0) ret = NF_DROP_ERR(err); } } } out: return ret; }
用Netcat进行TCP测试,结果是OK的。
想说点形而上的理解。对于本地始发以及本地终结的流量的数据包,我认为在socket层面做NAT效率会更高,因为socket本身就是一个连接跟踪,本地始发或者本地终结数据包没有必要再来一层nf_conntrack了。但是这样做的不合理性是对 特殊逻辑进行了特殊处理 ,这并不是一种良好的作风。
尽可能用统一的方法处理所有的问题 才是好的,但是没有万金油…有时候万金油有,但起到的作用却是麻药的作用,大卫米勒( 没错,就是Linux内核社区的David Miller )就老是提供这种万金油,而几乎他每一次提供的万金油都是一剂毒药,最终造成各种各样的CPU飙高,Soft lockup等常规问题。
不要试图针对特殊场景做特殊处理,也不要企图获得万金油。
浙江温州皮鞋湿,下雨进水不会胖。
以上所述就是小编给大家介绍的《Linux内核Policy Routing & iptables 的不完美实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- linux内核设计与实现
- select,poll,epoll内核实现
- Systemtap中内核trace事件的实现
- Systemtap 中内核 trace 事件的实现
- Linux内核中的进程等待与其实现解析
- 连接跟踪 (conntrack):原理、应用及 Linux 内核实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C语言编程:一本全面的C语言入门教程(第三版)
(美)Stephen Kochan / 张小潘 / 电子社博文视点资讯有限公司 / 2006年 / 59.00元
本书是极负盛名的C语言入门经典教材,其第一版发行至今已有20年的历史。本书内容详实全面,由浅入深,示例丰富,并在每个章节后面附有部分习题,非常适合读者自学使用。除此之外,《C语言编程》一书对于C语言标准的最新进展、C语言常见开发工具以及管理C语言大型项目等重要方面,也进行了深入浅出的说明。一起来看看 《C语言编程:一本全面的C语言入门教程(第三版)》 这本书的介绍吧!