kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

IPIP实现IP隧道

https://blog.csdn.net/kkdelta/article/details/39611061

IPIP实现IP隧道的简单示例

两台主机,A和B,每台主机由两块网卡,其中eth0在同一个网段,能够互相连通。

A的eth1和B的eth1分别在两个不同的网段。

A: eth0:192.168.9.5 eth1:192.168.8.5

B: eth0:192.168.9.6 eth1:192.168.10.6

A:

1
2
3
4
ip tun add lxT mode ipip remote 192.168.9.6 local 192.168.9.5
ip link set lxT up
ip add add 192.168.200.1 brd 255.255.255.255 peer 192.168.200.2 dev lxT
ip ro add 192.168.200.0/24 via 192.168.200.1

B:

1
2
3
4
ip tun add lxT mode ipip remote 192.168.9.5 local 192.168.9.6
ip link set lxT up
ip add add 192.168.200.2 brd 255.255.255.255 peer 192.168.200.1 dev lxT
ip ro add 192.168.200.0/24 via 192.168.200.2

在A机器添加路由信息,指定到192.168.10.6通过lxT

1
ip ro add 192.168.10.6/32 dev lxT

在B机器添加路由信息,指定到192.168.8.5通过lxT

1
ip ro add 192.168.8.5/32 dev lxT

这样 192.168.8.5 和 192.168.10.6 就可以相互ping通了

部分参数

ttl N 设置进入通道数据包的TTL为N。N是一个1—255之间的数字。0是一个特殊的值,表示这个数据包的TTL值是继承(inherit)的。ttl参数的缺省值是:inherit。

tos T或者dsfield T 设置进入通道数据包的TOS域,缺省是inherit。

mode MODE 设置通道模式。有效的模式包括:ipip、sit和gre。

nopmtudisc 在这个通道上禁止路径最大传输单元发现( Path MTU Discovery)。默认情况下,这个功能是打开的。注意:这个选项和固定的ttl是不兼容的,如果使用了固定的ttl参数,系统会打开路径最大传输单元发现( Path MTU Discovery)功能。

ip tunnel gw

CLIENT:

1
2
ifconfig eth1 11.0.0.20/24
route add -net 14.0.0.0/24 gw 11.0.0.1 dev eth1

RS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ifconfig eth0 192.168.1.191/24
modprobe ipip # ip tun add tunl0 mode ipip remote any local any
ip link set tunl0 mtu 1480 up
ifconfig tunl0:0 14.0.0.1/24

ip tun add tunl1 mode ipip remote 12.0.0.1 local 12.0.0.102 dev eth1          # better
#ip tun add tunl1 mode ipip remote 192.168.1.102 local 192.168.1.191 dev eth0  # upload err
ip link set tunl1 mtu 1480 up

ip rule add from 14.0.0.1 table 1
ip route add table 1 default dev tunl1

find /proc/ -name rp_filter -exec cat {} \;
find /proc/ -name rp_filter -exec sh -c "echo 0 > {}" \;

GW:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
find /proc/ -name rp_filter -exec cat {} \;
find /proc/ -name rp_filter -exec sh -c "echo 0 > {}" \;

echo 1 > /proc/sys/net/ipv4/ip_forward

modprobe ipip # ip tun add tunl0 mode ipip remote any local any
ip link set tunl0 mtu 1480 up

ip tun add tunl1 mode ipip remote 192.168.1.191 local 192.168.1.102 dev enp3s0     # better
#ip tun add tunl1 mode ipip remote 12.0.0.102 local 12.0.0.1 dev enx00e04b367c0c    # upload err?
ip link set tunl1 mtu 1480 up

ip rule add from 11.0.0.20 table 1
ip route add table 1 default dev tunl1

linux策略路由

https://blog.csdn.net/guodong1010/article/category/6149064

https://www.cnblogs.com/iceocean/articles/1594488.html

1.策略路由介绍

策略性是指对于IP包的路由是以网络管理员根据需要定下的一些策略为主要依据进行路由的。例如我们可以有这样的策略:“所有来直自网A的包,选择X路径;其他选择Y路径”,或者是“所有TOS为A的包选择路径F;其他选者路径K”。

Cisco 的网络操作系统 (Cisco IOS) 从11.0开始就采用新的策略性路由机制。而Linux是在内核2.1开始采用策略性路由机制的。策略性路由机制与传统的路由算法相比主要是引入了多路由表以及规则的概念。

2.linux方式

2.1 多路由表(multiple Routing Tables)

传统的路由算法是仅使用一张路由表的。但是在有些情形底下,我们是需要使用多路由表的。例如一个子网通过一个路由器与外界相连,路由器与外界有两条线路相连,其中一条的速度比较快,一条的速度比较慢。对于子网内的大多数用户来说对速度并没有特殊的要求,所以可以让他们用比较慢的路由;但是子网内有一些特殊的用户却是对速度的要求比较苛刻,所以他们需要使用速度比较快的路由。如果使用一张路由表上述要求是无法实现的,而如果根据源地址或其它参数,对不同的用户使用不同的路由表,这样就可以大大提高路由器的性能。

2.2 规则(rule)

规则是策略性的关键性的新的概念。我们可以用自然语言这样描述规则,例如我门可以指定这样的规则:

规则一:“所有来自192.16.152.24的IP包,使用路由表10,本规则的优先级别是1500”

规则二:“所有的包,使用路由表253,本规则的优先级别是32767”

我们可以看到,规则包含3个要素:

什么样的包,将应用本规则(所谓的SELECTOR,可能是filter更能反映其作用);

符合本规则的包将对其采取什么动作(ACTION),例如用那个表;

本规则的优先级别。优先级别越高的规则越先匹配(数值越小优先级别越高)。

3. linux策略路由配置方式

传统的linux下配置路由的工具是route,而实现策略性路由配置的工具是iproute2工具包。

3.1 接口地址的配置 IP Addr

对于接口的配置可以用下面的命令进行:

1
Usage: ip addr [ add | del ] IFADDR dev STRING

例如:

1
router># ip addr add 192.168.0.1/24 broadcast 192.168.0.255 label eth0 dev eth0

上面表示,给接口eth0赋予地址192.168.0.1 掩码是255.255.255.0(24代表掩码中1的个数),广播地址是192.168.0.255

3.2 路由的配置 IP Route

Linux最多可以支持255张路由表,其中有3张表是内置的:

1
2
3
4
5
6
7
  表255 本地路由表(Local table)本地接口地址,广播地址,已及NAT地址都放在这个表。该路由表由系统自动维护,管理员不能直接修改。

  表254 主路由表(Main table)如果没有指明路由所属的表,所有的路由都默认都放在这个表里,一般来说,旧的路由工具(如route)所添加的路由都会加到这个表。一般是普通的路由。

  表253 默认路由表(Default table)一般来说默认的路由都放在这张表,但是如果特别指明放的也可以是所有的网关路由。

  表 0 保留

路由配置命令的格式如下:

Usage: ip route list SELECTOR
1
ip route { change | del | add | append | replace | monitor } ROUTE

如果想查看路由表的内容,可以通过命令:

1
ip route list table table_number

对于路由的操作包括change、del、add 、append 、replace 、 monitor这些。例如添加路由可以用:

1
2
router># ip route add 0/0 via 192.168.0.4 table main
router># ip route add 192.168.3.0/24 via 192.168.0.3 table 1

第一条命令是向主路由表(main table)即表254添加一条路由,路由的内容是设置192.168.0.4成为网关。

第二条命令代表向路由表1添加一条路由,子网192.168.3.0(子网掩码是255.255.255.0)的网关是192.168.0.3。

在多路由表的路由体系里,所有的路由的操作,例如网路由表添加路由,或者在路由表里寻找特定的路由,需要指明要操作的路由表,所有没有指明路由表,默认是对主路由表(表254)进行操作。而在单表体系里,路由的操作是不用指明路由表的。

3.3 规则的配置 IP Rule

在Linux里,总共可以定义232个优先级的规则,一个优先级别只能有一条规则,即理论上总共可以有条规则。其中有3个规则是默认的。命令用法如下:

1
2
3
4
5
6
7
8
9
10
Usage: ip rule [ list | add | del ] SELECTOR ACTION

SELECTOR := [ from PREFIX ] [ to PREFIX ] [ tos TOS ]
	[ dev STRING ] [ pref NUMBER ]

ACTION := [ table TABLE_ID ] [ nat ADDRESS ]
	[ prohibit | reject | unreachable ]
	[ flowid CLASSID ]

TABLE_ID := [ local | main | default | new | NUMBER

首先我们可以看看路由表默认的所有规则:

1
2
3
4
root@netmonster# ip rule list
0: from all lookup local
32766: from all lookup main
32767: from all lookup default

规则0,它是优先级别最高的规则,规则规定,所有的包,都必须首先使用local表(254)进行路由。本规则不能被更改和删除。

规则32766,规定所有的包,使用表main进行路由。本规则可以被更改和删除。

规则32767,规定所有的包,使用表default进行路由。本规则可以被更改和删除。

在默认情况下进行路由时,首先会根据规则0在本地路由表里寻找路由,如果目的地址是本网络,或是广播地址的话,在这里就可以找到合适的路由;如果路由失败,就会匹配下一个不空的规则,在这里只有32766规则,在这里将会在主路由表里寻找路由;如果失败,就会匹配32767规则,即寻找默认路由表。如果失败,路由将失败。重这里可以看出,策略性路由是往前兼容的。

还可以添加规则:

1
2
router># ip rule add [from 0/0] table 1 pref 32800
router >#ip rule add from 192.168.3.112/32 [tos 0x10] table 2 pref 1500prohibit

第一条命令将向规则链增加一条规则,规则匹配的对象是所有的数据包,动作是选用路由表1的路由,这条规则的优先级是32800。

第二条命令将向规则链增加一条规则,规则匹配的对象是IP为192.168.3.112,tos等于0x10的包,使用路由表2,这条规则的优先级是1500,动作是。添加以后,我们可以看看系统规则的变化。

1
2
3
4
5
6
router># ip rule
0: from all lookup local
1500 from 192.168.3.112/32 [tos 0x10] lookup 2
32766: from all lookup main
32767: from all lookup default
32800: from all lookup 1

上面的规则是以源地址为关键字,作为是否匹配的依据的。除了源地址外,还可以用以下的信息:

From – 源地址

To – 目的地址(这里是选择规则时使用,查找路由表时也使用)

Tos – IP包头的TOS(type of sevice)域

Dev – 物理接口

Fwmark – 防火墙参数

采取的动作除了指定表,还可以指定下面的动作:

Table 指明所使用的表

Nat 透明网关

Action prohibit 丢弃该包,并发送 COMM.ADM.PROHIITED的ICMP信息

Reject 单纯丢弃该包

Unreachable丢弃该包,并发送 NET UNREACHABLE的ICMP信息

4.策略路由的应用

4.1 基于源地址选路( Source-Sensitive Routing)

如果一个网络通过两条线路接入互联网,一条是比较快的ADSL,另外一条是比较慢的普通的调制解调器。这样的话,网络管理员既可以提供无差别的路由服务,也可以根据源地址的不同,使一些特定的地址使用较快的线路,而普通用户则使用较慢的线路,即基于源址的选路。

4.2 根据服务级别选路( Quality of Service)

网络管理员可以根据IP报头的服务级别域,对于不同的服务要求可以分别对待对于传送速率、吞吐量以及可靠性的有不同要求的数据报根据网络的状况进行不同的路由。

4.3 节省费用的应用

网络管理员可以根据通信的状况,让一些比较大的阵发性通信使用一些带宽比较高但是比较贵的路径一段短的时间,然后让基本的通信继续使用原来比较便宜的基本线路。例如,管理员知道,某一台主机与一个特定的地址通信通常是伴随着大量的阵发性通信的,那么网络管理员可以安排一些策略,使得这些主机使用特别的路由,这些路由是按需拨号,带宽比较高的线路,通信完成以后就停止使用,而普通的通信则不受影响。这样既提高网络的性能,又能节省费用。

4.4 负载平衡(Load Sharing)

根据网络交通的特征,网络管理员可以在不同的路径之间分配负荷实现负载平衡。

5 linux下策略路由的实现

在Linux下,策略性路由是由RPDB实现的。对于RPDB的内部机制的理解,可以加深对于策略性路由使用的理解。文件主要包含:

1
2
3
4
5
fib_hash.c
fib_rules.c
fib_sematic
fib_frontend.c
route.c

RDPB主要由多路由表和规则组成。路由表以及对其的操作和其对外的接口是整个RPDB的核心部分。路由表主要由table,zone,node这些主要的数据结构构成。对路由表的操作主要包含物理的操作以及语义的操作。路由表除了向IP层提供路由寻找的接口以外还必须与几个元素提供接口:与用户的接口(即更改路由)、proc的接口、IP层控制接口、以及和硬件的接口(网络接口的改变会导致路由表内容的改变)。处在RDPB的中心的规则,由规则选取表。IP层并不直接使用路由表,而是通过一个路由适配层,路由适配层提供为IP层提供高性能的路由服务。

5.1 路由表(Fib Table)

数据结构:

在整个策略性路由的框架里,路由表是最重要的的数据结构,我们在上面以及对路由表的概念和结构进行了清楚的说明。Linux里通过下面这些主要的数据结构进行实现的。

1
2
3
4
5
6
7
主要数据结构       作用          位置
struct fib_table  路由表           ip_fib.h 116
struct fn_hash        路由表的哈希数据    fib_hash.c 104
struct fn_zone        zone域         fib_hash.c 85
struct fib_node       路由节点        fib_hash.c 68
struct fib_info       路由信息        ip_fib.h 57
struct fib_result 路由结果        ip_fib.h 86

数据结构之间的主要关系如下。路由表由路由表号以及路由表的操作函数指针还有表数据组成。这里需要注意的是,路由表结构里并不直接定义zone域,而是通过一个数据指针指向fn_hash。只有当zone里有数据才会连接到fn_zone_list里。

系统的所有的路由表由数组变量*fib_tables[RT_TABLE_MAX+1]维护,其中系统定义RT_TABLE_MAX为254,也就是说系统最大的路由表为255张,所有的路由表的操作都是对这个数组进行的。。同时系统还定义了三长路由表*local_table; *main_table

路由表的操作:

Linux策略路由代码的主要部分是对路由表的操作。对于路由表的操作,物理操作是直观的和易于理解的。对于表的操作不外乎就是添加、删除、更新等的操作。还有一种操作,是所谓的语义操作,语义操作主要是指诸如计算下一条的地址,把节点转换为路由项,寻找指定信息的路由等。

1、物理操作(operation):

路由表的物理操作主要包括如下这些函数:

路由标操作实现函数 位置

新建路由表

删除路由表

搜索路由 fn_hash_lookup fib_hash.c 269

插入路由到路由表 fn_hash_insert fib_hash.c 341

删除路由表的路由 fn_hash_delete

fn_hash_dump

fib_hash.c 433

fib_hash.c 614

更新路由表的路由 fn_hash_flush fib_hash.c 729

显示路由表的路由信息 fn_hash_get_info fib_hash.c 750

选择默认路由 fn_hash_select_default fib_hash.c 842

2、语义操作(semantics operation):

语义操作并不涉及路由表整体框架的理解,而且,函数名也是不言自明的,所以请大家参考fib_semantics.c。

3、接口(front end)

对于路由表接口的理解,关键在于理解那里有

IP

首先是路由表于IP层的接口。路由在目前linux的意义上来说,最主要的还是IP层的路由,所以和IP层的的接口是最主要的接口。和ip层的衔接主要是向IP层提供寻找路由、路由控制、寻找指定ip的接口。

Fil_lookup

ip_rt_ioctl fib_frontend.c 286;“ f

ip_dev_find 145

Inet

路由表还必须提供配置接口,即用户直接操作路由的接口,例如增加和删除一条路由。当然在策略性路由里,还有规则的添加和删除。

inet_rtm_delroute 351

inet_rtm_newroute 366

inet_check_attr 335

proc

在 /proc/net/route 里显示路由信息。

fib_get_procinfo

4、网络设备(net dev event)

路由是和硬件关联的,当网络设备启动或关闭的时候,必须通知路由表的管理程序,更新路由表的信息。

fib_disable_ip 567

fib_inetaddr_event 575

fib_netdev_event

5、内部维护( magic)

上面我们提到,本地路由表(local table)的维护是由系统自动进行的。也就是说当用户为硬件设置IP地址等的时候,系统自动在本地路由表里添加本地接口地址以及广播地址。

fib_magic 417

fib_add_ifaddr 459

fib_del_ifaddr 498

Rule

1、数据结构

规则在fib_rules.c的52行里定义为 struct fib_rule。而RPDB里所有的路由是保存在101行的变量fib_rules里的,注意这个变量很关键,它掌管着所有的规则,规则的添加和删除都是对这个变量进行的。

2、系统定义规则:

fib_rules被定义以后被赋予了三条默认的规则:默认规则,本地规则以及主规则。

u 本地规则local_rule

1
2
3
4
5
6
static struct fib_rule local_rule = {
	r_next: &main_rule, /*下一条规则是主规则*/
	r_clntref: ATOMIC_INIT(2),
	r_table: RT_TABLE_LOCAL, /*指向本地路由表*/
	r_action: RTN_UNICAST, /*动作是返回路由*/
};

u 主规则main_rule

1
2
3
4
5
6
7
static struct fib_rule main_rule = {
	r_next: &default_rule,/*下一条规则是默认规则*/
	r_clntref: ATOMIC_INIT(2),
	r_preference: 0x7FFE, /*默认规则的优先级32766*/
	r_table: RT_TABLE_MAIN, /*指向主路由表*/
	r_action: RTN_UNICAST, /*动作是返回路由*/
};

u 默认规则default rule

1
2
3
4
5
6
static struct fib_rule default_rule = {
	r_clntref: ATOMIC_INIT(2),
	r_preference: 0x7FFF,/*默认规则的优先级32767*/
	r_table: RT_TABLE_DEFAULT,/*指默认路由表*/
	r_action: RTN_UNICAST,/*动作是返回路由*/
};

规则链的链头指向本地规则。

RPDB的中心函数fib_lookup

现在到了讨论RPDB的实现的的中心函数fib_lookup了。RPDB通过提供接口函数fib_lookup,作为寻找路由的入口点,在这里有必要详细讨论这个函数,下面是源代码:,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
310 int fib_lookup(const struct rt_key *key, struct fib_result *res)
311 {
312   int err;
313   struct fib_rule *r, *policy;
314   struct fib_table *tb;
315
316   u32 daddr = key->dst;
317   u32 saddr = key->src;
318
321   read_lock(&fib_rules_lock);
322   for (r = fib_rules; r; r=r->r_next) {/*扫描规则链fib_rules里的每一条规则直到匹配为止*/
323       if (((saddr^r->r_src) & r->r_srcmask) ||
324           ((daddr^r->r_dst) & r->r_dstmask) ||
325 #ifdef CONFIG_IP_ROUTE_TOS
326           (r->r_tos && r->r_tos != key->tos) ||
327 #endif
328 #ifdef CONFIG_IP_ROUTE_FWMARK
329           (r->r_fwmark && r->r_fwmark != key->fwmark) ||
330 #endif
331           (r->r_ifindex && r->r_ifindex != key->iif))
332       continue;/*以上为判断规则是否匹配,如果不匹配则扫描下一条规则,否则继续*/
335       switch (r->r_action) {/*好了,开始处理动作了*/
336       case RTN_UNICAST:/*没有设置动作*/
337       case RTN_NAT: /*动作nat ADDRESS*/
338           policy = r;
339           break;
340       case RTN_UNREACHABLE: /*动作unreachable*/
341           read_unlock(&fib_rules_lock);
342           return -ENETUNREACH;
343       default:
344           case RTN_BLACKHOLE:/* 动作reject */
345           read_unlock(&fib_rules_lock);
346           return -EINVAL;
347       case RTN_PROHIBIT:/* 动作prohibit */
348           read_unlock(&fib_rules_lock);
349           return -EACCES;
350       }
351       /*选择路由表*/
352       if ((tb = fib_get_table(r->r_table)) == NULL)
353           continue;
		/*在路由表里寻找指定的路由*/
354       err = tb->tb_lookup(tb, key, res);
355       if (err == 0) {/*命中目标*/
356           res->r = policy;
357           if (policy)
358               atomic_inc(&policy->r_clntref);
359           read_unlock(&fib_rules_lock);
360           return 0;
361       }
362       if (err < 0 && err != -EAGAIN) {/*路由失败*/
363           read_unlock(&fib_rules_lock);
364           return err;
365       }
366   }
368   read_unlock(&fib_rules_lock);
369   return -ENETUNREACH;
370 }

上面的这段代码的思路是非常的清晰的。首先程序从优先级高到低扫描所有的规则,如果规则匹配,处理该规则的动作。如果是普通的路由寻址或者是nat地址转换的换,首先从规则得到路由表,然后对该路由表进行操作。这样RPDB终于清晰的显现出来了。

5.2 IP层路由适配(IP route)

路由表以及规则组成的系统,可以完成路由的管理以及查找的工作,但是为了使得IP层的路由工作更加的高效,linux的路由体系里,route.c里完成大多数IP层与RPDB的适配工作,以及路由缓冲(route cache)的功能。

调用接口

IP层的路由接口分为发送路由接口以及接收路由接口:

发送路由接口

IP层在发送数据时如果需要进行路由工作的时候,就会调用ip_route_out函数。这个函数在完成一些键值的简单转换以后,就会调用ip_route_output_key函数,这个函数首先在缓存里寻找路由,如果失败就会调用ip_route_output_slow,ip_route_output_slow里调用fib_lookup在路由表里寻找路由,如果命中,首先在缓存里添加这个路由,然后返回结果。

ip_route_out route.h

ip_route_output_key route.c 1984;

ip_route_output_slow route.c 1690;"

接收路由接口

IP层接到一个数据包以后,如果需要进行路由,就调用函数ip_route_input,ip_route_input现在缓存里寻找,如果失败则ip_route_inpu调用ip_route_input_slow, ip_route_input_slow里调用fib_lookup在路由表里寻找路由,如果命中,首先在缓存里添加这个路由,然后返回结果。

ip_route_input_slow route.c 1312;“ f

ip_route_input route.c 1622;“ f

cache

路由缓存保存的是最近使用的路由。当IP在路由表进行路由以后,如果命中就会在路由缓存里增加该路由。同时系统还会定时检查路由缓存里的项目是否失效,如果失效则清除。

反向路径过滤 -- reverse path filt

find /proc/ -name rp_filter -exec cat {} \;

find /proc/ -name rp_filter -exec sh -c “echo 0 > {}” \;


http://blog.chinaunix.net/uid-20417916-id-3050031.html

反向路径过滤 – reverse path filter

一、原理

先介绍个非对称路由的概念

参考《Understanding Linux Network Internals》三十章,

30.2. Essential Elements of Routing

Symmetric routes and asymmetric routes

Usually, the route taken from Host A to Host B is the same as the route used to get back from Host B to Host A; the route is then called symmetric . In complex setups, the route back may be different; in this case, it is asymmetric.

关于反向路径过滤,参考《Understanding Linux Network Internals》三十一章,

31.7. Reverse Path Filtering

We saw what an asymmetric route is in the section “Essential Elements of Routing in Chapter 30. Asymmetric routes are not common, but may be necessary in certain cases. The default behavior of Linux is to consider asymmetric routing suspicious and therefore to drop any packet whose source IP address is not reachable through the device the packet was received from, according to the routing table.

However, this behavior can be tuned via /proc on a per-device basis, as we will see in Chapter 36. See also the section “Input Routing” in Chapter 35.

二、检查流程

如果一台主机(或路由器)从接口A收到一个包,其源地址和目的地址分别是10.3.0.2和10.2.0.2, 即, 如果启用反向路径过滤功能,它就会以为关键字去查找路由表,如果得到的输出接口不为A,则认为反向路径过滤检查失败,它就会丢弃该包。

关于反向路径过滤,ipv4中有个参数,这个参数的说明在Documentation/networking/ip-sysctl.txt中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.

    Current recommended practice in RFC3704 is to enable strict mode
    to prevent IP spoofing from DDos attacks. If using asymmetric routing
    or other complicated routing, then loose mode is recommended.

    The max value from conf/{all,interface}/rp_filter is used
    when doing source validation on the {interface}.

    Default value is 0. Note that some distributions enable it
    in startup scripts.

三、源代码分析

git commit 373da0a2a33018d560afcb2c77f8842985d79594

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
net/ipv4/fib_frontend.c
 192 int fib_validate_source(struct sk_buff *skb, __be32 src, __be32 dst, u8 tos,
 193                         int oif, struct net_device *dev, __be32 *spec_dst,
 194                         u32 *itag)
 195 {
             // 是否启用反向路径过滤
 216         /* Ignore rp_filter for packets protected by IPsec. */
 217         rpf = secpath_exists(skb) ? 0 : IN_DEV_RPFILTER(in_dev);
 
             // 检查路由表
             // 注意这里的源地址贺目的地址是反过来的,
             // 看看其他函数是如何调用fib_validate_source()就明白了。
 227         if (fib_lookup(net, &fl4, &res))
 228                 goto last_resort;

             // 运行到这里,说明反向路由是可达的
             // 下面分成两种情况检查输出设备是否就是输入设备
 237 #ifdef CONFIG_IP_ROUTE_MULTIPATH
             // 启用多路径时,任意一个匹配,就用它了
 238         for (ret = 0; ret < res.fi->fib_nhs; ret++) {
 239                 struct fib_nh *nh = &res.fi->fib_nh[ret];
 240
 241                 if (nh->nh_dev == dev) {
 242                         dev_match = true;
 243                         break;
 244                 }
 245         }
 246 #else
 247         if (FIB_RES_DEV(res) == dev)
 248                 dev_match = true;
 249 #endif
 250         if (dev_match) {
             // 反向路径过滤检查成功了,返回
 251                 ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
 252                 return ret;
 253         }
 254         if (no_addr)
 255                 goto last_resort;
             // 运行到这里,说明反向路径检查是失败的,
             // 如果rpf为1,表示反向路径检查必须成功才能正常返回,
             // 否则只好返回错误。
 256         if (rpf == 1)
 257                 goto e_rpf;
 278 e_rpf:
 279         return -EXDEV;

五、如何解决

两种方法:

1 On R2:

ip route add 10.3.0.0/16 via 10.2.0.2

增加一条关于10.3.0.0/16子网的路由。

2 On R2:

/etc/sysctl.conf

net.ipv4.conf.default.rp_filter = 0

禁用反向路径检查。

参数ip_early_demux

ip_early_demux 内核中默认是1(开启), 所以在ip_rcv_finish 到 tcp_v4_rcv 中 skb->destructor = sock_edemux;

且很大概率 skb->_skb_refdst = (unsigned long)dst | SKB_DST_NOREF; // #define SKB_DST_NOREF 1UL

对于NOREF的dst,如果要缓存(tcp_prequeue()或sk_add_backlog()), 则要调skb_dst_force(skb);

1
2
3
4
5
6
7
8
static inline void skb_dst_force(struct sk_buff *skb)
{
	if (skb_dst_is_noref(skb)) {
		WARN_ON(!rcu_read_lock_held());
		skb->_skb_refdst &= ~SKB_DST_NOREF;
		dst_clone(skb_dst(skb));
	}   
}

http://blog.chinaunix.net/uid-20662820-id-4935075.html

The routing cache has been suppressed in Linux 3.6 after a 2 years effort by David and the other Linux kernel developers. The global cache has been suppressed and some stored information have been moved to more separate resources like socket.

Metrics were stored in the routing cache entry which has disappeared. So it has been necessary to introduce a separate TCP metrics cache. A netlink interface is available to update/delete/add entry to the cache.

总结起来说就是Linux内核从3.6开始将全局的route cache全部剔除,取而代之的是各个子系统(tcp协议栈)内部的cache,由各个子系统维护。

当内核接收到一个TCP数据包来说,首先需要查找skb对应的路由,然后查找skb对应的socket。David Miller 发现这样做是一种浪费,对于属于同一个socket(只考虑ESTABLISHED情况)的路由是相同的,那么如果能将skb的路由缓存到socket(skb->sk)中,就可以只查找查找一次skb所属的socket,就可以顺便把路由找到了,于是David Miller提交了一个patch ipv4: Early TCP socket demux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(commit 41063e9dd11956f2d285e12e4342e1d232ba0ea2)
ipv4: Early TCP socket demux.

	Input packet processing for local sockets involves two major demuxes.
	One for the route and one for the socket.

	But we can optimize this down to one demux for certain kinds of local
	sockets.

	Currently we only do this for established TCP sockets, but it could
	at least in theory be expanded to other kinds of connections.

	If a TCP socket is established then it's identity is fully specified.

	This means that whatever input route was used during the three-way
	handshake must work equally well for the rest of the connection since
	the keys will not change.

	Once we move to established state, we cache the receive packet's input
	route to use later.

	Like the existing cached route in sk->sk_dst_cache used for output
	packets, we have to check for route invalidations using dst->obsolete
	and dst->ops->check().

	Early demux occurs outside of a socket locked section, so when a route
	invalidation occurs we defer the fixup of sk->sk_rx_dst until we are
	actually inside of established state packet processing and thus have
	the socket locked.

然而Davem添加的这个patch是有局限的,因为这个处理对于转发的数据包,增加了一个在查找路由之前查找socket的逻辑,可能导致转发效率的降低。 Alexander Duyck提出增加一个ip_early_demux参数来控制是否启动这个特性。

1
2
3
4
5
6
7
8
9
10
This change is meant to add a control for disabling early socket demux.
The main motivation behind this patch is to provide an option to disable
the feature as it adds an additional cost to routing that reduces overall
throughput by up to 5%. For example one of my systems went from 12.1Mpps
to 11.6 after the early socket demux was added. It looks like the reason
for the regression is that we are now having to perform two lookups, first
the one for an established socket, and then the one for the routing table.

By adding this patch and toggling the value for ip_early_demux to 0 I am
able to get back to the 12.1Mpps I was previously seeing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static int ip_rcv_finish(struct sk_buff *skb)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct rtable *rt;

	if (sysctl_ip_early_demux && !skb_dst(skb) && skb->sk == NULL) {
		const struct net_protocol *ipprot;
		int protocol = iph->protocol;

		ipprot = rcu_dereference(inet_protos[protocol]);
		if (ipprot && ipprot->early_demux) {
			ipprot->early_demux(skb);
			/* must reload iph, skb->head might have changed */
			iph = ip_hdr(skb);
		}
	}

	/*
	 * Initialise the virtual path cache for the packet. It describes
	 * how the packet travels inside Linux networking.
	 */
	if (!skb_dst(skb)) {
		int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
						   iph->tos, skb->dev);
		if (unlikely(err)) {
			if (err == -EXDEV)
				NET_INC_STATS_BH(dev_net(skb->dev),
					 LINUX_MIB_IPRPFILTER);
			goto drop;
		}
	}
	......

ip_early_demux就这样诞生了,目前内核中默认是1(开启),但是如果你的数据流量中60%以上都是转发的,那么请关闭这个特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void tcp_v4_early_demux(struct sk_buff *skb)
{
	const struct iphdr *iph;
	const struct tcphdr *th;
	struct sock *sk;

	if (skb->pkt_type != PACKET_HOST)
		return;

	if (!pskb_may_pull(skb, skb_transport_offset(skb) + sizeof(struct tcphdr)))
		return;

	iph = ip_hdr(skb);
	th = tcp_hdr(skb);

	if (th->doff < sizeof(struct tcphdr) / 4)
		return;

	sk = __inet_lookup_established(dev_net(skb->dev), &tcp_hashinfo,
				       iph->saddr, th->source,
				       iph->daddr, ntohs(th->dest),
				       skb->skb_iif);
	if (sk) {
		skb->sk = sk;
		skb->destructor = sock_edemux;
		if (sk->sk_state != TCP_TIME_WAIT) {
			struct dst_entry *dst = sk->sk_rx_dst;

			if (dst)
				dst = dst_check(dst, 0);
			if (dst &&
			    inet_sk(sk)->rx_dst_ifindex == skb->skb_iif)
				skb_dst_set_noref(skb, dst);
		}
	}
}

tcp选项TCP_DEFER_ACCEPT

http://www.pagefault.info/?p=346

TCP_DEFER_ACCEPT这个选项可能大家都知道,不过我这里会从源码和数据包来详细的分析这个选项。要注意,这里我所使用的内核版本是3.0.

首先看man手册中的介绍(man 7 tcp):

1
2
TCP_DEFER_ACCEPT (since Linux 2.4)
Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable. 

我先来简单介绍下,这个选项主要是针对server端的服务器,一般来说我们三次握手,当客户端发送syn,然后server端接收到,然后发送syn + ack,然后client接收到syn+ack之后,再次发送ack(client进入establish状态),最终server端收到最后一个ack,进入establish状态。

而当正确的设置了TCP_DEFER_ACCEPT选项之后,server端会在接收到最后一个ack之后,并不进入establish状态,而只是将这个socket标记为acked,然后丢掉这个ack。此时server端这个socket还是处于syn_recved,然后接下来就是等待client发送数据, 而由于这个socket还是处于syn_recved,因此此时就会被syn_ack定时器所控制,对syn ack进行重传,而重传次数是由我们设置TCP_DEFER_ACCEPT传进去的值以及TCP_SYNCNT选项,proc文件系统的tcp_synack_retries一起来决定的(后面分析源码会看到如何来计算这个值).而我们知道我们传递给TCP_DEFER_ACCEPT的是秒,而在内核里面会将这个东西转换为重传次数.

这里要注意,当重传次数超过限制之后,并且当最后一个ack到达时,下一次导致超时的synack定时器还没启动,那么这个defer的连接将会被加入到establish队列,然后通知上层用户。这个也是符合man里面所说的(Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection.) 也就是最终会完成这个连接.

我们来看抓包,这里server端设置deffer accept,然后客户端connect并不发送数据,我们来看会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//客户端发送syn
19:38:20.631611 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [S], seq 2500439144, win 32792, options [mss 16396,sackOK,TS val 9008384 ecr 0,nop,wscale 4], length 0
//server回了syn+ack
19:38:20.631622 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9008384 ecr 9008384,nop,wscale 4], length 0
 
//client发送最后一个ack
19:38:20.631629 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9008384 ecr 9008384], length 0
 
//这里注意时间,可以看到过了大概1分半之后,server重新发送了syn+ack
19:39:55.035893 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9036706 ecr 9008384,nop,wscale 4], length 0
19:39:55.035899 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9036706 ecr 9036706,nop,nop,sack 1 {0:1}], length 0
 
//再过了1分钟,server close掉这条连接。
19:40:55.063435 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [F.], seq 1, ack 1, win 2048, options [nop,nop,TS val 9054714 ecr 9036706], length 0
 
19:40:55.063692 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [F.], seq 1, ack 2, win 2050, options [nop,nop,TS val 9054714 ecr 9054714], length 0
 
19:40:55.063701 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [.], ack 2, win 2048, options [nop,nop,TS val 9054714 ecr 9054714], length 0

这里要注意,close的原因是当synack超时之后,nginx接收到了这条连接,然后读事件超时,最终导致close这条连接。

接下来就来看内核的代码。

先从设置TCP_DEFER_ACCEPT开始,设置TCP_DEFER_ACCEPT是通过setsockopt来做的,而传递给内核的值是秒,下面就是内核中对应的do_tcp_setsockopt函数,它用来设置tcp相关的option,下面我们能看到主要就是将传递进去的val转换为将要重传的次数。

1
2
3
4
5
6
case TCP_DEFER_ACCEPT:
	/* Translate value in seconds to number of retransmits */
	//注意参数
	icsk->icsk_accept_queue.rskq_defer_accept =
	secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ, TCP_RTO_MAX / HZ);
	break;

这里可以看到通过调用secs_to_retrans来将秒转换为重传次数。接下来就来看这个函数,它有三个参数,第一个是将要转换的秒,第二个是RTO的初始值,第三个是RTO的最大值。 可以看到这里都是依据RTO来计算的,这是因为这个重传次数是syn_ack的重传次数。

这个函数实现很简单,就是一个定时器退避的计算过程(定时器退避可以看我前面的blog的介绍),每次乘2,然后来计算重传次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static u8 secs_to_retrans(int seconds, int timeout, int rto_max)
{
	u8 res = 0;
 
	if (seconds > 0) {
		int period = timeout;
		//重传次数
		res = 1;
		//开始遍历
		while (seconds > period && res < 255) {
			res++;
			//定时器退避
			timeout <<= 1;
			if (timeout > rto_max)
				timeout = rto_max;
			//定时器的秒数
			period += timeout;
		}
	}
	return res;
}

然后来看当server端接收到最后一个ack的处理,这里只关注defer_accept的部分,这个函数是tcp_check_req,它主要用来检测SYN_RECV状态接收到包的校验。

req->retrans表示已经重传的次数。

acked标记主要是为了syn_ack定时器来使用的。

1
2
3
4
5
6
7
8
//两个条件,一个是重传次数小于defer_accept,一个是序列号,这两个都必须满足。
if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
	TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
	//此时设置acked。
	inet_rsk(req)->acked = 1;
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
	return NULL;
}

而当tcp_check_req返回之后,在tcp_v4_do_rcv中会丢掉这个包,让socket继续保存在半连接队列中。

然后来看syn ack定时器,这个定时器我以前有分析过(http://simohayha.iteye.com/admin/blogs/481989) ,因此我这里只是简要的再次分析下。如果需要更详细的分析,可以看我上面的链接,这个定时器会调用inet_csk_reqsk_queue_prune函数,在这个函数中做相关的处理。

这里我们就主要关注重试次数。其中icsk_syn_retries是TCP_SYNCNT这个option设置的。这个值会比sysctl_tcp_synack_retries优先.然后是rskq_defer_accept,它又比icsk_syn_retries优先.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void inet_csk_reqsk_queue_prune(struct sock *parent,
				const unsigned long interval,
				const unsigned long timeout,
				const unsigned long max_rto)
{
	........................
	//最大的重试次数
	int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
	int thresh = max_retries;
	unsigned long now = jiffies;
	struct request_sock **reqp, *req;
	int i, budget;
 
	....................................
	//更新设置最大的重试次数。
	if (queue->rskq_defer_accept)
		max_retries = queue->rskq_defer_accept;
 
	budget = 2 * (lopt->nr_table_entries / (timeout / interval));
	i = lopt->clock_hand;
 
	do {
		reqp=&lopt->syn_table[i];
		while ((req = *reqp) != NULL) {
			if (time_after_eq(now, req->expires)) {
				int expire = 0, resend = 0;
				//这个函数主要是判断超时和是否重新发送syn ack,然后保存在expire和resend这个变量中。
				syn_ack_recalc(req, thresh, max_retries,
						   queue->rskq_defer_accept,
						   &expire, &resend);
				....................................................
				if (!expire &&
					(!resend ||
					 !req->rsk_ops->rtx_syn_ack(parent, req, NULL) ||
					 inet_rsk(req)->acked)) {
					unsigned long timeo;
					//更新重传次数。
					if (req->retrans++ == 0)
						lopt->qlen_young--;
					timeo = min((timeout << req->retrans), max_rto);
					req->expires = now + timeo;
					reqp = &req->dl_next;
					continue;
				}
				//如果超时,则丢掉这个请求,并对应的关闭连接.
				/* Drop this request */
				inet_csk_reqsk_queue_unlink(parent, req, reqp);
				reqsk_queue_removed(queue, req);
				reqsk_free(req);
				continue;
			}
			reqp = &req->dl_next;
		}
 
		i = (i + 1) & (lopt->nr_table_entries - 1);
 
	} while (--budget > 0);
	...............................................
}