1.背景介绍

场景跟前面提到的fullnat是一样的。

如下组网,ADS集群内存在计算节点1(10.0.103.96)写sysdb保存数据的情景;而sysdb的服务器可能在计算节点2(10.0.103.97)、计算节点3(10.0.103.98)上,通过SLB对外提供接入服务,如10.0.104.107:10000。 因此,计算节点1写sysdb时,需要从10.0.103.96发报文到10.0.104.107(源地址:目的地址为10.0.103.96:10.0.104.107),然后SLB做FULLNAT,将源地址、目的地址替换为10.0.103.107:10.0.103.97。

组网图

这里有一个问题,就是如何指导计算节点1将报文发送给SLB(10.0.104.107)?

2.解决方法

2.1 粗暴的解决方法

很简单,就是在各个计算节点上配置路由为SLB的内部IP(10.0.103.107),即报文直接经VLAN 60转给SLB,报文从eth1口送上去给SLB;应答报文仍然经过eth1送给计算节点。 但这要求用户手工配置计算节点的路由,在部署的时候略为麻烦(部署时还需要yum安装一些包,但这必须配置路由为网关)。

2.2 正确的解决方法

按照最早的想法,如果将计算节点的网关配置为vlan 60的地址,报文可以经过vlan 60转到vlan 70,然后从eth0口上送给SLB;SLB将报文处理完毕后,根据目的地址将应答报文再从eth1发送给计算节点。

但实测发现,SLB直接将报文丢弃了,并没有应答,连接建立失败,而且也没有应答ICMP差错报文。 linux有一个叫做反向过滤(reverse path filter)的功能,简单来说就是为了防DDOS攻击,会检查报文源地址的合法性,如果反查源地址的路由表,发现源地址下一跳的最佳出接口并不是收到报文的入接口,则将报文丢弃。所以只要把反向过滤关掉就可以搞定了。

2.2.1 配置方法

内核sysctl中的rp_filter变量可以控制反向过滤。其取值如下(定义在Documentation/networking/ip-sysctl.txt):

rp_filter - INTEGER
	0 - No source validation.
	1 - Strict mode as defined in RFC3704 Strict Reverse Path
	    Each incoming packet is tested against the FIB and if the interface
	    is not the best reverse path the packet check will fail.
	    By default failed packets are discarded.
	2 - Loose mode as defined in RFC3704 Loose Reverse Path
	    Each incoming packet's source address is also tested against the FIB
	    and if the source address is not reachable via any interface
	    the packet check will fail.

针对我们的需求,需要禁用反向过滤,所以这里将变量设置为0(其实配置为2更合适,只有不可达的才丢,回头试一下)。sysctl变量修改有多种办法,可以直接修改/proc/sys/net/ipv4/conf/${ethx}/rp_filter的值,也可以使用sysctl命令配置,不过这两种做法在机器重启以后都会失效;如果要一直生效,可以修改/etc/sysctl.conf:

# Controls source route verification
net.ipv4.conf.default.rp_filter = 0

2.2.2 代码解析

这个过程发生在报文上送阶段,由于是第一个包,所以走的是慢路径:

static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			       u8 tos, struct net_device *dev)
{
	struct fib_result res;
	struct in_device *in_dev = in_dev_get(dev);
	struct flowi fl = { .nl_u = { .ip4_u =
				      { .daddr = daddr,
					.saddr = saddr,
...
	/* 查FIB决定报文路由。原来FIB不是华三独创呀。 */
	/*
	 *	Now we are ready to route packet.
	 */
	if ((err = fib_lookup(net, &fl, &res)) != 0) {
		if (!IN_DEV_FORWARD(in_dev))
			goto e_hostunreach;
		goto no_route;
	}
...
    /* 报文是上送本机的 */
	if (res.type == RTN_LOCAL) {
		int result;
		/* 检查源地址是否合法,不合法丢弃。反向过滤就是在这里做的检查 */
		result = fib_validate_source(saddr, daddr, tos,
					     net->loopback_dev->ifindex,
					     dev, &spec_dst, &itag, skb->mark);
		if (result < 0)
			goto martian_source;
		...
	}

源地址检查:

int fib_validate_source(__be32 src, __be32 dst, u8 tos, int oif,
			struct net_device *dev, __be32 *spec_dst,
			u32 *itag, u32 mark)
{
	struct in_device *in_dev;
	struct flowi fl = { .nl_u = { .ip4_u =
				      { .daddr = src,
					.saddr = dst,
		/* 对,就是这里将源地址、目的地址做了反向,下面查询FIB会用 */
					.tos = tos } },
			    .mark = mark,
			    .iif = oif };

	struct fib_result res;
	int no_addr, rpf;
	int ret;
	struct net *net;

	no_addr = rpf = 0;
	rcu_read_lock();
	in_dev = __in_dev_get_rcu(dev);
	if (in_dev) {
		//入接口上没有地址?
		no_addr = in_dev->ifa_list == NULL;
		//入接口支持反向过滤的情况,0/1/2
		rpf = IN_DEV_RPFILTER(in_dev);
		if (mark && !IN_DEV_SRC_VMARK(in_dev))
			fl.mark = 0;
	}
	rcu_read_unlock();

	if (in_dev == NULL)
		goto e_inval;

	net = dev_net(dev);
	//反向查FIB信息
	if (fib_lookup(net, &fl, &res))
		goto last_resort;
	if (res.type != RTN_UNICAST)
		goto e_inval_res;
	*spec_dst = FIB_RES_PREFSRC(res);
	fib_combine_itag(itag, &res);
#ifdef CONFIG_IP_ROUTE_MULTIPATH
	if (FIB_RES_DEV(res) == dev || res.fi->fib_nhs > 1)
#else
	if (FIB_RES_DEV(res) == dev)
#endif
	{
		//源地址查路由的下一跳出接口,跟入接口一致,检查成功返回(多路径的情况不太一样,只要多于1条即可)
		ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
		fib_res_put(&res);
		return ret;
	}
	fib_res_put(&res);
	//查路由
	if (no_addr)
		goto last_resort;
	//1表示严格反向过滤。走到这说明出入接口不一致,rpfilter生效,返回-EINVAL,指导入报文慢路径丢弃报文。
	if (rpf == 1)
		goto e_inval;
	fl.oif = dev->ifindex;

	ret = 0;
	if (fib_lookup(net, &fl, &res) == 0) {
		if (res.type == RTN_UNICAST) {
			*spec_dst = FIB_RES_PREFSRC(res);
			ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
		}
		fib_res_put(&res);
	}
	return ret;

last_resort:
	//入接口没有地址,或者查不到路由信息的情况
	if (rpf)	//对应1或者2,都会丢掉包
		goto e_inval;
	//这儿没看懂。如果关闭了反向过滤,为什么这里要给spec_dst赋值呢?外面貌似也没用到。
	*spec_dst = inet_select_addr(dev, 0, RT_SCOPE_UNIVERSE);
	*itag = 0;
	return 0;

e_inval_res:
	fib_res_put(&res);
e_inval:
	return -EINVAL;
}

不过代码还有些地方没搞懂。比如,为什么fib_validate_source后面又查了一次fib,是为了处理rp_filter为2的情况吗?只是设置了oif。后面慢慢写写各个linux network的一些重点流程吧。 另外,我在mac上用Clion看内核源码不能跳转,哪位大神知道是神马原因吗?解决了可以获得支付宝巨额奖励一块钱。