说实话,IP协议的逐跳选项用的真不是很多,但是却必不可少。
在IPv4中,它作为一个普通的IP选项塞入变长的IPv4头中,加以必须被路由器处理之约束。
在IPv6中,它则作为一个独立的扩展头被定长IPv6头的next header字段指引,加以必须被路由器处理之约束。
哪个更优雅,不用多说。如果你是路由器实现者,你会觉得哪一个IP版本处理逐跳选项更加方便,你会如何设计处理逻辑?
话题有点发散,但这是后面准备写的内容,本文谈另一个相关的话题,即逐跳选项是如何被处理的,我以 逐跳路由器告警选项(Router Alert Option) 描述之。
第一个问题,路由器进行IP转发的逻辑是怎样的。
将目标IP地址不是本机IP地址的数据包收到本机的方案很多,以Linux系统为例,包括但不限于:
使用NAT Redirect。
使用Netfilter nfqueue。
使用PACKET/PF_Ring等机制来镜像或者旁路。
Netmap/DPDK。
…
一般的DPI设备都是采用这些方式来 “捕获” 数据报文。但是本文不谈这些。
排除数据包被有意截获不说,仅从IP协议的层面上来讲,逐跳被路由的IP数据包 只有在目标IP地址是其自身时才会被接收,除非IP数据包携带逐跳路由器告警选项(Router Alert Option)! 即在下面的情况下,数据包会被当前路由器接收:
数据包的目标IP地址就是当前路由器的IP地址之一。
数据包的目标IP地址不属于当前路由器,但是它携带了路由告警选项(Router Alert Option)。
其中第1点是显而易见的,这是IP路由的基本常识,本文主要说说第2点。
第一次接触RSVP协议是在2004年,那时真的是猛学了一段网络协议,特别是感兴趣的路由交换这块,此外为了考华为3Com的HCSE,也接触了一些高层的协议,RSVP就是其中之一。
我在当时不理解RSVP协议是如何工作的。疑问大致就是 既然数据报文的目标IP地址不是途径的路由器,那么路由器又是如何接收到数据报文并且处理资源预留的呢? 我不清楚是什么机制让路由器能收下这个目标并不它的报文。
后来我知道这是路由器告警选项(Router Alert Option)做的,它在RFC2113(https://tools.ietf.org/html/rfc2113)中被描述:
This memo describes a new IP Option type that alerts transit routers
to more closely examine the contents of an IP packet. This is useful
for, but not limited to, new protocols that are addressed to a
destination but require relatively complex processing in routers
along the path.
换句话说,就是 携带路由器告警选项的数据包,单凭标准的IP转发处理是不够的,必须根据数据包数据载荷的内容进行进一步的处理。
那么,可以想象,路由器告警选项(Router Alert Option)的处理逻辑是下面的样子:
然后,由于数据包载荷的内容以及所需的处理逻辑高度策略化,所以路由器告警选项(Router Alert Option)都是在用户态进行处理,处理完之后,将数据包的TTL/Hoplimit字段递减后原封不动注入协议栈,使其继续被逐跳转发前行。
因此,数据包转发也就有了两条路径:
由于IPv4对IP选项的处理非标准化处理,以下全部基于IPv6讨论。
说实话,IPv6的IP转发逻辑更加清晰直接:
IPv6协议头固定长度,处理起来更规则。
逐跳选项头在经由路由器上被无条件解析并处理。
除了逐跳头,其余所有扩展头都是目标地址是本机时按照next头的逻辑才被处理, 就像处理普通上层一样 。
美中不足就是这个逐跳头,破坏了规矩,使得目的地址不是本机也要被处理,不过又该如何呢?谁让它叫做 逐跳头 呢?无论如何,我都觉得应该有更加优雅的方法,只是目前还没有想到。
在实现上,路由器告警选项(Router Alert Option)是藏匿在IPv6逐跳扩展头里面的,不管是IPv4还是IPv6,标准规定,只要是数据包途径的路由器,都必须处理逐跳选项,这是硬性的规定。至于说路由器告警选项(Router Alert Option),只是逐跳头的内容而已,它决定了数据包应该如何被处理。
逐跳头决定了逐跳头的内容一定会被处理。
逐跳头的内容决定了数据包如何被进一步处理。
在原理方面,以上基本就是全部了。下面就是代码实现和Demo样例了。
先看路由器告警选项(Router Alert Option)的格式:
关键的就是那个 select value,它决定了途径的路由器要处理哪些携带路由器告警选项(Router Alert Option)的包。
IP Forward的逻辑如下:
hop_by_hop_process_done(skb)
{
other_hhop_process(skb);
// Router Alert option
list_for_each(ra_entry, router_alert_list) {
if (ra_entry.select_value == skb.hhop.ra.select_value) {
recv_to_user_process(skb);
return 1;
}
}
return 0;
}
ip_forward(skb)
{
...
if (hop_by_hop_process_done(skb)) {
return 0;
}
continue_forward(skb);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
也就是说,数据包携带的逐跳选项头里的路由器告警(Router Alert)select value和当前路由器本地监听的路由器告警(Router Alert)处理进程的select value相等,说明本地有进程会 进一步处理该数据报文。
接下来的问题是,我们看得见摸得着的比如Linux系统,提供了哪些现成的API让我们可以亲眼见到上面的过程呢?
我们看一下IPv6的编程Manual:
http://man7.org/linux/man-pages/man7/ipv6.7.html
其中有关于路由器告警(Router Alert)的sockopt以及其用法:
按照这个Manual,我来简单地实现一个监听进程:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define LENGTH 1500
#define __u8 unsigned char
#define __be16 unsigned short
struct ipv6hdr {
__u8 priority:4,
version:4;
__u8 flow_lbl[3];
__be16 payload_len;
__u8 nexthdr;
__u8 hop_limit;
struct in6_addr saddr;
struct in6_addr daddr;
};
#define MY_VALUE 5
int do_it()
{
// TODO
return 0;
}
int main(int argc, char **argv)
{
int ret;
int sd;
struct ipv6hdr *iphdr;
struct sockaddr_in6 dest;
int length = LENGTH;
unsigned char data[LENGTH] = { 0 };
int ra_select_value = MY_VALUE;
// 按照要求,只能RAW
sd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);
if (sd < 0) {
perror("create socket\n");
}
// 按照要求,侦听数值5这个select value
if(setsockopt(sd, IPPROTO_IPV6, IPV6_ROUTER_ALERT, (const void *) &ra_select_value, sizeof(ra_select_value)) < 0) {
printf("setsockopt ");
}
// 接收被IP forward逻辑给递送上来的需要进一步策略化处理的数据包
ret = recvfrom(sd, data, length, 0, NULL, NULL);
if (ret < 0) {
printf("Error in sending message : %s (%d)\n", strerror(errno), errno);
return -1;
}
// 数据本身就是一个IP数据报文
iphdr = (struct ipv6hdr *)&data[0];
{ // 查看一下内容
int i;
printf("begin\n");
for (i = 0; i < ret; i++) {
printf("%.2X ", data[i]);
}
printf("\nend\n\n");
}
{ // 这就是我们的所谓的进一步处理!
unsigned char cmd[LENGTH] = {0};
strncpy(cmd, (unsigned char *)&data[0] + sizeof(struct ipv6hdr) + 8 + 20, ret - sizeof(struct ipv6hdr) - 8 - 20);
printf("%s\n", cmd);
do_it(); // 做的像个样子
}
// 递减hop limit
iphdr->hop_limit --;
memset(&dest, 0, sizeof(dest));
dest.sin6_family = AF_INET6;
memcpy(dest.sin6_addr.s6_addr, &iphdr->daddr, sizeof(struct in6_addr));
// 按照要求,it is the user's responsibility to send them out again.
ret = sendto(sd, data, ret, 0, (struct sockaddr*) &dest, sizeof(dest));
if (ret < 0) {
printf("Error in sending message : %s (%d)\n", strerror(errno), errno);
return -1;
}
close(sd);
return(0);
}
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
我们把它编译成 ra_policy 以备用。
现在我们构造一个携带路由器告警选项的数据包,让其经过部署 ra_policy 程序的路由器,看看会发生什么。
先来看一下我的演示拓扑,非常简易:
这么设计这个测试拓扑的理由非常简单直接。我们假设一个数据包逐跳经由的路由器分别为Router0,Router1,Router2,我们准备在Router1上演示逐跳头的路由器告警选项(Router Alert Option)如何被处理,那么就要假想一个数据包从Router0发过来,在Router1上被处理后发送给下一跳Router2,因此我们在Router0上构造这个数据包P并注入,值得注意的是,数据包P的源地址并非Router0上的地址,目标地址也不是Router1和Router2。
现在就剩下携带路由器告警选项(Router Alert Option)的数据包构造程序还没有。
可以非常容易地用RAW socket写一个,但是我找到一个现成的,来自下面的网站:
http://www.pdbuchan.com/rawsock/rawsock.html
我使用的是下面的程序:
http://www.pdbuchan.com/rawsock/tcp6_hop_frag.c
我对其进行了简单的修改:
由于本文我并不care分片这个特性,我将data数据文件进行了修改。
为了体现所谓的 根据数据包的内容进一步处理, data的内容改成了:
[root@localhost hopbyhop]# cat data
请将网卡的MTU缩小为1300,并且调整下面的内核参数:
net.core.rmem_default = 400382
我严格按照路由器R的enp0s9的MAC地址进行了以太帧封装,而不是使用广播MAC地址。
其它就没有什么改动了。编译成 tcp6_hop_frag 以备用。
程序本身的代码并不重要,说实话,tcp6_hop_frag.c这个代码写得并不是很优雅,我不是很喜欢这个代码,比代码更重要的是它发出去的报文的格式,我们要充分理解这个格式:
现在看看上面注入的数据包P到达Router1时,发生了什么:
[root@localhost hopbyhop]# ./ra_policy
begin
60 00 00 00 00 82 00 FF 20 01 0D B8 00 00 00 00 02 14 51 FF FE 2F 15 56 24 04 68 00 40 05 08 03 00 00 00 00 00 00 20 0E 06 00 05 02 00 05 01 00 00 50 00 50 00 00 00 00 00 00 00 00 50 02 FF FF 7B BA 00 00 E8 AF B7 E5 B0 86 E7 BD 91 E5 8D A1 E7 9A 84 4D 54 55 E7 BC A9 E5 B0 8F E4 B8 BA 31 33 30 30 EF BC 8C E5 B9 B6 E4 B8 94 E8 B0 83 E6 95 B4 E4 B8 8B E9 9D A2 E7 9A 84 E5 86 85 E6 A0 B8 E5 8F 82 E6 95 B0 EF BC 9A 0A 6E 65 74 2E 63 6F 72 65 2E 72 6D 65 6D 5F 64 65 66 61 75 6C 74 20 3D 20 34 30 30 33 38 32 0A
end
请将网卡的MTU缩小为1300,并且调整下面的内核参数:
net.core.rmem_default = 400382
[root@localhost hopbyhop]#
1
2
3
4
5
6
7
8
9
嗯,打印出了数据包的内容,原来数据包的内容就是指示这个路由器做一些资源配置方面的事情啊,然后Router1照着做就是了,当然,它也是可以拒绝的,不管怎样,Router1的ra_policy进程最终将数据包传给了下一跳Router2…在Router2看来,数据包是下面的样子:
当然,数据包到达Router2的时候,Router2依然不是其最终的目的地(它的最终目的地是Google!),那么如果上一个Router没有把逐跳头的路由器告警选项(Router Alert Option)摘除,那么每一个途径的路由器都要做类似Router1的处理。
有意思的是, Router1可以修改数据包的内容,让后续的路由器作出不同的策略决策!
是的,RSVP跟这个也差不多,就是这么个意思。
以上就是关于路由器告警选项(Router Alert Option)如何工作的简单演示,总结一下就是:
逐跳选项(hop by hop)每个途径的路由器必须处理。
路由器告警(Router Alert Option)选项藏匿在逐跳选项里。
路由器告警选项被应用层进程进行策略化处理。
携带路由器选项的数据包被拉到应用层进程处理后重新注入协议栈继续前往下一跳。
————————————————
版权声明:本文为CSDN博主「dog250」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dog250/article/details/89153594