Rxsi Blog GameServer Developer

握手&挥手源码解析:第一次握手

2022-09-08
Rxsi
TCP

第一次握手

tcp_v4_connect

IPV4的条件下,第一次握手对应的内核源码是tcp_v4_connect,具体代码内容如下:

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; // 上层传入的目标地址信息
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    __be16 orig_sport, orig_dport; // __be16、__be32是大端序,网络序是大端序
    __be32 daddr, nexthop;
    struct flowi4 *fl4;
    struct rtable *rt;
    int err;
    struct ip_options_rcu *inet_opt;
    struct inet_timewait_death_row *tcp_death_row = sock_net(sk)->ipv4.tcp_death_row;

    if (addr_len < sizeof(struct sockaddr_in)) // 地址长度校验
        return -EINVAL;

    if (usin->sin_family != AF_INET) // 是否是IPV4
        return -EAFNOSUPPORT;

    // 一些地址、路由信息的处理
    // ....
    
    tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; // 设置报文的初始MSS值, 默认值为563

    tcp_set_state(sk, TCP_SYN_SENT); // 设置状态为 SYN_SENT
    err = inet_hash_connect(tcp_death_row, sk); // 计算客户端的端口,是通过根据目标IP和端口计算出一个随机数,然后从这个范围里面逐个查找可用的端口号。这里面涉及到tcp_tw_reuse选项,如果设置了该选项,那么就可以复用处于tw状态的socket了。
    if (err)
        goto failure; // 绑定端口号失败
    
    // 地址路由的处理
    // ......

    if (likely(!tp->repair)) {
        if (!tp->write_seq) // 没有设置初始的序号
            WRITE_ONCE(tp->write_seq,
                   secure_tcp_seq(inet->inet_saddr,
                          inet->inet_daddr,
                          inet->inet_sport,
                          usin->sin_port)); // 计算规则是根据双方地址+端口计算出一个seq值,然后再加上(纳秒级时间戳>>6)得到的值,这个值是唯一的。
        tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
                         inet->inet_saddr,
                         inet->inet_daddr);
    }
    
    // ....
    
    err = tcp_connect(sk); // 真正构造SYN包并发送

    if (err)
        goto failure;

    return 0;

failure: // 如果上面有任何一项校验失败,则跳转到这里
    tcp_set_state(sk, TCP_CLOSE); // 设置状态为 CLOSE
    ip_rt_put(rt);
    sk->sk_route_caps = 0;
    inet->inet_dport = 0;
    return err;
}

可见tcp_v4_connect负责 IP 地址和路由信息的校验与设置,同时会设置 TCP 状态并初始发送序列号

tcp_connect

经过了tcp_v4_connect的必要设置,在tcp_connect会进行SYN包的构建与发送

/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk); // 将sock结构体转换成tcp_sock结构体。tcp_sock结构体包含了sock结构体,同时包含了更多tcp独有的数据项
    struct sk_buff *buff; // sk_buff是一个数据报的数据集合,会以双向链表和红黑树进行组织管理,这个和VMA的管理方式一致。
    int err;

    tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);

    if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
        return -EHOSTUNREACH; /* Routing failure or similar. */

    tcp_connect_init(sk); // 初始化tcp_sock里面的TCP信息,包括初始发送/接收序列号,发送/接收窗口、初始化rto=1.

    if (unlikely(tp->repair)) {
        tcp_finish_connect(sk, NULL);
        return 0;
    }

    buff = tcp_stream_alloc_skb(sk, 0, sk->sk_allocation, true); // 申请一个sk_buff
    if (unlikely(!buff))
        return -ENOBUFS;

    tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); // sk_buff初始化
    tcp_mstamp_refresh(tp); // 设置时间戳信息
    tp->retrans_stamp = tcp_time_stamp(tp);
    tcp_connect_queue_skb(sk, buff);
    tcp_ecn_send_syn(sk, buff);
    tcp_rbtree_insert(&sk->tcp_rtx_queue, buff); // 将sk_buff插入sk里面的红黑树中,后续要获取sk_buff只需要从sk->tcp_rtx_queue这颗红黑树上查找即可。

    /* Send off SYN; include data in Fast Open. */
    err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
          tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); // 如果打开了fast open选项,意味着可以在发送SYN包时发送数据,那么后续调用tcp_send_syn_data方法,否则调用tcp_transmit_skb方法,而实际在tcp_send_syn_data中最终调用的也是tcp_transmit_skb方法。tcp_transmit_skb方法负责的是将数据包(sk_buff)发送到IP层
    if (err == -ECONNREFUSED)
        return err;

    /* We change tp->snd_nxt after the tcp_transmit_skb() call
     * in order to make this packet get counted in tcpOutSegs.
     */
    WRITE_ONCE(tp->snd_nxt, tp->write_seq); // 更新序列号
    tp->pushed_seq = tp->write_seq;
    buff = tcp_send_head(sk);
    if (unlikely(buff)) {
        WRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(buff)->seq);
        tp->pushed_seq	= TCP_SKB_CB(buff)->seq;
    }
    TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);

    /* 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); // 超时重传定时器,inet_csk(sk)->icsk_rto值为1,在前面tcp_connect_init中初始化的。后续在定时器超时之后,会以 icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX) 方式进行翻倍增长,最大是120。每次定时器到达时会判断当前已经重试的次数,达到tcp_syn_retries配置次数之后就会停止定时器
    return 0;
}

可以看到在这个函数内,会构建SYN包,并将其传递给更下层的 IP 层进行发包,同时也会启动超时重传定时器,以初始超时时间为1,后续每秒都倍增。最大的重试次数是tcp_syn_retries,默认值为 6

rxsi@VM-20-9-debian:~$ cat /proc/sys/net/ipv4/tcp_syn_retries 
6

tcp_retransmit_timer

该函数负责处理所有需要重发的数据包

void tcp_retransmit_timer(struct sock *sk)
{
    // .....
    if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&
        !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) { // 当对方接收窗口已经变为0的处理
    // ....
    }

    if (tcp_write_timeout(sk)) // 判断是否已经达到重传次数的上限,如果达到上限,那么在这个函数里面会关闭tcp,并返回1
        goto out;

    icsk->icsk_retransmits++; // 重传次数递增
    if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1) > 0) { // 进行重传,内部调用的是tcp_transmit_skb方法,如果返回>0代表重传失败,意味着当前系统资源不足。则直接进行下一轮的定时器,进行重试,定时器的时间戳为TCP_RESOURCE_PROBE_INTERVAL
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      TCP_RESOURCE_PROBE_INTERVAL,
                      TCP_RTO_MAX);
        goto out;
    }

out_reset_timer:
    if (sk->sk_state == TCP_ESTABLISHED &&
        (tp->thin_lto || READ_ONCE(net->ipv4.sysctl_tcp_thin_linear_timeouts)) &&
        tcp_stream_is_thin(tp) &&
        icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) { // 链接已经建立时的重传
        icsk->icsk_backoff = 0;
        icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX); // 重传时间根据rtt计算
    } else {
        /* Use normal (exponential) backoff */
        icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX); // 其他状态的数据包的重传时间都翻倍增长
    }
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                  tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX); // 开启新的定时器,时间间隔是icsk_rto
    if (retransmits_timed_out(sk, READ_ONCE(net->ipv4.sysctl_tcp_retries1) + 1, 0))
        __sk_dst_reset(sk);

out:;
}

static int tcp_write_timeout(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct net *net = sock_net(sk);
    bool expired = false, do_reset;
    int retry_until;

    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { // 当前处于SYN_SENT或者SYN_RECV状态
        if (icsk->icsk_retransmits)
            __dst_negative_advice(sk);
        retry_until = icsk->icsk_syn_retries ? :
            READ_ONCE(net->ipv4.sysctl_tcp_syn_retries);
        expired = icsk->icsk_retransmits >= retry_until; // 判断已重传次数是否已经达到sysctl_tcp_syn_retries配置
    } 
    // ......
    if (expired) { // 已经达到重传上限
        /* Has it gone just too far? */
        tcp_write_err(sk); // 关闭
        return 1;
    }
    // ......
    return 0; // 定时器还可以继续
}

第一次握手丢失会发生什么?

当客户端发送完SYN报文,那么就会进入SYN_SEND状态,在这之后如果未能收到服务端回复的SYN-ACK报文,那么就会通过超时重传机制进行重发,最大的重发次数由tcp_syn_retries(默认是6)控制。如果在限定的次数内没有成功接收到第二次握手包,那么就会关闭 TCP,转为CLOSE状态。

在 TCP 关闭之后如果接收到了上一次的第二次握手包,由于该包是 TCP 报文,因此 IP 层会调用tcp_v4_rcv方法进行处理。而此时如果客户端没有在监听这个端口,那么会由于无法查找到对应的 TCP socket,会回复RST报文。

// IP层接收到TCP数据包时会调用这个方法
int tcp_v4_rcv(struct sk_buff *skb)
{
    // 忽略一些包体长度校验
lookup:
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
                   th->dest, sdif, &refcounted); // 这里传进的是source端口和dest端口,在里面会获取source的IP和dest的IP,然后再进行查找
    if (!sk) // 没有查找到socket,跳转到no_tcp_socket
        goto no_tcp_socket;
    // ....
no_tcp_socket:
    drop_reason = SKB_DROP_REASON_NO_SOCKET;
    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) // 这里固定返回1,即!1得到的结果是false
        goto discard_it;

    tcp_v4_fill_cb(skb, iph, th);

    if (tcp_checksum_complete(skb)) { // 检查check_sum失败
csum_error:
        drop_reason = SKB_DROP_REASON_TCP_CSUM;
        trace_tcp_bad_csum(skb);
        __TCP_INC_STATS(net, TCP_MIB_CSUMERRORS);
bad_packet:
        __TCP_INC_STATS(net, TCP_MIB_INERRS);
    } else { // check_sum校验成功,回复RST报文。
        tcp_v4_send_reset(NULL, skb);
    }
    // ..........
}

通过上面的源码我们可以知道,只要五元组对应不上,那么就无法成功找到 socket,系统就会回复RST报文。

而如果在客户端这边,正好有一个五元组对应上,那么后续的处理逻辑会由于序列号对应不上,而回复RST报文

// TCP处于SYN_SENT状态时的处理函数
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
                     const struct tcphdr *th)
{
    // .....
    if (th->ack) { // 像下面的注释一样,如果这个包有ACK标志,并且序列号范围不对,那么会返回RST包。这种情况就处理了旧的第二次握手包的问题
        /* rfc793:
         * "If the state is SYN-SENT then
         *    first check the ACK bit
         *      If the ACK bit is set
         *	  If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
         *        a reset (unless the RST bit is set, if so drop
         *        the segment and return)"
         */
        if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
            after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
            /* Previous FIN/ACK or RST/ACK might be ignored. */
            if (icsk->icsk_retransmits == 0)
                inet_csk_reset_xmit_timer(sk,
                        ICSK_TIME_RETRANS,
                        TCP_TIMEOUT_MIN, TCP_RTO_MAX);
            goto reset_and_undo; // 这里会返回1,并在上层被捕获,然后发送RST包
        }
    }


reset_and_undo:
    tcp_clear_options(&tp->rx_opt);
    tp->rx_opt.mss_clamp = saved_clamp;
    return 1; // 上层会捕获这个值
}

客户端如果在关闭旧socket之后立即重新发送第一次握手,如果随机出的新端口和上一次的不一样,那么服务端会新建一个 socket 与之对应,此时服务端会成功进入SYN_RECV状态。如果在这之前服务端已经根据旧的握手包建立了一个 socket,那么该socket 就会因为定时重发了SYN-ACK包而达到次数上限或者收到了客户端返回的RST报文而销毁。

如果新端口和上一次的端口是一样的,且服务端因为接收了上一次的旧握手包已处于SYN_RECV状态,则服务端会返回的SYN-ACK。由于里面包含的是序列号是上一次握手的,因此在客户端接收到之后会返回RST使服务端进行重置,后续会继续重发新握手包,建立新链接。

相对应的,如果新的握手包先于旧的握手包到达了服务端,那么服务端接收到旧的握手包也是通过SYN-ACK返回当前记录的正确的序列号,而客户端接收到之后不会因此而关闭连接,只会再次重发序号正确的数据包。相较于旧的握手包先到的处理流程,这时候其实客户端就起到了协助判断是否需要重置当前连接的作用,这也是三次握手的必要性!

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               bool fastopen, bool *req_stolen)
{
    // ....
    if (paws_reject || !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                  tcp_rsk(req)->rcv_nxt, tcp_rsk(req)->rcv_nxt + req->rsk_rcv_wnd)) { // 处于SYN_RECV状态的服务端接收到了一个不在窗口内的包
    /* Out of window: send ACK and drop. */
    if (!(flg & TCP_FLAG_RST) &&
        !tcp_oow_rate_limited(sock_net(sk), skb,
                  LINUX_MIB_TCPACKSKIPPEDSYNRECV,
                  &tcp_rsk(req)->last_oow_ack_time))
        req->rsk_ops->send_ack(sk, skb, req); // 返回ACK报文,服务端本身建立的连接有可能是旧的,因此返回ACK报文,由客户端协助判断。
    if (paws_reject)
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
    return NULL;
    }
}    

Similar Posts

上一篇 进程

Comments