Linux 内核网络之 tcp 三次握手

系统 Linux
今天我们一起来了解一下Linux 内核网络之 tcp 三次握手的相关流程。

三次握手流程如下图:

服务器端监听

在client端向server端进行连接前,server处于监听状态。流程如下:

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;
//计算半连接队列的长度
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
..
//为半连接队列申请内存
lopt_size += nr_table_entries * sizeof(struct request_sock );
if (lopt_size > PAGE_SIZE)
/ 如果申请内存大于1页,则申请虚拟地址连续的空间 /
lopt = __vmalloc(lopt_size,GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO,PAGE_KERNEL);
else
/ 申请内存在1页内,则申请物理地址连续的空间 */
lopt = kzalloc(lopt_size, GFP_KERNEL);
//全连接队列头初始化
queue->rskq_accept_head = NULL;
// 半连接队列的最大长度
lopt->nr_table_entries = nr_table_entries;
...
//半连接队列设置
queue->listen_opt = lopt;
return 0;
}

服务器端在监听时初始化半连接 hash 表,然后挂到接收队列中,等待客户端连接。

另外 queue->rskq_accept_head为全连接队列,是一个以链表的形式进行管理全连接。

关于半连接和全连接的结构如下:

client 发送 SYN 报文

client 向 server 进行 connect 时,最终会调用 tcp_v4_connect 向server发送 SYN 报文。

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
//设置 socket 状态为 TCP_SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
...
//函数用来根据 sk 中的信息,构建一个完成的 syn 报文,并将它发送出去。
err = tcp_connect(sk);
...
}
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
tcp_connect_init(sk);
//申请 skb 并构造为一个 SYN 包
buff = alloc_skb_fclone(MAX_TCP_HEADER + 15, sk->sk_allocation);
...
//添加到发送队列 sk_write_queue 上
__skb_queue_tail(&sk->sk_write_queue, buff);
...
//发送syn报文
tcp_transmit_skb(sk, buff, 1, GFP_KERNEL);
...
/* 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_connect 中申请并构造一个SYN 包,然后将其发出。同时还启动了一个重传定时器,该定时器的作用就是等到一定时间后若收不到服务器的反馈时进行重传。

server 端接收 SYN 并发送 SYN+ACK

所有到server端的tcp数据包都会经过网卡、软中断,最终到 tcp_v4_rcv。

在 tcp_v4_rcv 中根据报文报文头中的信息,从监听的hash 表 listening_hash 中找到对应监听的 socket 结构。然后进入 tcp_v4_do_rcv

进行握手处理。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
//服务器收到第一步握手 SYN 或者第三步 ACK 都会走到这里
if (sk->sk_state == TCP_LISTEN) {
//从半连接表syn_table中取出节点
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
...
}
}

由于当前 socket 是 listen 状态,首先会到 tcp_v4_hnd_req 去查看半连接队列。服务器第一次响应 SYN 的时候,半连接队列里是空的,所以相当于什么也没干就返回了。

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
// 从半连接队里中查询
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr);
return sk;
}
struct request_sock *inet_csk_search_req(const struct sock *sk,
struct request_sock ***prevp,
const __be16 rport, const __be32 raddr,
const __be32 laddr)
{
for (prev = &lopt->syn_table[inet_synq_hash(raddr, rport, lopt->hash_rnd,
lopt->nr_table_entries)];
(req = *prev) != NULL;
prev = &req->dl_next) {
...
}
}
return req;
}

在 tcp_rcv_state_process 中根据各种状态做不同的处理。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
case TCP_LISTEN:
...
//判断是否为 SYN 包
if(th->syn) {
//调用 tcp_v4_conn_request
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
...
}
goto discard;
case TCP_SYN_SENT:
...
return 0;
}

由于对方发来的 SYN 报文, 调用 tcp_v4_conn_request 进行处理。

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
...
//查看半连接队列是否已满
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
//在全连接队列满的情况下,如果有 young_ack,那么直接丢
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
//分配 request_sock 内核对象,该request_sock->sk 此时还为空
req = reqsk_alloc(&tcp_request_sock_ops);
if (!req)
goto drop;
...
tcp_rsk(req)->snt_isn = isn;
// 发送 syn+ack 包
if (tcp_v4_send_synack(sk, req, dst))
goto drop_and_free;
if (want_cookie) {
reqsk_free(req);
} else {
//添加到半连接队列,并开启计时器,
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}
return 0;
}

inet_csk_reqsk_queue_is_full 如果返回 true 就表示半连接队列满了,另外 sysctl_tcp_syncookies 判断是否打开了内核参数 tcp_syncookies,如果未打开则返回 false。

如果半连接队列满了,而且 ipv4.tcp_syncookies 参数设置为 0,那么来自客户端的握手包将 goto drop,也就是数据包直接丢弃,此时客户端感知不到报文被 server 丢弃,依靠重传定时器重传。

SYN Flood 攻击就是通过消耗光服务器上的半连接队列来使得正常的用户连接请求无法被响应。不过在现在的 Linux 内核里只要打开 tcp_syncookies,半连接队列满了仍然也还可以保证正常握手的进行。

sk_acceptq_is_full 来判断全连接队列是否满了, inet_csk_reqsk_queue_young 判断的是有没有 young_ack(未处理完的半连接请求)。

若全连接队列满的情况下,且同时有 young_ack ,那么内核同样直接丢掉该 SYN 握手包。

young_ack 是半连接队列里保持着的一个计数器。记录的是刚有SYN到达,没有被SYN_ACK重传定时器重传过 SYN_ACK,同时也没有完成过三次握手的 sock 数量。

从上面可以看到,若队列已满,server 端直接丢弃报文,并不通知客户端。这时候客户端只能通过发送 SYN 包时开启的重传定时器超时进行重传。

server 构造 synack 报文进行回应。

若启用 syncookies,则是根据需要来判断三次握手的,因此无需保存连接请求,直接将其释放。

若未开启 syncookies,则需将连接请求块保存到其父传输控制块中的半连接散列表中,并设置启动连接定时器。计时器的作用是如果某个时间之内还收不到客户端的第三次握手的话,服务器会重传 synack 包。

request_sock 内核对象加入到半连接表中,如下图

客户端响应 SYNACK

client 收到 synack 包后,最终会走到 tcp_rcv_state_process中,此时socket的状态为 TCP_SYN_SENT

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
case TCP_LISTEN:
...
case TCP_SYN_SENT:
//处理 synack 包,返回值大于0表示需给对方发送RST段,该TCP段的释放由tcp_rcv_state_process调用者处理
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, tp);
return 0;
}
...
return 0;
}

关于 synack 包的处理逻辑为 tcp_rcv_synsent_state_process。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
...
if (th->ack) {
...
//若收到ack+rst段,则调用tcp_reset设置ECONNREFUSED错误码,同时通知等待该套接口的进程,然后关闭套接口
if (th->rst) {
tcp_reset(sk);
goto discard;
}
//在SYS_SENT状态下接收的段必须存在SYN标志,否则说明接收到的段无效,然后跳到discard_and_undo丢弃该段
if (!th->syn)
goto discard_and_undo;
TCP_ECN_rcv_synack(tp, th);
// 初始化与窗口有关的成员变量
tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
tcp_ack(sk, skb, FLAG_SLOWPATH);// 删除发送队列和重传定时器
/* Ok.. it's good. Set up sequence numbers and
• move to established.
*/
tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
...
//设置已完成连接状态
tcp_set_state(sk, TCP_ESTABLISHED);
...
//初始化拥塞控制
tcp_init_congestion_control(sk);
...
//若启用了连接保活,则启用连接保活定时器
if (sock_flag(sk, SOCK_KEEPOPEN))
inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
if (!tp->rx_opt.snd_wscale)
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0;
//若不处于SOCK_DEAD,则唤醒等待该套接口的进程,同时向套接口的异步等待队列上的进程发送信号,通知他们该套接口可以输出数据了
if (!sock_flag(sk, SOCK_DEAD)) {
/* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 /
sk->sk_state_change(sk);
/ 如果使用了异步通知,则发送SIGIO通知进程可写 */
sk_wake_async(sk, 0, POLL_OUT);
}
...
discard:
__kfree_skb(skb);
return 0;
} else {
// 发送ack段,同时更新窗口。
tcp_send_ack(sk);
}
return -1;
}
...
}

处理 TCP 段中存在 ACK 标志的情况。

启动连接保活定时器。

唤醒调用connect阻塞的进程,然后发送 ACK 报文。

void tcp_send_ack(struct sock sk)
{
/ If we have been reset, we may not send again. */
//发送ack时,tcp必须不在TCP_CLOSE状态
if (sk->sk_state != TCP_CLOSE) {
...
//为ack分配一个SKB,若分配失败则在启动延时确认定时器后返回
buff = alloc_skb(MAX_TCP_HEADER, GFP_ATOMIC);
...
/* Send it off, this clears delayed acks for us. */
//设置序号和发送时间,调用tcp_transmit_skb将ack段发送出去
TCP_SKB_CB(buff)->seq = TCP_SKB_CB(buff)->end_seq = tcp_acceptable_seq(sk, tp);
TCP_SKB_CB(buff)->when = tcp_time_stamp;
tcp_transmit_skb(sk, buff, 0, GFP_ATOMIC);
}
}

client 收到对端发送的 synack 包后,清除了 connect 时设置的重传定时器,把 socket 状态设置为 TCP_ESTABLISHED,同时唤醒调用 connect 而阻塞的进程,开启保活定时器并发送第三次握手的 ack 确认包。

server 端处理 ACK 包

关于 server 处理 ack 报文的过程如下:

|-> tcp_v4_do_rcv

. |-> tcp_v4_hnd_req

. . |-> inet_csk_search_req // 从半连接中取出连接请求块request_sock

. . |-> tcp_check_req

. . . |-> syn_recv_sock => tcp_v4_syn_recv_sock

. . . . |-> tcp_create_openreq_child

. . . . . |-> inet_csk_clone // 生成一个sock结构,设置 TCP_SYN_RECV 状态

. . . |->

inet_csk_reqsk_queue_unlink // 把连接请求块request_sock从半连接队列中删除

. . . |-> inet_csk_reqsk_queue_add //把request_sock和生成的sock进行关联,并挂到icsk_accept_queue 全连接队列中

. |-> tcp_child_process

. . |-> tcp_rcv_state_process //设置状态 TCP_ESTABLISHED

. . |-> sk_data_ready => sock_def_readable //唤醒阻塞在accept上的进程

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
//服务器收到第一步握手 SYN 或者第三步 ACK 都会走到这里
if (sk->sk_state == TCP_LISTEN) {
//从半连接表syn_table中取出连接请求块request_sock,同时生成一个新的sock结构
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
// 新生成的sock和监听的不一样
if (nsk != sk) {
//设置状态 TCP_ESTABLISHED并唤醒阻塞在accept上的进程
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
...
}
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
...
// 从半连接hash表中获取连接请求块request_sock
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);
...
}
inet_csk_search_req

在半连接队列里进行查找并返回一个半连接 request_sock 对象。然后进入到 tcp_check_req 中

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); //tcp_v4_syn_recv_sock
//清理半连接队列
inet_csk_reqsk_queue_unlink(sk, req, prev);
inet_csk_reqsk_queue_removed(sk, req);
//把request_sock和生成的sock进行关联,并把request_sock添加到全连接队列
inet_csk_reqsk_queue_add(sk, req, child);
return child;
...
}

创建子 socket 并初始化,然后把新生成newsk 加入到 ehash hash 表中, 以后当有报文到来时,从该 hash 表中找对应的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);
...
// 把 newsk 加入到 已完成链接的ehash hash表中
__inet_hash(&tcp_hashinfo, newsk, 0);
__inet_inherit_port(&tcp_hashinfo, sk, newsk);
...
}

设置 TCP_ESTABLISHED 状态

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len)
{
...
if (th->ack) {
...
switch(sk->sk_state) {
case TCP_SYN_RECV:
//设置状态 TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
...
}

第三次握手的主要功能就是从半连接 hash表中摘除连接请求块 request_sock,然后生成一个 sock 与之进行关联,然后再把 request_sock 添加到全连接队列。

server 从 accept 中被唤醒

server 调用 accept 时由于 icsk_accept_queue 队列没有为空,进程被阻塞等待。

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
...
if (sk->sk_state != TCP_LISTEN) /* socket必须处于监听状态 *
goto out_err;
/* Find already established connection /
// 发现没有ESTABLISHED状态的连接请求块
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
/ 等待超时时间,如果是非阻塞则为0 */
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep /
error = -EAGAIN;
if (!timeo) / 如果是非阻塞的,则直接退出 /
goto out_err;
/ 阻塞等待,直到有全连接。如果用户有设置等待超时时间,超时后会退出 /
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
/ 获取新连接的sock,释放连接控制块 */
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);
out:
release_sock(sk);
return newsk;
out_err:
newsk = NULL;
*err = error;
goto out;
}

上三次握手完成后,server 被唤醒,此时全连接队列 icsk_accept_queue 不空,server 调用 reqsk_queue_get_child() 从全连接队列中获取一个新的sock。

static inline struct sock *reqsk_queue_get_child(struct request_sock_queue *queue,
struct sock parent)
{
/ 从全连接队列中,取出第一个ESTABLISHED状态的连接请求块 */
struct request_sock *req = reqsk_queue_remove(queue);
struct sock child = req->sk; / 一个已建立的连接 */
BUG_TRAP(child != NULL);
/* 当前backlog队列的全连接数减一 */
sk_acceptq_removed(parent);
__reqsk_free(req);
return child;
}

accept 的作用就是从已经建立好的全连接队列中取出一个返回已完成连接的 sock 返回给用户进程。

责任编辑:华轩 来源: 今日头条
相关推荐

2021-03-08 18:08:08

TCP Connect 协议

2023-09-07 16:46:54

TCP数据传递

2023-10-24 15:22:09

TCPUDP

2020-12-08 06:34:16

TCP握手SYN 报文

2022-10-10 07:34:36

TCP三次握手区块链

2015-10-13 09:42:52

TCP网络协议

2019-06-12 11:26:37

TCP三次握手四次挥手

2024-01-12 08:23:11

TCPACK服务器

2022-07-25 07:07:35

TCP客户端服务器

2021-07-03 17:47:25

TCP控制协议

2018-10-15 08:06:33

TCP握手原理

2023-09-02 21:57:52

网络TCP协议

2018-07-05 14:25:01

TCP握手原理

2022-07-07 09:00:17

TCP 连接HTTP 协议

2019-12-12 10:36:43

TCPSYNIP

2020-08-27 07:41:28

TCP协议数据

2021-01-29 06:11:08

TCP通信三次握手

2021-05-18 12:27:40

TCP控制协议

2019-02-01 09:38:16

2023-03-01 23:56:11

点赞
收藏

51CTO技术栈公众号