Linux内核Policy Routing & iptables 的不完美实现

栏目: 服务器 · Linux · 发布时间: 5年前

内容简介:版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! 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多年以来被人云亦云地喷了个无地自容!

总结一下,数据包的源地址取决于第一次路由的查询结果!这问题在多运营商线路接入的主机上非常显而易见:

Linux内核Policy Routing & iptables 的不完美实现

请看上图,我们的配置如下:

# 默认路由走电信
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 的不完美实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Linear Optimization

Introduction to Linear Optimization

Dimitris Bertsimas、John N. Tsitsiklis / Athena Scientific / 1997-02-01 / USD 89.00

"The true merit of this book, however, lies in its pedagogical qualities which are so impressive..." "Throughout the book, the authors make serious efforts to give geometric and intuitive explanations......一起来看看 《Introduction to Linear Optimization》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换