内核构造skb数据包的实现总结

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

/macrossdzh/article/details/5438306
一、IPv4、TCP和UDP的校验和计算
校验和是网络协议用来识别传输错误的冗余域。

有些校验和不但能检测错误,还能自动修正某些类型的错误。

校验和的想法很简单。

在传输一个数据包之前,发送方计算出一个很小的、固定长度的域(校验和)包含数据的某种散列。

如果在传输过程中某几位数据被改变,很可能损坏的数据会产生一个不同的校验和。

取决于你用来产生校验和使用的函数,校验和提供不同级别的可靠性。

IP协议采用的校验和是简单的一个包括求和取反码,这个方法太弱了,不能被认为是可靠的。

对于更可靠的完整性检查,你必须依赖于L2 CRC或者
SSL/IPSec消息认证码。

不同的协议可以使用不同的校验和算法。

IP协议校验和只覆盖IP头。

大多数L4协议的校验和均覆盖头和数据。

看起来在L2(比如,以太网)有校验和,L3(比如,IP)有另一个,L4(比如,TCP)还有一个的做法是冗余的,因为它们全都应用于数据的重叠部分,但是检查是有价值的。

错误不只在传输过程中发生,也会在层之间移动中发生。

而且,每个协议负责保证他自己的正确传输,不能假设高或低的层完成这个任务。

举一个可能发生的复杂情况的例子,想象LAN1上的PC A通过Internet发送数据给LAN2上的PC B。

假设LAN1中使用的L2协议使用校验和而LAN2上的不使用。

那么最少一个高层提供某种形式的校验和来减小接受损坏数据的可能性是很重要的。

每个协议的定义中都建议使用校验和,虽然它不是必须的。

然而,必须承认的是一个好的相关协议的设计可以去掉一些不同层协议之间的重叠特性带来的开销。

因为大多数L2和L4协议提供校验和,在L3中也有校验和就不是严格必须的。

正是由于这个原因,IPv6中去掉了这个校验和。

在IPv4中,IP校验和是一个16位域覆盖整个IP头,包括选项。

校验和最初由数据包源来计算,并在整个到目标的过程中一个跳跃一个跳跃的被更新以反映每个路由器带来的头部变化。

在更新校验和之前,每个跳跃首先必须检查包的完整性通过比较包中的和本地计算的校验和。

如果完整性检查失败,包会被丢弃,但是不会产生ICMP:L4 协议会处理的(例如,使用在给定时间内没有应答就强制重发的定时器)。

这里是一些会触发更新校验和需求的情况:
1、减小TTL
路由器在转发数据包前必须减小包IP头中的TTL。

由于IP校验和同样覆盖了那个域,原始
的校验和就不再有效了。

你将在第20章"ip_forward函数"节中看到TTL被ip_decrease_ttl 减小,这个函数也处理校验和。

数据包破坏(包括NAT)
所有带来一个或多个IP头部域改变的特性均强制重新计算校验和。

NAT大概是最著名的例子。

2、IP选项处理
由于选项是头的一部分,它们也被校验和覆盖。

于是,每次它们被需要增加或者修改IP头(例如,时间戳的增加)的方式处理时,强制重新计算校验和。

3、分片
当一个数据包被分片时,每个分片有一个不同的头。

大多数的域保持不变,但是与分片有关的域,比如偏移,就是不同的了。

因此,校验和不得不重新计算。

由于IP协议适用的校验和使用与TCP、UDP、ICMP相同的简单算法,它们共用一组通用的函数。

也有一个为了IP校验和特殊优化的函数。

按照IP校验和算法的定义,头部被分成16位的字进行求和和取补码。

图18-13显示了一个校验和计算的例子,为了简单只对两个16位字求和。

Linux不对16位字求和,它对32位甚至64位字求和,目的是更快速计算(这需要在求和和取反码之间增加一个额外的步骤;参看下一节中csum_fold的描述)。

实现这个算法的函数叫做ip_fast_csum,在大多数体系结构中直接用汇编语言。

分组头的校验和(checksum)算法是16位累加和后的反码,TCP和UDP数据报头也使用相同的校验算法,但参与运算的数据与IP分组头不一样。

IPv4分组头的结构如下所示:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
其中的"Header Checksum"域即为头校验和部分。

当要计算IPv4分组头校验和时,发送方先将其置为0,然后按16位逐一累加至IPv4分组头结束,累加和保存于一个32位的数值中。

如果总的字节数为奇数,则最后一个字节单独相加。

累加完毕将结果中高16位再加到低16位上,重复这一过程直到高16位为全0。

下面用实际截获的IPv4分组(数据连路层DLC的包)来演示整个计算过程:
0x0000: 00 60 47 41 11 c9 00 09 6b 7a 5b 3b 08 00 45 00
0x0010: 00 1c 74 68 00 00 80 11 59 8f c0 a8 64 01 ab 46
0x0020: 9c e9 0f 3a 04 05 00 08 7f c5 00 00 00 00 00 00
0x0030: 00 00 00 00 00 00 00 00 00 00 00 00
在上面的16进制采样中,起始为Ethernet帧(DLC包)的开头。

IPv4分组头从地址偏移量
0x000e开始,第一个字节为0x45,最后一个字节为0xe9,即IPv4分组头到目标IP地址为止。

根据以上的算法描述,我们可以作如下计算:
(1) 0x4500 + 0x001c + 0x7468 + 0x0000 + 0x8011 + 0x0000(累加和位置先置0) + 0xc0a8 + 0x6401 + 0xab46 + 0x9ce9 = 0x3a66d
(2) 0xa66d + 0x3 = 0xa670
(3) 0xffff - 0xa670 = 0x598f
注意在第一步我们用0x0000设置头校验和部分。

可以看出这一分组头的校验和与收到的值完全一致。

以上的过程仅用于发送方计算初始的校验和,实际中对于中间转发的路由器和最终接收方,可将收到的IPv4分组头校验和部分直接按同样算法相加,如果结果为0xffff,则校验正确。

对于TCP和UDP的数据报,其头部也包含16位的校验和,校验算法与IPv4分组头完全一致,但参与校验的数据不同。

这时校验和不仅包含整个TCP/UDP数据报,还覆盖了一个虚头部。

虚头部的定义如下:
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| source address |
+--------+--------+--------+--------+
| destination address |
+--------+--------+--------+--------+
| zero |protocol| TCP/UDP length |
+--------+--------+--------+--------+
其中有IP源地址,IP目的地址,协议号(TCP:6/UDP:17)及TCP或UDP数据报的总长度(头部+数据)。

将虚头部加入校验的目的,是为了再次核对数据报是否到达正确的目的地,并防止IP欺骗攻击(spoofing)。

上述报文在0x0018处的协议类型=十六进制11,即该报文是一个UDP报文,其长度存放在0x0027开始的两个字节(含源目端口地址4字节+UDP长度2字节+校验和2字节=8字节,以及UDP数据的长度:故本数据包UDP数据的长度实际为0字节),IP源目地址存放在0x0x1a到0x0x21共八个字节中,先将校验和0x002a处的两个字节置0,计算UDP包的校验和如下:
(1) 0xc0a8+0x6401(前为源IP)+0xab46+0x9ce9(前为目IP)+0x0011(即Zero和Protocol)+ 0x0008(UDP长度)+ 0x0f3a(源端口)+0x0405(目端口)+0x0008(UDP长度)+0x0000(校验和预置为0)+…(这里没有任何数据了:UDP数据的长度实际为0字节)=0x28038
(2) 0x28038=>0x8038+0x0002=0x803A
(3) 0xFFFF-0x803A=0x7FC5
计算结果和0x0028处的结果相同,注意UDP长度出现了两次。

二、内核构造udp数据包,计算校验和需要注意的问题
其中涉及到两个函数
skb_checksum(const struct sk_buff *skb, int offset,
int len, __wsum csum)
四个参数解释:
skb:不用说了
offset:IP Header长度
len:IP payload长度
csum:0,计算校验和时为0
csum_tcpudp_magic(__be32 saddr, __be32 daddr,
unsigned short len,
unsigned short proto,
__wsum sum)
saddr:源IP
daddr:目的IP
proto:传输协议
sum:IP payload校验和
两个函数配合使用,前者是计算UDP payload校验和,后者是计算整个IP payload校验和。

因为校验和不涉及链路层,如果skb是直接从网卡驱动取出来的话,就需要把skb->data
设置为iph
在调用skb_checksum以前必须设置udph->check为0。

这是协议规定的
三、不同情况下构造skb数据包的实现
1,正常网卡收到数据包后的情况:
它的工作就是剥离mac头,然后给一些字段赋值,最后调用netif_rx将剥离mac头后的数据报(比如ip数据包)发送到上层协议。

由协议栈处理。

在此以ldd3中的snull为例,虽然snull跟硬件不相关,但这个过程都是类似的。

1struct sk_buff *skb;
2struct snull_priv *priv = netdev_priv(dev);
3skb = dev_alloc_skb(pkt->datalen + 2);
4if (!skb) {
5if (printk_ratelimit())
6 printk(KERN_NOTICE "snull rx: low on mem - packet dropped/n");
7 priv->stats.rx_dropped++;
8goto out;
9}
10skb_reserve(skb, 2); /* align IP on 16B boundary */
11memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
12/* Write metadata, and then pass to the receive level */
13skb->dev = dev;
14skb->protocol = eth_type_trans(skb, dev);
15skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
16priv->stats.rx_packets++;
17priv->stats.rx_bytes += pkt->datalen;
18netif_rx(skb);
此刻收到的数据包的格式如下:mac+ip+udp/udp+data
这时候的处理就是剥离mac头,然后需要更新的一些域值。

这些都是在函数eth_type_trans 函数里做的。

需要注意的是,skb->dev = dev;这条语句是很重要的,如果没有此语句,将会导致系统错误而死机(至少在我的板子上是这样的)。

注意:eth_type_trans()函数主要赋值的是:
skb->mac.raw,skb->protocol和skb->pkt_type。

见下面的代码有无mac头的情况。

2,完全从一个字符串开始构造一个新的skb数据包。

以前只是看过如何修改数据包,自己构造数据包,这还是头一次,刚开始确实给我难住了,来来经过看内核代码和自己摸索,我自己写的代码如下:
19/*假设:data是一个指向字符串的指针,data_len是data的长度*/
20struct ipv6hdr *ipv6h;
21struct udphdr *udph;
22struct sk__buff * new_skb;
23int length = data_len + sizeof(struct ipv6hdr) + sizeof(udphdr);
24new_skb = dev_alloc_skb(length);
25if(!new_skb)
26 {
27 printk("low memory.../n"):
28return -1;
29 }
30skb_reserve(new_skb,length);
31memcpy(skb_push(new_skb,data_len),data,data_len);
32new_skb->h.uh = udph = (struct udphdr *)skb_push(new_skb,sizeof(struct udphdr));
33memcpy(udph,&udph_tmp,sizeof(struct udphdr)); //注意,此刻我的udph_tmp是在另一个过程中截获的数据包的udp头,如果完全是自己构造数据包,则需要自己填充udp数据头中的字段。

34udph->len = .............. ; //此处需要给udph->len赋值。

注意udph->len是__u16的。

存储时是高低位互换的,所以你应该先将你要更新的数字编成16进制的数,然后高低位互换,在赋值给udh->len。

35udplen = new_skb->len;
36new_skb->nh.ipv6h = ipv6h = (struct ipv6hdr *)skb_push(new_skb,sizeof(struct ipv6hdr));
37memcpy(ipv6h,&ipv6h_tmp,sizeof(struct ipv6hdr)); //同udp头注释。

38ipb6h->payload_len = ..........;//此处同udph->len.需要注意的是,此处所指的长度并不包括ipv6头的长度,而是去掉ipv6头后的长度。

39udph->check = 0;
40udph->check = csum_ipv6_magic(&ipv6h->saddr, &ipv6h->daddr, udplen, IPPROTO_UDP, csum_partial((char *)udph, udplen, 0));
41///////////////注意,如果是ipv4,则还需要计算ip校验和,但此处是ipv6,不用计算ip 检验和,所以此处没有ipv6头的校验。

//////////////////////////
42new_skb->mac.raw = new_skb->data; //因为无mac头
43new_skb->protocol = htons(ETH_P_IPV6); //表明包是ipv6数据包
44new_skb->pkt_type = PACKET_HOST; //表明是发往本机的包
45new_skb->dev = &can_control; //此处很重要,如果没有这条语句,则内核跑死。

至少在我板子上是这样的。

can_control是我的net_device结构体变量。

46netif_rx(new_skb);
3,当需要改变原有skb的数据域的情况。

此时,有两种办法:
可以先判断skb的tailroom,如果空间够大,则我们可以把需要添加的数据放在skb的tailroom里。

如果tailroom不够大,则需要调用skb_copy_expand函数来扩充tailroom或者headroom。

例如我们需要在skb的后面加上一个16个字节的字符串,则代码类似如下:
47if(skb_tailroom(skb) < 16)
48{
49 nskb = skb_copy_expand(skb, skb_headroom(skb),
skb_tailroom(skb)+16,GFP_ATOMIC);
50if(!nskb)
51 {
52 printk("low memory..../n");
53 dev_kfree_skb(skb);
54return -1;
55 }
56
57else
58 {
59 kfree_skb(skb); // 注意,如果此时是钩子函数钩出来的,则skb不能在这里释放,否则会造成死机。

60 skb = nskb;
61 }
62
63 memcpy(skb_put(skb,16),ipbuf,16); //ipbuf为要加到skb后面的字符串
64
65 udplen = skb->len - sizeof(struct ipv6hdr);
66
67 udph->len += 0x1000; //换成十进制为 + 16
68 ipv6h->payload_len += 0x1000;
69
70 udph->check = 0;
71 udph->check = csum_ipv6_magic(&ipv6h->saddr, &ipv6h->daddr, udplen, IPPROTO_UDP, csum_partial((char *)udph,udplen,0));
72
73 skb->mac.raw = new_skb->data; //因为无mac头
74 skb->protocol = htons(ETH_P_IPV6); //表明包是ipv6数据包
75 skb->pkt_type = PACKET_HOST; //表明是发往本机的包
76 skb->dev = &can_control; //此处很重要,如果没有这条语句,则内核跑死。

至少在我板子上是这样的。

can_control是我的net_device结构体变量。

77 netif_rx(skb);
78}
注意:当调用skb_copy_expand或者修改了skb的数据域后,一定要更新udph->len和ipv6h->payload_len。

否则上层应用(比如udp套接字)收到的数据包还是原来的数据包而不是修改后的数据包,因为udph->len的原因。

参考:
/wfsun/blog/item/1a265109d49e60cc3bc763da.html
/u2/83905/showart_2024232.html
/bbs/archiver/tid-1142700.html
/5834192.html。

相关文档
最新文档