Linux中TCP三次握手的实现

前言

网上三次握手八股文一大堆,我“为了面试”也去看了看,刚好那时候接触Linux比较多,突然想到TCP三次握手在Linux内核中是如何去实现的呢?是不是会有不同?然后我就开始了漫长的百度(ps: 我比较菜,还不能拿着Linux上千个源码文件去怼)、源码之路。终于弄清了Linux中TCP三次握手的大致过程。



TCP三次握手

我那边都知道TCP建立连接前都需要客户端发送一个SYN包,服务端响应一个SYN + ACK包,客户端响应一个ACK包,这是三次握手的基本流程。

但如果让我们去实现三次握手,应该怎么去设计呢?



Linux中的实现

我们以网络编程中的相应函数为切入点

Linux中三次握手的主要函数有socket()、bind()、listen()、connect()、recv()、accept()


socket()

socket 在内核里并不是一个内核对象。而是包含 file、socket、sock 等多个相关内核对象构成,每个内核对象还定义了 ops 操作函数集合。

1
2
// 返回的是一个文件描述符fd
fd = socket(AF_INET,SOCK_STREAM, 0);

bind()

调用bind()函数绑定端口,会使connect()时选择端口方式无效。不推荐在客户端中使用bind(),这会打乱connect的端口选择过程。(但某些情况下可能会bind(),比如说在服务端和客户端进行协商使用什么端口来调用)

流程:

  1. 判断用户传入的端口是否小于1024

  2. 调用inet_csk_get_port来判断传入端口是否被占用,如果被占用就返回EADDRINUSE。这个方法不会到 ESTABLISH 的哈希表进行可用检测,只在 bind 状态的 socket 里查。所以默认情况下,只要端口用过一次就不会再次使用

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
// file: net/ipv4/af_inet.c

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);
unsigned short snum;
int chk_addr_ret;
int err;

if (sk->sk_prot->bind) {
// 如果这个socket有他自己的bind()函数,就使用这个bind()函数
err = sk->sk_prot->bind(sk, uaddr, addr_len);
goto out;
}

// ......

// 用户传入的端口号
snum = ntohs(addr->sin_port);
err = -EACCES;
// 传入的端口号不允许小于1024
if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
goto out;

//......

// 尝试确定端口号
// 实际调用的是inet_csk_get_port,用来尝试确定端口号是否被占用
// 如果被占用就返回EADDRINUSE,然后就会显示"Address already in use"(端口被占用)
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}

// ......
}


// file: net/ipv4/inet_connection_sock.c
/**
* 获取对给定sock的本地端口的引用,
* 如果snum为零,则表示选择任何可用的本地端口。
*/
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
struct inet_bind_hashbucket *head;
struct hlist_node *node;
struct inet_bind_bucket *tb;
int ret, attempts = 5;
struct net *net = sock_net(sk);
int smallest_size = -1, smallest_rover;

local_bh_disable();

// 如果传入的snum不为0
if (!snum) {
int remaining, rover, low, high;

again:
// 获取本地指定的端口范围
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;
// 在端口范围中取一个随机位置开始遍历
smallest_rover = rover = net_random() % remaining + low;

smallest_size = -1;
do {
// 如果本地设置了一个端口不可用
if (inet_is_reserved_local_port(rover))
goto next_nolock;

head = &hashinfo->bhash[inet_bhashfn(net, rover,
hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)

// 冲突检测,如果端口是用到的,去have_sum逻辑
if (net_eq(ib_net(tb), net) && tb->port == rover) {
if (tb->fastreuse > 0 &&
sk->sk_reuse &&
sk->sk_state != TCP_LISTEN &&
(tb->num_owners < smallest_size || smallest_size == -1)) {
smallest_size = tb->num_owners;
smallest_rover = rover;
if (atomic_read(&hashinfo->bsockets) > (high - low) + 1) {
spin_unlock(&head->lock);
snum = smallest_rover;
goto have_snum;
}
}
goto next;
}
break;
next:
spin_unlock(&head->lock);
next_nolock:
if (++rover > high)
rover = low;
} while (--remaining > 0);

/* Exhausted local port range during search? It is not
* possible for us to be holding one of the bind hash
* locks if this test triggers, because if 'remaining'
* drops to zero, we broke out of the do/while loop at
* the top level, not from the 'break;' statement.
*/
ret = 1;
if (remaining <= 0) {
if (smallest_size != -1) {
snum = smallest_rover;
goto have_snum;
}
goto fail;
}
/* OK, here is the one we will use. HEAD is
* non-NULL and we hold it's mutex.
*/
snum = rover;
} else {
have_snum:
head = &hashinfo->bhash[inet_bhashfn(net, snum,
hashinfo->bhash_size)];
spin_lock(&head->lock);
inet_bind_bucket_for_each(tb, node, &head->chain)
if (net_eq(ib_net(tb), net) && tb->port == snum)
// 判断端口是否被占用
goto tb_found;
}

// ......
}

listen()

主要是申请和初始化接收队列,包括全连接队列和半连接队列

全连接队列长度:用户传入的 backlog 和 net.core.somaxconn 之间较小的那个值

半连接队列长度:min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于16。

流程:

  1. 根据文件描述符fd查找socket内核对象
  2. 获取内核参数net.core.somaxconn,与用户传入的backlog相比较,取最小的
  3. 调用sock->ops->listen(sock, backlog)函数,实际是inet_listen
  4. 判断是否是listen状态
    • 如果不是listen状态,开始监听(接收队列的创建、初始化。内存申请、半连接队列长度的计算、全连接队列头的初始化等)。1. 计算半连接队列长度(与sysctl_max_syn_backlog取 一次最小值;保证不能比8小;向上对齐到2的整数次幂)。2. 申请内存。3. 全队列头初始化,设置为null。4. 将半连接队列挂载到接收队列上。
  5. 设置全连接队列的长度(backlog与net.core.somaxconn之间较小的哪个值)
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
//file: net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;

// 根据 fd(文件描述符) 查找对应的 socket 内核对象
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
// 获取内核参数
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
// 如果设置的backlog大于核心参数
if ((unsigned)backlog > somaxconn)
// 使用核心参数
backlog = somaxconn;

err = security_socket_listen(sock, backlog);
if (!err)
// 调用协议栈注册的listen函数
err = sock->ops->listen(sock, backlog);

fput_light(sock->file, fput_needed);
}
return err;
}


// file: net/ipv4/af_inet.c
// 服务端的全连接队列长度是listen时传入的backlog和net.core.somaxconn之间最小的
int inet_listen(struct socket *sock, int backlog)
{
// ......

// 还不是listen状态(没有listen过)
if (old_state != TCP_LISTEN) {
// 开始监听
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
}
// 设置全连接队列长度
sk->sk_max_ack_backlog = backlog;
err = 0;

// ......
}


// file: net/ipv4/inet_connection_sock.c
// 用来初始化接收队列
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
// ......

// 用来申请和初始化icsk_accept_queue(半连接队列和全连接队列)这个对象
// 1.定义接收队列数据结构
// 2.接收队列的申请和初始化(内存的申请、半连接队列长度的计算、全连接队列头的初始化)
int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);

// ......
}


// file: net/core/request_sock.c
/*
* queue:用来存放全连接和半连接队列的结构体
* nr_table_entries:是内核参数和用户传入的backlog中的最小值
*
* 1. 定义一个listen_sock指针(半连接队列)
* 2. 计算半连接队列的长度,然后申请内存。半连接队列的长度是 min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于16。
* 3. 将全连接队列头queue->rskq_accept_head设置成NULL,
* 将半连接队列挂载到接收队列queue上
*/
int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size = sizeof(struct listen_sock);

// 半连接队列
struct listen_sock *lopt;

// 计算半连接队列的长度
// 再次和sysctl_max_syn_backlog内核对象又取一次最小值
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
// 保证nr_table_entries不能比8小。
nr_table_entries = max_t(u32, nr_table_entries, 8);
// 用来上对齐到2的整数次幂
// 比如说当前nr_table_entries是最小值8
// 经过roundup_pow_of_two(8+1) = 16
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);

// 为listen_sock对象申请内存,这里包含了半连接队列
lopt_size += nr_table_entries * sizeof(struct request_sock *);
if (lopt_size > PAGE_SIZE)
lopt = vzalloc(lopt_size);
else
lopt = kzalloc(lopt_size, GFP_KERNEL);
if (lopt == NULL)
return -ENOMEM;

// 为了效率,不记录nr_table_entries
// 而是记录2的几次幂等于nr_table_entries
// t
for (lopt->max_qlen_log = 3;
(1 << lopt->max_qlen_log) < nr_table_entries;
lopt->max_qlen_log++);

get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
rwlock_init(&queue->syn_wait_lock);

// 全连接队列头初始化
queue->rskq_accept_head = NULL;

// 半连接队列设置
lopt->nr_table_entries = nr_table_entries;
write_lock_bh(&queue->syn_wait_lock);
queue->listen_opt = lopt;
write_unlock_bh(&queue->syn_wait_lock);

return 0;
}

connect()

流程:

  1. 进入内核系统根据用户传入的fd(文件描述符)来查询对应的socket内核对象
  2. 调用该sock对象的sock->ops->connect方法(inet_stream_connect)
  3. 根据sock的状态来进入不同的处理逻辑。第一次connect,sock的状态都是unconnect,所以会调sk->sk_prot->connect方法(tcp_v4_connect)
  4. 将socket状态设置为TCP-SYN-SENT
  5. 动态选择一个端口(首先判断是否bind()了一个端口,如果没有,就根据目标地址和端口等信息生成一个随机数,然后在本地端口范围中通过这个随机数逐渐增加遍历,如果是本地配置的保留端口就跳过,如果不是就遍历已经使用的端口的哈希链表(hinfo->bhash),判断是否已经被使用。如果该端口已经被使用并且TCP连接中的四元组与当前建立的四元组完全一致,就不可使用。继续遍历,找到了就返回端口,没找到就提示”Address already in use”)
  6. 进行tcp连接。(首先申请并设置skb,然后添加到发送队列sk_write_queue上,进行发送,启动重传定时器)
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
//file: net/socket.c
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;

// 根据用户fd查找内核中的socket对象
sock = sockfd_lookup_light(fd, &err, &fput_needed);

// ......

// 调用sock中ops的connect()方法
// 其实是inet_stream_connect()
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
sock->file->f_flags);
// ......
}


//file: ipv4/af_inet.c
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sock *sk = sock->sk;

// ......

switch (sock->state) {
default:
err = -EINVAL;
goto out;
case SS_CONNECTED:
err = -EISCONN;
goto out;
case SS_CONNECTING:
err = -EALREADY;
/* Fall out of switch with err, set for this state */
break;
case SS_UNCONNECTED:
// 刚创建完的socket状态就是SS_UNCONNECTED,
// 所以第一次connect的就会走这个case
err = -EISCONN;
if (sk->sk_state != TCP_CLOSE)
goto out;

// 调用sock的tcp_v4_connect方法
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;

// 修改sock的状态
sock->state = SS_CONNECTING;

/* Just entered SS_CONNECTING state; the only
* difference is that return value in non-blocking
* case is EINPROGRESS, rather than EALREADY.
*/
err = -EINPROGRESS;
break;
}
// ......
}


//file: net/ipv4/tcp_ipv4.c
// 启动传出连接
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// 设置socket状态为TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);

// 动态选择一个端口
err = inet_hash_connect(&tcp_death_row, sk);

// 函数用来根据sk中的信息,构建一个完成的syn报文,并将它发送出去
err = tcp_connect(sk);
}


//file:net/ipv4/inet_hashtables.c
// 绑定一个端口
int inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk)
{
// inet_sk_port_offset(sk): 根据要连接的目的IP和端口等信息生成一个随机数
// __inet_check_established: 检查是否和现有 ESTABLISH 的连接是否冲突的时候用的函数
return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk),
__inet_check_established, __inet_hash_nolisten);
}


//file:net/ipv4/inet_hashtables.c
// 端口号选择
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
struct sock *sk, u32 port_offset,
int (*check_established)(struct inet_timewait_death_row *,
struct sock *, __u16, struct inet_timewait_sock **),
int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
{
struct inet_hashinfo *hinfo = death_row->hashinfo;
// 是否绑定过端口(bind()方法)
const unsigned short snum = inet_sk(sk)->inet_num;
struct inet_bind_hashbucket *head;
struct inet_bind_bucket *tb;
int ret;
struct net *net = sock_net(sk);
int twrefcnt = 1;

// 如果没有绑定端口
if (!snum) {
int i, remaining, low, high, port;
static u32 hint;
u32 offset = hint + port_offset;
struct hlist_node *node;
struct inet_timewait_sock *tw = NULL;

// 获取本机的端口范围信息
inet_get_local_port_range(&low, &high);
remaining = (high - low) + 1;

local_bh_disable();
// 遍历查找端口
for (i = 1; i <= remaining; i++) {
// 之前算出的随机数+i
port = low + (i + offset) % remaining;

// 如果本机配置了该端口不可用
if (inet_is_reserved_local_port(port))
continue;

// 查找和遍历已经使用的端口的哈希链表
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock(&head->lock);

inet_bind_bucket_for_each(tb, node, &head->chain) {
// 如果该端口已经被使用
if (net_eq(ib_net(tb), net) &&
tb->port == port) {
if (tb->fastreuse >= 0)
goto next_port;
WARN_ON(hlist_empty(&tb->owners));
// 通过 check_established 继续检查是否可用
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}
}
// ......
}


//file:net/ipv4/tcp_output.c
// tcp连接
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;

// tcp连接初始化
tcp_connect_init(sk);

// 申请skb并构造为一个SYN包
buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
if (unlikely(buff == NULL))
return -ENOBUFS;

// ......

// 添加到发送队列
__tcp_add_write_queue_tail(sk, buff);

// ......

// 实际发出syn
err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
if (err == -ECONNREFUSED)
return err;

// ......

/* Timer for repeating the SYN until an answer. */
// 启动重传定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}

判断四元组

两对四元组中只要任意一个元素不同,都算是两条不同的连接。以下的两条 TCP 连接完全可以同时存在(假设 192.168.1.101 是客户端,192.168.1.100 是服务端)

  • 连接1:192.168.0.1 5000 192.168.0.2 8090
  • 连接2:192.168.0.1 5000 192.168.0.2 8091

check_established 作用就是检测现有的 TCP 连接中是否四元组和要建立的连接四元素完全一致。如果不完全一致,那么该端口仍然可用

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
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp)
{

// 找到hash桶,所有ESTABLISH状态的socket组成的哈希表。
unsigned int hash = inet_ehashfn(net, daddr, lport,
saddr, inet->inet_dport);

spin_lock(lock);

/* Check TIME-WAIT sockets first. */

// 遍历查看是否有四元组一样的,一样就报错
sk_nulls_for_each(sk2, node, &head->twchain) {
tw = inet_twsk(sk2);

if (INET_TW_MATCH(sk2, net, hash, acookie,
saddr, daddr, ports, dif)) {
if (twsk_unique(sk, sk2, twp))
goto unique;
else
goto not_unique;
}
}

tw = NULL;

/* And established part... */
// 使用 INET_MATCH 来判断是否可用。
sk_nulls_for_each(sk2, node, &head->chain) {
if (INET_MATCH(sk2, net, hash, acookie,
saddr, daddr, ports, dif))
goto not_unique;
}

unique:
// ......
return 0;

not_unique:
spin_unlock(lock);
return -EADDRNOTAVAIL;
}

// include/net/inet_hashtables.h
#define INET_MATCH(__sk, __net, __hash, __cookie, __saddr, __daddr, __ports, __dif) \
(((__sk)->sk_hash == (__hash)) && net_eq(sock_net(__sk), (__net)) && \
(inet_sk(__sk)->inet_daddr == (__saddr)) && \
(inet_sk(__sk)->inet_rcv_saddr == (__daddr)) && \
((*((__portpair *)&(inet_sk(__sk)->inet_dport))) == (__ports)) && \
(!((__sk)->sk_bound_dev_if) || ((__sk)->sk_bound_dev_if == (__dif))))

所以一台客户端机最大能建立的连接数并不是 65535。只要 server 足够多,单机发出百万条连接没有任何问题。


recv()

服务器响应SYN

主要工作是判断下接收队列是否满了,满的话可能会丢弃该请求,否则发出 synack。申请 request_sock 添加到半连接队列中,同时启动定时器。

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
// file: net/ipv4/tcp_ipv4.c
// 处理握手过程
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
// ......

// 如果已经建立的TCP连接
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
sock_rps_save_rxhash(sk, skb->rxhash);
if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
return 0;
}

if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
goto csum_err;


// 服务器收到第一步握手SYN或者第三步ACK都会进入里
if (sk->sk_state == TCP_LISTEN) {
// 进入tcp_v4_hnd_req中查看半连接队列
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;

if (nsk != sk) {
sock_rps_save_rxhash(nsk, skb->rxhash);
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb->rxhash);

// 根据不同的socket状态进行不同的处理
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}

// ......
}


// file:net/ipv4/tcp_input.c
// 除了 ESTABLISHED 和 TIME_WAIT,其他状态下的 TCP 处理都走这里
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
// ......

// 第一次握手或第三次握手,服务器收到ack包
case TCP_LISTEN:
if (th->ack)
// 如果是响应包
return 1;

if (th->rst)
goto discard;

if (th->syn) {
// 如果是SYN握手包
// conn_request是个函数指针,指向tcp_v4_conn_request
// 服务器响应 SYN 的主要处理逻辑都在这个tcp_v4_conn_request里
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;

kfree_skb(skb);
return 0;
}

// ......
}


// file: net/ipv4/tcp_ipv4.c
// 响应SYN的主要处理逻辑都在这个方法中
// 1.先判断一下半连接队列是否满了
// 如果满了就进入tcp_syn_flood_action(syn flood攻击)查看是否开启tcp_syncookies内核参数,没有开启就会丢弃该握手包
// 2.判断全连接队列是否满了
// 如果满了并且如果有young_ack,直接丢弃
// young_ack(未处理完的半连接请求)是半连接队列里保持着的一个计数器。
// 记录的是刚有SYN到达,没有被SYN_ACK重传定时器重传过SYN_ACK,
// 同时也没有完成过三次握手的sock数量
// 3.申请request_sock分配内核对象
// 4.构造syn+ack包并发送响应,tcp_v4_send_synack()方法中。
// 5.添加到半连接队列中并开启计时器重传。
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
// ......

// 判断半连接队列是否满了,如果满了就进入syn_flood_warning去判断是否开启
// 了 tcp_syncookies 内核参数。
// 如果队列满,且未开启 tcp_syncookies,那么该握手包将直接被丢弃
if (inet_csk_reqsk_queue_is_full(sk) && !isn)
if (net_ratelimit())
syn_flood_warning(skb);

// ......

// 在全连接的情况下,如果有young_ack,那么直接丢
// young_ack 是半连接队列里保持着的一个计数器。
// 记录的是刚有SYN到达,没有被SYN_ACK重传定时器重传过SYN_ACK,
// 同时也没有完成过三次握手的sock数量
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;

// 分配request_sock内核对象
req = inet_reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop;

// ......

// tcp_v4_send_synack()构造并发送syn+ack响应
if (tcp_v4_send_synack(sk, dst, req,
(struct request_values *)&tmp_ext) ||
want_cookie)
goto drop_and_free;

// 添加到半连接队列,并开启计时器
// 计时器的作用是在某个时间之内还收不到客户端的第三次握手,
// 服务器就会重传syn+ack包
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);

// ......
}


// file: net/ipv4/tcp_ipv4.c
// 构造并发送syn + ack
static int tcp_v4_send_synack(struct sock *sk, struct dst_entry *dst,
struct request_sock *req,
struct request_values *rvp)
{
// ......

// 构造syn + ack包
skb = tcp_make_synack(sk, dst, req, rvp);

if (skb) {
// 如果构建成功
__tcp_v4_send_check(skb, ireq->loc_addr, ireq->rmt_addr);

// 发送syn + ack响应
err = ip_build_and_send_pkt(skb, sk, ireq->loc_addr,
ireq->rmt_addr,
ireq->opt);
err = net_xmit_eval(err);
}

dst_release(dst);
return err;
}

客户端响应SYN + ACK

清除了 connect 时设置的重传定时器,把当前 socket 状态设置为 ESTABLISHED,开启保活计时器后发出第三次握手的 ack 确认。

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
// file:net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
// ......

// 客户端第二次握手处理
case TCP_SYN_SENT:
// 处理 syn+ack 包
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
if (queued >= 0)
return queued;

/* Do step6 onward by hand. */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return 0;
}
}


// file: net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
// ......

if (th->ack) {
// ......

// 修改socket状态
tcp_set_state(sk, TCP_ESTABLISHED);

// 连接建立完成
security_inet_conn_established(sk, skb);

// ......

// 初始化拥塞控制
tcp_init_congestion_control(sk);

// ......

// 保活计时器打开
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));

// ......

if (sk->sk_write_pending ||
icsk->icsk_accept_queue.rskq_defer_accept ||
icsk->icsk_ack.pingpong) {
/* Save one ACK. Data will be ready after
* several ticks, if write_pending is set.
*
* It may be deleted, but with this feature tcpdumps
* look so _wonderfully_ clever, that I was not able
* to stand against the temptation 8) --ANK
*/

// 延迟确认
inet_csk_schedule_ack(sk);
icsk->icsk_ack.lrcvtime = tcp_time_stamp;
icsk->icsk_ack.ato = TCP_ATO_MIN;
tcp_incr_quickack(sk);
tcp_enter_quickack_mode(sk);
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
TCP_DELACK_MAX, TCP_RTO_MAX);

discard:
__kfree_skb(skb);
return 0;
} else {
// 申请构造ack包然后返回响应
tcp_send_ack(sk);
}
return -1;
}

/* No ACK in the segment */

if (th->rst) {
/* rfc793:
* "If the RST bit is set
*
* Otherwise (no ACK) drop the segment and return."
*/

goto discard_and_undo;
}

// ......

if (th->syn) {
/* We see SYN without ACK. It is attempt of
* simultaneous connect with crossed SYNs.
* Particularly, it can be connect to self.
*/

// 如果是syn包,就设置socket状态为TCP_SYN_RECV
tcp_set_state(sk, TCP_SYN_RECV);

// ......

// 序号seq+1
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

// ......

// 发送syn + ack包
tcp_send_synack(sk);
}
// ......
}


// file:net/ipv4/tcp_output.c
void tcp_send_ack(struct sock *sk)
{
// ......

// 申请和构造ack包
buff = alloc_skb(MAX_TCP_HEADER, GFP_ATOMIC);

// ......

// 发送ack包
tcp_transmit_skb(sk, buff, 0, GFP_ATOMIC);
}

服务端响应ACK

把当前半连接对象删除,创建了新的 sock 后加入到全连接队列中,最后将新连接状态设置为 ESTABLISHED。

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
// 服务端第三次握手的ack与第一次握手一样,都会进入到tcp_v4_do_rcv,此时去半连接队列中查看就不是空的了,会保留第一次握手的半连接信息
// file:net/ipv4/tcp_ipv4.c
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
// ......

// 查找listen socket的半连接队列
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
// 如果找到了
return tcp_check_req(sk, skb, req, prev);

// ......
}


// file:net/ipv4/tcp_minisocks.c
// 主要是创建一个子socket,然后清理半连接队列,添加到全连接队列中
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct request_sock **prev)
{
// ......

// 创建子socket
// 对应的是tcp_v4_syn_recv_sock 函数
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;

// 清理半连接队列
inet_csk_reqsk_queue_unlink(sk, req, prev);
inet_csk_reqsk_queue_removed(sk, req);

// 添加全连接队列
inet_csk_reqsk_queue_add(sk, req, child);
return child;

// ......
}


// file:net/ipv4/tcp_ipv4.c
// 创建sock内核对象
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
// ......

// 判断接收队列是否满了,如果满了就修改一下计数器然后丢弃
if (sk_acceptq_is_full(sk))
goto exit_overflow;

// 创建sock && 初始化
newsk = tcp_create_openreq_child(sk, req, skb);
// ......
}


// file: include/net/inet_connection_sock.h
static inline void inet_csk_reqsk_queue_unlink(struct sock *sk,
struct request_sock *req,
struct request_sock **prev)
{
// 把连接请求块从半连接队列中删除
reqsk_queue_unlink(&inet_csk(sk)->icsk_accept_queue, req, prev);
}


// file:net/ipv4/syncookies.c
static inline void inet_csk_reqsk_queue_add(struct sock *sk,
struct request_sock *req,
struct sock *child)
{
// 将握手成功的request_sock对象插入到全连接队列链表的尾部
reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child);
}


// file:net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
// ......
switch (sk->sk_state) {
case TCP_SYN_RECV:
//第三次握手处理
if (acceptable) {
tp->copied_seq = tp->rcv_nxt;
smp_mb();

// 修改状态为已连接
tcp_set_state(sk, TCP_ESTABLISHED);

// ......

} else {
return 1;
}
break;
}
// ......
}

accept()

服务端accept:从已建立好的全连接队列中取第一个返回给用户进程

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
// file: net/ipv4/inet_connection_sock.c
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
// ......

// 从全连接队列中获取第一个元素
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);

// ......
}


// file:include/net/request_sock.h
static inline struct sock *reqsk_queue_get_child(struct request_sock_queue *queue,
struct sock *parent)
{
// 从全连接队列中获取第一个元素
struct request_sock *req = reqsk_queue_remove(queue);
struct sock *child = req->sk;

WARN_ON(child == NULL);

sk_acceptq_removed(parent);
__reqsk_free(req);
return child;
}


// file:include/net/request_sock.h
static inline int reqsk_queue_removed(struct request_sock_queue *queue,
struct request_sock *req)
{
struct listen_sock *lopt = queue->listen_opt;

if (req->retrans == 0)
--lopt->qlen_young;

return --lopt->qlen;
}


总结

握手前的准备

  • 客户端:通过socket()方法获取一个fd文件描述符,通过bind()方法绑定一个端口(可以不调用该方法;如果调用了,首先会判断用户 传入的端口号是否大于1024,然后通过inet_csk_get_port方法判断该端口是否被占用,如果被占用就返回``EADDRINUSE`。只会在bind状态的socket里面查找,不会去ESTABLISH 的哈希表进行可用检测)
  • 服务端:通过socket()方法获取一个fd文件描述符,通过listen()方法初始化连接队列(全连接队列大小 = min(backlog, net.core.somaxconn),半连接队列大小 = min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再向上取整到 2 的幂次,但最小不能小于16

第一次握手

  • 客户端:根据传入的fd文件描述符,找到对应的socket内核对象。调用sock->ops->connect方法(inet_stream_connect),然后在tcp_v4_connect()方法中将socket状态设置为TCP_SYN_SENT,通过_inet_hash_connect()方法动态选择一个端口号(首先判断是否bind()了一个端口,如果没有,就根据目标地址和端口等信息生成一个随机数,然后在本地端口范围中通过这个随机数逐渐增加遍历,如果是本地配置的保留端口就跳过,如果不是就遍历已经使用的端口的哈希链表(hinfo->bhash),判断是否已经被使用,如果该端口已经被使用并且TCP连接中的四元组与当前建立的四元组完全一致,就不能使用,继续遍历。找到了就返回端口,没找到就提示Address already in use)。通过tcp_connect()构建skb并添加到发送队列sk_write_queue上,启动重传定时器,进行发送。

第二次握手

  • 服务端:首先会判断半连接队列是否满了,如果满了就进入syn_flood_warning去判断是否开启了 tcp_syncookies 内核参数。如果队列满,且未开启 tcp_syncookies,那么该握手包将直接被丢弃,然后去判断一下全连接队列是否满了,如果满了并且有young_ack,直接丢弃,否则发出 synack。申请request_sock 添加到半连接队列中,同时启动定时器。young_ack(未处理完的半连接请求)是半连接队列里保持着的一个计数器。记录的是刚有SYN到达,没有被SYN_ACK重传定时器重传过SYN_ACK,同时也没有完成过三次握手的sock数量。
  • 客户端:清除重传定时器,将当前socket状态设置为ESTABLISHED,开启保活计时器后发出ack确认

第三次握手

  • 服务端:找到半连接队列中的request_sock对象,判断全连接队列是否满了,如果满了就修改计数器然后丢弃,没满就创建新的sock对象,将半连接中的连接请求进行删除,添加到全连接队列中,将状态设置为TCP_ESTABLISHED

最后

调用accept()方法从全连接队列中获取一个返回给用户进程

作者

zhaommmmomo

发布于

2021-07-11

更新于

2023-06-27

许可协议