kk Blog —— 通用基础

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

Linux TCP数据包接收处理 tcp_recvmsg

http://blog.csdn.net/mrpre/article/details/33347221

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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
/* 
 *    This routine copies from a sock struct into the user buffer.
 *
 *    Technical note: in 2.3 we work on _locked_ socket, so that
 *    tricks with *seq access order and skb->users are not required.
 *    Probably, code can be easily improved even more.
 */

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int copied = 0;
	u32 peek_seq;
	u32 *seq;
	unsigned long used;
	int err;
	int target;    /* Read at least this many bytes */
	long timeo;
	struct task_struct *user_recv = NULL;
	int copied_early = 0;
	struct sk_buff *skb;
	u32 urg_hole = 0;

	//功能:“锁住sk”,并非真正的加锁,而是执行sk->sk_lock.owned = 1 
	//目的:这样软中断上下文能够通过owned ,判断该sk是否处于进程上下文。
	//提供一种同步机制。
	lock_sock(sk);

	TCP_CHECK_TIMER(sk);

	err = -ENOTCONN;
	if (sk->sk_state == TCP_LISTEN)
		goto out;

	//获取延迟,如果用户设置为非阻塞,那么timeo ==0000 0000 0000 0000
	//如果用户使用默认recv系统调用
	//则为阻塞,此时timeo ==0111 1111 1111 1111
	//timeo 就2个值
	timeo = sock_rcvtimeo(sk, nonblock);

	/* Urgent data needs to be handled specially. */
	if (flags & MSG_OOB)
		goto recv_urg;

	//待拷贝的下一个序列号
	seq = &tp->copied_seq;

	//设置了MSG_PEEK,表示不让数据从缓冲区移除,目的是下一次调用recv函数
	//仍然能够读到相同数据
	if (flags & MSG_PEEK) {
		peek_seq = tp->copied_seq;
		seq = &peek_seq;
	}

	//如果设置了MSG_WAITALL,则target  ==len,即recv函数中的参数len
	//如果没设置MSG_WAITALL,则target  == 1
	target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);

	//大循环
	do {
		u32 offset;

		/* Are we at urgent data? Stop if we have read anything or have SIGURG pending. */
		if (tp->urg_data && tp->urg_seq == *seq) {
			if (copied)
				break;
			if (signal_pending(current)) {
				copied = timeo ? sock_intr_errno(timeo) : -EAGAIN;
				break;
			}
		}

		/* Next get a buffer. */

		//小循环
		skb_queue_walk(&sk->sk_receive_queue, skb) {
			/* Now that we have two receive queues this
				* shouldn't happen.
				*/
			if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
								KERN_INFO "recvmsg bug: copied %X "
									      "seq %X rcvnxt %X fl %X\n", *seq,
									      TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
									      flags))
				break;

			//如果用户的缓冲区(即用户malloc的buf)长度够大,offset一般是0。
			//即 “下次准备拷贝数据的序列号”==此时获取报文的起始序列号
			//什么情况下offset >0呢?很简答,如果用户缓冲区12字节,而这个skb有120字节
			//那么一次recv系统调用,只能获取skb中的前12个字节,下一次执行recv系统调用
			//offset就是12了,offset表示从第12个字节开始读取数据,前12个字节已经读取了。
			//那这个"已经读取12字节"这个消息,存在哪呢?
			//在*seq = &tp->copied_seq;中
			offset = *seq - TCP_SKB_CB(skb)->seq;
			if (tcp_hdr(skb)->syn)
				offset--;
			if (offset < skb->len)
				goto found_ok_skb;
			if (tcp_hdr(skb)->fin)
				goto found_fin_ok;
			WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
					"copied %X seq %X rcvnxt %X fl %X\n",
					*seq, TCP_SKB_CB(skb)->seq,
					tp->rcv_nxt, flags);
		}

		//执行到了这里,表明小循环中break了,既然break了,说明sk_receive_queue中
		//已经没有skb可以读取了
		//如果没有执行到这里说明前面的小循环中执行了goto,读到有用的skb,或者读到fin都会goto。
		//没有skb可以读取,说明什么?
		//可能性1:当用户第一次调用recv时,压根没有数据到来
		//可能性2:skb->len一共20字节,假设用户调用一次 recv,读取12字节,再调用recv,
		//读取12字节,此时skb由于上次已经被读取了12字节,只剩下8字节。
		//于是代码的逻辑上,再会要求获取skb,来读取剩下的8字节。

		//可能性1的情况下,copied == 0,肯定不会进这个if。后续将执行休眠
		//可能性2的情况下,情况比较复杂。可能性2表明数据没有读够用户想要的len长度
		//虽然进程上下文中,没有读够数据,但是可能我们在读数据的时候
		//软中断把数据放到backlog队列中了,而backlog对队列中的数据或许恰好让我们读够数
		//据。

		//copied了数据的,copied肯定>=1,而target 是1或者len
		//copied只能取0(可能性1),或者0~len(可能性2)
		//copied >= target 表示我们取得我们想要的数据了,何必进行休眠,直接return
		//如果copied 没有达到我们想要的数据,则看看sk_backlog是否为空
		//空的话,尽力了,只能尝试休眠
		//非空的话,还有一线希望,我们去sk_backlog找找数据,看看是否能够达到我们想要的
		//数据大小

		//我觉得copied == target是会出现的,但是出现的话,也不会进现在这个流程
		//,如有不对,请各位大神指正,告诉我
		//说明情况下copied == target

		/* Well, if we have backlog, try to process it now yet. */
		if (copied >= target && !sk->sk_backlog.tail)
			break;


		if (copied) {
			//可能性2,拷贝了数据,但是没有拷贝到指定大小
			if (sk->sk_err ||
							sk->sk_state == TCP_CLOSE ||
							(sk->sk_shutdown & RCV_SHUTDOWN) ||
							!timeo ||
							signal_pending(current))
				break;
		} else {
			//可能性1
			if (sock_flag(sk, SOCK_DONE))
				break;

			if (sk->sk_err) {
				copied = sock_error(sk);
				break;
			}

			if (sk->sk_shutdown & RCV_SHUTDOWN)
				break;

			if (sk->sk_state == TCP_CLOSE) {
				if (!sock_flag(sk, SOCK_DONE)) {
					/* This occurs when user tries to read
						* from never connected socket.
						*/
					copied = -ENOTCONN;
					break;
				}
				break;
			}

			//是否是阻塞的,不是,就return了。
			if (!timeo) {
				copied = -EAGAIN;
				break;
			}

			if (signal_pending(current)) {
				copied = sock_intr_errno(timeo);
				break;
			}
		}

		tcp_cleanup_rbuf(sk, copied);

		//sysctl_tcp_low_latency 默认0tp->ucopy.task == user_recv肯定也成立

		if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
			/* Install new reader */
			if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
				user_recv = current;
				tp->ucopy.task = user_recv;
				tp->ucopy.iov = msg->msg_iov;
			}

			tp->ucopy.len = len;

			WARN_ON(tp->copied_seq != tp->rcv_nxt &&
				!(flags & (MSG_PEEK | MSG_TRUNC)));

			/* Ugly... If prequeue is not empty, we have to
				* process it before releasing socket, otherwise
				* order will be broken at second iteration.
				* More elegant solution is required!!!
				*
				* Look: we have the following (pseudo)queues:
				*
				* 1. packets in flight
				* 2. backlog
				* 3. prequeue
				* 4. receive_queue
				*
				* Each queue can be processed only if the next ones
				* are empty. At this point we have empty receive_queue.
				* But prequeue _can_ be not empty after 2nd iteration,
				* when we jumped to start of loop because backlog
				* processing added something to receive_queue.
				* We cannot release_sock(), because backlog contains
				* packets arrived _after_ prequeued ones.
				*
				* Shortly, algorithm is clear --- to process all
				* the queues in order. We could make it more directly,
				* requeueing packets from backlog to prequeue, if
				* is not empty. It is more elegant, but eats cycles,
				* unfortunately.
				*/

			if (!skb_queue_empty(&tp->ucopy.prequeue))
				goto do_prequeue;

			/* __ Set realtime policy in scheduler __ */
		}

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else
					sk_wait_data(sk, &timeo); 
		//在此处睡眠了,将在tcp_prequeue函数中调用wake_up_interruptible_poll唤醒
		
		//软中断会判断用户是正在读取检查并且睡眠了,如果是的话,就直接把数据拷贝
		//到prequeue队列,然后唤醒睡眠的进程。因为进程睡眠,表示没有读到想要的字节数
		//此时,软中断有数据到来,直接给进程,这样进程就能以最快的速度被唤醒。


		if (user_recv) {
			int chunk;

			/* __ Restore normal policy in scheduler __ */

			if ((chunk = len - tp->ucopy.len) != 0) {
				NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk);
				len -= chunk;
				copied += chunk;
			}

			if (tp->rcv_nxt == tp->copied_seq &&
							!skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
				tcp_prequeue_process(sk);

				if ((chunk = len - tp->ucopy.len) != 0) {
					NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
					len -= chunk;
					copied += chunk;
				}
			}
		}
		if ((flags & MSG_PEEK) &&
						(peek_seq - copied - urg_hole != tp->copied_seq)) {
			if (net_ratelimit())
				printk(KERN_DEBUG "TCP(%s:%d): Application bug, race in MSG_PEEK.\n",
									      current->comm, task_pid_nr(current));
			peek_seq = tp->copied_seq;
		}
		continue;

	found_ok_skb:
		/* Ok so how much can we use? */
		//skb中还有多少聚聚没有拷贝。
		//正如前面所说的,offset是上次已经拷贝了的,这次从offset开始接下去拷贝
				used = skb->len - offset;
		//很有可能used的大小,即skb剩余长度,依然大于用户的缓冲区大小(len)。所以依然
		//只能拷贝len长度。一般来说,用户还得执行一次recv系统调用。直到skb中的数据读完
		if (len < used)
			used = len;

		/* Do we have urgent data here? */
		if (tp->urg_data) {
			u32 urg_offset = tp->urg_seq - *seq;
			if (urg_offset < used) {
				if (!urg_offset) {
					if (!sock_flag(sk, SOCK_URGINLINE)) {
						++*seq;
						urg_hole++;
						offset++;
						used--;
						if (!used)
							goto skip_copy;
					}
				} else
					used = urg_offset;
			}
		}

		if (!(flags & MSG_TRUNC)) {
			{
				//一般都会进这个if,进行数据的拷贝,把能够读到的数据,放到用户的缓冲区
				err = skb_copy_datagram_iovec(skb, offset,
						msg->msg_iov, used);
				if (err) {
					/* Exception. Bailout! */
					if (!copied)
						copied = -EFAULT;
					break;
				}
			}
		}

		//更新标志位,seq 是指针,指向了tp->copied_seq
		//used是我们有能力拷贝的数据大小,即已经拷贝到用户缓冲区的大小
		//正如前面所说,如果用户的缓冲区很小,一次recv拷贝不玩skb中的数据,
		//我们需要保存已经拷贝了的大小,下次recv时,从这个大小处继续拷贝。
		//所以需要更新copied_seq。
		*seq += used;
		copied += used;
		len -= used;

		tcp_rcv_space_adjust(sk);

skip_copy:
		if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
			tp->urg_data = 0;
			tcp_fast_path_check(sk);
		}

		//这个就是判断我们是否拷贝完了skb中的数据,如果没有continue
		//这种情况下,len经过 len -= used; ,已经变成0,所以continue的效果相当于
		//退出了这个大循环。可以理解,你只能拷贝len长度,拷贝完之后,那就return了。

		//还有一种情况used + offset ==  skb->len,表示skb拷贝完了。这时我们只需要释放skb
		//下面会讲到
		if (used + offset < skb->len)
			continue;

		//看看这个数据报文是否含有fin,含有fin,则goto到found_fin_ok
		if (tcp_hdr(skb)->fin)
			goto found_fin_ok;

		//执行到这里,标明used + offset ==  skb->len,报文也拷贝完了,那就把skb摘链释放
		if (!(flags & MSG_PEEK)) {
			sk_eat_skb(sk, skb, copied_early);
			copied_early = 0;
		}
		//这个cintinue不一定是退出大循环,可能还会执行循环。
		//假设用户设置缓冲区12字节,你skb->len长度20字节。
		//第一次recv读取了12字节,skb剩下8,下一次调用recv再想读取12,
		//但是只能读取到这8字节了。
		//此时len 变量长度为4,那么这个continue依旧在这个循环中,
		//函数还是再次从do开始,使用skb_queue_walk,找skb
		//如果sk_receive_queue中skb仍旧有,那么继续读,直到len == 0
		//如果没有skb了,我们怎么办?我们的len还有4字节怎么办?
		//这得看用户设置的recv函数阻塞与否,即和timeo变量相关了。
		continue;

	found_fin_ok:
		/* Process the FIN. */
		++*seq;
		if (!(flags & MSG_PEEK)) {
			//把skb从sk_receive_queue中摘链
			sk_eat_skb(sk, skb, copied_early);
			copied_early = 0;
		}
		break;
	} while (len > 0);

	//到这里是大循环退出
	//休眠过的进程,然后退出大循环 ,才满足 if (user_recv) 条件
	if (user_recv) {
		if (!skb_queue_empty(&tp->ucopy.prequeue)) {
			int chunk;

			tp->ucopy.len = copied > 0 ? len : 0;

			tcp_prequeue_process(sk);

			if (copied > 0 && (chunk = len - tp->ucopy.len) != 0) {
				NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk);
				len -= chunk;
				copied += chunk;
			}
		}

		//数据读取完毕,清零
		tp->ucopy.task = NULL;
		tp->ucopy.len = 0;
	}

	/* According to UNIX98, msg_name/msg_namelen are ignored
		* on connected socket. I was just happy when found this 8) --ANK
		*/

	/* Clean up data we have read: This will do ACK frames. */
	//很重要,将更新缓存,并且适当的时候发送ack
	tcp_cleanup_rbuf(sk, copied);

	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;

out:
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;

recv_urg:
	err = tcp_recv_urg(sk, msg, len, flags);
	goto out;
}

Linux TCP数据包接收处理 tcp_v4_rcv

http://blog.sina.com.cn/s/blog_52355d840100b6sd.html

tcp_v4_rcv函数

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
int tcp_v4_rcv(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct tcphdr *th;
	struct sock *sk;
	int ret;
	  
	//如果不是发往本地的数据包,则直接丢弃
	if (skb->pkt_type != PACKET_HOST)
		goto discard_it;

	TCP_INC_STATS_BH(TCP_MIB_INSEGS);

	//包长是否大于TCP头的长度
	if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
		goto discard_it;

	//取得TCP首部
	th = tcp_hdr(skb);

	//检查TCP首部的长度和TCP首部中的doff字段是否匹配
	if (th->doff < sizeof(struct tcphdr) / 4)
		goto bad_packet;

	//检查TCP首部到TCP数据之间的偏移是否越界
	if (!pskb_may_pull(skb, th->doff * 4))
		goto discard_it;

	if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
		goto bad_packet;

	 th = tcp_hdr(skb);
	iph = ip_hdr(skb);
	TCP_SKB_CB(skb)->seq = ntohl(th->seq);

	//计算end_seq,实际上,end_seq是数据包的结束序列号,实际上是期待TCP确认
	//包中ACK的数值,在数据传输过程中,确认包ACK的数值等于本次数据包SEQ
	//号加上本数据包的有效载荷,即skb->len - th->doff * 4,但是在处理SYN报文或者
	//FIN报文的时候,确认包的ACK等于本次处理数据包的SEQ+1,考虑到这种情况,
	//期待下一个数据包的ACK就变成了TCP_SKB_CB(skb)->seq + th->syn + th->fin +
	//skb->len - th->doff * 4

	// TCP_SKB_CB宏会返回skb->cb[0],一个类型为tcp_skb_cb的结构指针,这个结
	//构保存了TCP首部选项和其他的一些状态信息

	TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
					skb->len - th->doff * 4);
	TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
	TCP_SKB_CB(skb)->when   = 0;
	TCP_SKB_CB(skb)->flags    = iph->tos;
	TCP_SKB_CB(skb)->sacked = 0;

	//根据四元组查找相应连接的sock结构,大体有两个步骤,
	//首先用__inet_lookup_established函数查找已经处于establish状态的连接,
	//如果查找不到的话,就调用__inet_lookup_listener函数查找是否存在四元组相
	//匹配的处于listen状态的sock,这个时候实际上是被动的接收来自其他主机的连接
	//请求

	//如果查找不到匹配的sock,则直接丢弃数据包
	sk = __inet_lookup(&tcp_hashinfo, iph->saddr, th->source,
			   iph->daddr, th->dest, inet_iif(skb));
	if (!sk)
		goto no_tcp_socket;

	//检查sock是否处于半关闭状态
	process:
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;
 
	//检查IPSEC规则
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	nf_reset(skb);

	//检查BPF规则
	if (sk_filter(sk, skb))
		goto discard_and_relse;

	skb->dev = NULL;

	//这里主要是和release_sock函数实现互斥,release_sock中调用了
	// spin_lock_bh(&sk->sk_lock.slock);
	bh_lock_sock_nested(sk);
	ret = 0;

	//查看是否有用户态进程对该sock进行了锁定
	//如果sock_owned_by_user为真,则sock的状态不能进行更改
	if (!sock_owned_by_user(sk)) {

#ifdef CONFIG_NET_DMA
		struct tcp_sock *tp = tcp_sk(sk);
		if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
			tp->ucopy.dma_chan = get_softnet_dma();
		if (tp->ucopy.dma_chan)
			ret = tcp_v4_do_rcv(sk, skb);
		else
#endif
		{
			//进入预备处理队列
			if (!tcp_prequeue(sk, skb))
				ret = tcp_v4_do_rcv(sk, skb);
		}
	} else
		//如果数据包被用户进程锁定,则数据包进入后备处理队列,并且该进程进入
		//套接字的后备处理等待队列sk->lock.wq
		sk_add_backlog(sk, skb);
	bh_unlock_sock(sk);

	sock_put(sk);
	return ret;

no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
		TCP_INC_STATS_BH(TCP_MIB_INERRS);
	} else {
		tcp_v4_send_reset(NULL, skb);
	}

discard_it:
	kfree_skb(skb);
	return 0;

discard_and_relse:
	sock_put(sk);
	goto discard_it;

do_time_wait:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
		TCP_INC_STATS_BH(TCP_MIB_INERRS);
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}
	switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
	case TCP_TW_SYN: {
		struct sock *sk2 = inet_lookup_listener(&tcp_hashinfo,
							iph->daddr, th->dest,
							inet_iif(skb));
		if (sk2) {
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
			inet_twsk_put(inet_twsk(sk));
			sk = sk2;
			goto process;
		}
	}
	case TCP_TW_ACK:
		tcp_v4_timewait_ack(sk, skb);
		break;
	case TCP_TW_RST:
		goto no_tcp_socket;
	case TCP_TW_SUCCESS:;
	}
	goto discard_it;
}

SYN cookies机制下连接的建立

http://blog.csdn.net/justlinux2010/article/details/12619761

在正常情况下,服务器端接收到客户端发送的SYN包,会分配一个连接请求块(即request_sock结构),用于保存连接请求信息,并且发送SYN+ACK包给客户端,然后将连接请求块添加到半连接队列中。客户端接收到SYN+ACK包后,会发送ACK包对服务器端的包进行确认。服务器端收到客户端的确认后,根据保存的连接信息,构建一个新的连接,放到监听套接字的连接队列中,等待用户层accept连接。这是正常的情况,但是在并发过高或者遭受SYN flood攻击的情况下,半连接队列的槽位数量很快就会耗尽,会导致丢弃新的连接请求,SYN cookies技术可以使服务器在半连接队列已满的情况下仍能处理新的SYN请求。

如果开启了SYN cookies选项,在半连接队列满时,SYN cookies并不丢弃SYN请求,而是将源目的IP、源目的端口号、接收到的客户端初始序列号以及其他一些安全数值等信息进行hash运算,并加密后得到服务器端的初始序列号,称之为cookie。服务器端在发送初始序列号为cookie的SYN+ACK包后,会将分配的连接请求块释放。如果接收到客户端的ACK包,服务器端将客户端的ACK序列号减1得到的值,与上述要素hash运算得到的值比较,如果相等,直接完成三次握手,构建新的连接。SYN cookies机制的核心就是避免攻击造成的大量构造无用的连接请求块,导致内存耗尽,而无法处理正常的连接请求。

启用SYN cookies是通过在启动环境中设置以下命令完成:

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

注意,即使开启该机制并不意味着所有的连接都是用SYN cookies机制来完成连接的建立,只有在半连接队列已满的情况下才会触发SYN cookies机制。由于SYN cookies机制严重违背TCP协议,不允许使用TCP扩展,可能对某些服务造成严重的性能影响(如SMTP转发),对于防御SYN flood攻击的确有效。对于没有收到攻击的高负载服务器,不要开启此选项,可以通过修改tcp_max_syn_backlog、tcp_synack_retries和tcp_abort_on_overflow系统参数来调节。

下面来看看内核中是怎么通过SYN cookie机制来完成连接的建立。

客户端的连接请求由

1
2
3
4
tcp_v4_do_rcv()
	tcp_rcv_state_process()
		icsk->icsk_af_ops->conn_request()
			tcp_v4_conn_request()

函数处理。tcp_v4_conn_request()中有一个局部变量want_cookie,用来标识是否使用SYN cookies机制。want_cookie的初始值为0,如果半连接队列已满,并且开启了tcp_syncookies系统参数,则将其值设置为1,如下所示:

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
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
#ifdef CONFIG_SYN_COOKIES
	int want_cookie = 0;
#else
#define want_cookie 0 /* Argh, why doesn't gcc optimize this :( */
#endif

...... 

	/* TW buckets are converted to open requests without
	 * limitations, they conserve resources and peer is
	 * evidently real one.
	 */
	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
		if (sysctl_tcp_syncookies) {
			want_cookie = 1;
		} else
#endif
	   
		goto drop;
	}
......

drop:
	return 0;
}

如果没有开启SYN cookies机制,在半连接队列满时,会跳转到drop处,返回0。在调用tcp_v4_conn_request()的tcp_rcv_state_process()中会直接释放SKB包。

我们前面提高过,造成半连接队列满有两种情况(不考虑半连接队列很小的情况),一种是负载过高,正常的连接数过多;另一种是SYN flood攻击。如果是第一种情况,此时是否继续构建连接,则要取决于连接队列的情况及半连接队列的重传情况,如下所示:

1
2
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
	goto drop;

sk_acceptq_is_full()函数很好理解,根据字面意思就可以看出,该函数是检查连接队列是否已满。inet_csk_reqsk_queue_young()函数返回半连接队列中未重传过SYN+ACK段的连接请求块数量。如果连接队列已满并且半连接队列中的连接请求块中未重传的数量大于1,则会跳转到drop处,丢弃SYN包。如果半连接队列中未重传的请求块数量大于1,则表示未来可能有2个完成的连接,这些新完成的连接要放到连接队列中,但此时连接队列已满。如果在接收到三次握手中最后的ACK后连接队列中没有空闲的位置,会忽略接收到的ACK包,连接建立会推迟,所以此时最好丢掉部分新的连接请求,空出资源以完成正在进行的连接建立过程。还要注意,这个判断并没有考虑半连接队列是否已满的问题。从这里可以看出,即使开启了SYN cookies机制并不意味着一定可以完成连接的建立。

如果可以继续连接的建立,调用inet_reqsk_alloc()分配连接请求块,如下所示:

1
2
3
req = inet_reqsk_alloc(&tcp_request_sock_ops);
if (!req)
	goto drop;

看到这里可能就有人疑惑,既然开启了SYN cookies机制,仍然分配连接请求块,那和正常的连接构建也没有什么区别了。这里之所以要分配连接请求块是用于发送SYN+ACK包给客户端,发送后会释放掉,并不会加入到半连接队列中。

接下来就是计算cookie的值,由cookie_v4_init_sequence()函数完成,如下所示:

1
2
3
4
5
6
7
if (want_cookie) {
#ifdef CONFIG_SYN_COOKIES
	syn_flood_warning(skb);
	req->cookie_ts = tmp_opt.tstamp_ok;
#endif
	isn = cookie_v4_init_sequence(sk, skb, &req->mss);
}

计算得到的cookie值会保存在连接请求块tcp_request_sock结构的snt_isn成员中,接着会调用__tcp_v4_send_synack()函数发送SYN+ACK包,然后释放前面分配的连接请求块,如下所示:

1
2
if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
	goto drop_and_free;

在服务器端发送完SYN+ACK包后,我们看到在服务器端没有保存任何关于这个未完成连接的信息,所以在接收到客户端的ACK包后,只能根据前面发送的SYN+ACK包中的cookie值来决定是否继续构建连接。

我们接下来看接收到ACK包后的处理情况。ACK包在tcp_v4_do_rcv()函数中调用的tcp_v4_hnd_req()中处理,如下所示:

1
2
3
4
5
6
7
8
9
10
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
	......
 
#ifdef CONFIG_SYN_COOKIES
	if (!th->rst && !th->syn && th->ack)
		sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt));
#endif
	return sk;
}

由于在服务器端没有保存未完成连接的信息,所以在半连接队列或ehash散列表中都不会找到对应的sock结构。如果开启了SYN cookies机制,则会检查接收到的数据包是否是ACK包,如果是,在cookie_v4_check()中会调用cookie_check()函数检查ACK包中的cookie值是否有效。如果有效,则会分配request_sock结构,并根据ACK包初始化相应的成员,开始构建描述连接的sock结构。创建过程和正常的连接创建过程一样。

SSH端口转发 及 SSH代理

SSH端口转发

假设A、B为内网机,C为可登录公网机。那么A连B过程为:

假设 A、B、C 的ssh端口分别为portA、portB、portC。

1. 远程转发-R, 远程机转发到本地机

在被连的B上先运行如下命令

1
ssh -p portC -f -N -R 9000:localhost:portB userC@hostC-IP

这样到C机上9000端口的连接会被转发到B机的portB上。

2.本地转发-L, 本地机转发到远程机

在发起连接的A上运行如下命令

1
ssh -p portC -f -N -L 9999:localhost:9000 userC@hostC-IP

这样到A机9999端口的连接会被转发到C机的9000上。而C的9000又会被转发到B的portB上。
所以只要在A上运行:

1
ssh -p 9999 userB@127.0.0.1

就相当于ssh到了B机的portB上。

参数
1
2
3
4
f  表示后台用户验证,这个选项很有用,没有shell的不可登陆账号也能使用.
N 表示不执行脚本或命令
-L 本地转发
-R 远程转发
路由器设置省去C机

在路由器添加转发规则,端口为A机ssh监听端口,ip为A机内网IP。记下路由器公网IP。
然后只要在B机上直接连路由器公网IP+转发端口

1
ssh -p portA -f -N -R 9999:localhost:portB userA@routeA-IP

然后在A机上直接

1
ssh -p 9999 userB@127.0.0.1

A就能连上B

SSH代理–网站限制内网IP?代理一下就好了

1 远程机有公网IP

只要在本地运行

1
ssh -p port -qTfnN -D 7070 user@sshserver

然后在firefox的 首选项->高级->网络->设置 里面改成手动配置代理,只填"SOCKS"这一行即可。

2 若远程机为内网机

先按前面端口转发的方法,在本机映射一个到远程机的端口localport,然后命令改成

1
ssh -p localport -qTfnN -D 7070 user@127.0.0.1

这样firefox下要填127.0.0.1和7070