文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Linux 内核网络之 tcp 三次握手

2024-11-30 18:07

关注

服务器端监听

在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);
...

//启动重传定时器
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;

//在处理完接收的段后,还需要处理紧急数据,然后释放该段,最后检测是否有数据需要发送。
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);// 删除发送队列和重传定时器

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)) {

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);
...

//设置序号和发送时间,调用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)
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

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);

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

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

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯