zoukankan      html  css  js  c++  java
  • 深入理解TCP协议及其源代码

    TCP握手协议

    在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接.

    第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

    SYN:同步序列编号(Synchronize Sequence Numbers)

    第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

    第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手.

    完成三次握手,客户端与服务器开始传送数据。

    A与B建立TCP连接时:首先A向B发SYN(同步请求),然后B回复SYN搜索+ACK(同步请求应答),最后A回复ACK确认,这样TCP的一次连接(三次握手)的过程就建立了!

    一、TCP报文格式

            TCP/IP协议的详细信息参看《TCP/IP协议详解》三卷本。下面是TCP报文格式图:


    图1 TCP报文格式

            上图中有几个字段需要重点介绍下:
            (1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
            (2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
            (3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
                    (A)URG:紧急指针(urgent pointer)有效。
                    (B)ACK:确认序号有效。
                    (C)PSH:接收方应该尽快将这个报文交给应用层。
                    (D)RST:重置连接。
                    (E)SYN:发起一个新连接。
                    (F)FIN:释放一个连接。

            需要注意的是:
                    (A)不要将确认序号Ack与标志位中的ACK搞混了。
                    (B)确认方Ack=发起方Req+1,两端配对。 

    二、三次握手
            所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:


    图2 TCP三次握手

            (1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
            (2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
            (3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
            
            SYN攻击:
                    在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
                    #netstat -nap | grep SYN_RECV

    下面用抓包工具wireshark

    wireshark安装:sudo apt-get install wireshark

    下面会出现

     选择yes。

    如果没有出现,则需要我们手动设置:

    #dpkg-reconfigure wireshark-common
    “Should non-superusers be able to capture packages?//选择Yes (默认是no)

    当前用户获取权限:

    sudo vim /etc/group

     保存退出即可。

    运行直接输入:wireshark

    下面开始加入断点,在server.c文件中bind,listen,accpet之前加入break_bind(),break_listen(),break_accpet()

     在client.c中connect()之前加入break_connect().分别编译两个文件gcc -o client client.c,gcc -o server server.c,生成可执行程序client,server。

    打开两个终端分别输入:

    gdb server gdb client

     开始打断点 :

    在上面右图中打上 b break_bind() b break_listen b break_accept

    在上面左图中打上 b break_connect(),

    然后开始运行wireshark。

    开始调试gdb server中   

    start

    c

    c

    此时accept陷入阻塞。查看wireshark没有任何数据

    开始调试gdb client中,运行connect()也没有抓到任何数据,

    start 

    c

    之后有数据了。

     

     

     三次握手的具体过程发生在accept和connect之间。

    分析代码:

    结构体变量struct proto tcp_prot指定了TCP协议栈的访问接口函数:

     

     

     首先客户端发送SYN报文:

    tcp_v4_connect函数:发送syn,设置TCP_SYN_SENT(tcp_set_state),构造SYN并发送(tcp_connect).

    tcp_set_state(sk, TCP_SYN_SENT);
        err = inet_hash_connect(tcp_death_row, sk);
        if (err)
            goto failure;
    
        sk_set_txhash(sk);
    
        rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                       inet->inet_sport, inet->inet_dport, sk);
        if (IS_ERR(rt)) {
            err = PTR_ERR(rt);
            rt = NULL;
            goto failure;

    tcp_connect函数:

    /* Build a SYN and send it off. */
    int tcp_connect(struct sock *sk)
    {
    ...
         /* Reserve space for headers. */
          skb_reserve(buff, MAX_TCP_HEADER);
    
           tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
          tp->retrans_stamp = tcp_time_stamp;
          tcp_connect_queue_skb(sk, buff);
           tcp_ecn_send_syn(sk, buff);
    
        /* 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);
    ...
          /* 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);
    ...

    
    

    另一头服务端accept等待连接请求

    inet_csk_accept函数

    /*
     * This will accept the next outstanding connection.
     */
    struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
    {
    
    
     /* We need to make sure that this socket is listening,
     * and that it has something pending.
     */
     error = -EINVAL;
     if (sk->sk_state != TCP_LISTEN)
     goto out_err;
    
     /* Find already established connection */
     if (reqsk_queue_empty(queue)) {
     long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
    ...
     error = inet_csk_wait_for_connect(sk, timeo);
    ...
    }
    EXPORT_SYMBOL(inet_csk_accept);

    inet_csk_wait_for_connect()函数:

    /*
    * Wait for an incoming connection, avoid race conditions. This must be called
     * with the socket locked.
     */245static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
    {
    ... //无限for循环,一旦有请求则跳出
     for (;;) {
     prepare_to_wait_exclusive(sk_sleep(sk), &wait,
     TASK_INTERRUPTIBLE);
    ...
    }

     三次握手中携带SYN/ACK的TCP头数据的发送和接收
    连接建立成功后,接收数据放入accept队列
    跟踪这部分代码的思路:

    网卡接收到数据需要通知上层协议来接收并处理数据,那么应该有TCP协议的接收数据的函数被底层网络驱动callback方式进行调用,针对这个思路我们需要回头来看TCP/IP协议栈的初始化过程是不是有将recv的函数指针发布给网络底层代码

    TCP/IP协议栈初始化:
    inet_init函数

    static const struct net_protocol tcp_protocol = {
     .early_demux = tcp_v4_early_demux,
     .handler = tcp_v4_rcv,
     .err_handler = tcp_v4_err,
     .no_policy = 1,
     .netns_ok = 1,
     .icmp_strict_tag_validation = 1,
    };
    ...
    static int __init inet_init(void)
    {
    ...
     /*
     * Add all the base protocols.
     */
    
     if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
     pr_crit("%s: Cannot add ICMP protocol
    ", __func__);
     if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
     pr_crit("%s: Cannot add UDP protocol
    ", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
     pr_crit("%s: Cannot add TCP protocol
    ", __func__);
    #ifdef CONFIG_IP_MULTICAST1719 if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
     pr_crit("%s: Cannot add IGMP protocol
    ", __func__);
    #endif
    ...
    }



     服务端接收客户端发来的SYN,发送SYN+ACK
    tcp_v4_do_rcv函数

    /* The socket must have it's spinlock held when we get
     * here.
     *
     * We have a potential double-lock case here, so even when
     * doing backlog processing we use the BH locking scheme.
     * This is because we cannot sleep with the original spinlock
     * held.
     */
    int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
    {
    ...
     if (sk->sk_state == TCP_LISTEN) {
     struct sock *nsk = tcp_v4_hnd_req(sk, skb);
     if (!nsk)
     goto discard;
    
     if (nsk != sk) {
    ...
     return 0;
     }
     } else
     sock_rps_save_rxhash(sk, skb);
    
     if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
     rsk = sk;
     goto reset;
     }
    ...
    }
    EXPORT_SYMBOL(tcp_v4_do_rcv);

    客户端收到服务端的SYN+ACK,发送ACK

    tcp_rcv_synsent_state_process函数:

    static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,const struct tcphdr *th, unsigned int len)
    {
    
    ..
     tcp_send_ack(sk);
    ...
    }

    到这里我们已经从linux网络核心的角度从架构上整体理解了三次握手,即携带SYN/ACK标志的数据收发过程

  • 相关阅读:
    深入浅出js单例模式
    前端常见面试-存储/缓存篇
    JavaScript内存管理
    delete操作符
    解决window.location.href跳转无效问题解决办法
    前端程序员经常忽视的一个JavaScript面试题
    【华为云技术分享】漫谈LiteOS-端云互通组件-MQTT开发指南(上)
    【华为云技术分享】序列特征的处理方法之二:基于卷积神经网络方法
    【华为云技术分享】原来CTR预估模型的发展有这样的规律
    【华为云技术分享】在家办公怎么弄?华为云DevCloud宝典一看就懂——项目管理篇
  • 原文地址:https://www.cnblogs.com/buzhidao1/p/12096595.html
Copyright © 2011-2022 走看看