zoukankan      html  css  js  c++  java
  • 网络协议栈(3)server与connect的交互

    一、侦听和连接
    现在暂时不考虑网络拥塞问题,假设我们生活在新闻联播里,网络和谐,网速超快。现在一个好客的server在listen之后通过accept准备接受四面八方的朋友,此时就有一个客户端系统通过connect系统调用来连接这个服务器上的侦听套接口,我们暂时分析一下这个服务器的大致流程。当然,服务器在明,客户端在暗处,正所谓明枪易躲暗箭难防,这个服务器也是很多人希望攻击的对象,最为常见的就是DOS(Denial of Service)攻击,这种攻击从技术上说没有含量,但是通常是最为有效的,正所谓“乱箭射死英雄”,就是这个道理了。当了解了这个大致的过程之后,就可以回过头来看看这些攻击是怎么实现的。
    二、服务器的listen系统调用
    sys_listen--->>>inet_listen--->>>inet_csk_listen_start
    这个调用链中一些关键的操作
    1、inet_csk_listen_start:
    其中进行了最为重要的一个操作,就是设置这个套接口为侦听状态,相当于说小店今天算是正式挂牌开张了。如果没有这个标志,所以向这个端口发送的套接口都不会被接受,因为那是私闯民宅啊。对应代码
        /* There is race window here: we announce ourselves listening,
         * but this transition is still not validated by get_port().
         * It is OK, because this socket enters to hash table only
         * after validation is complete.
         */
        sk->sk_state = TCP_LISTEN;
    2、inet_csk_listen_start--->>reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries)
    该函数根据listen第二个参数backlog的值,在其中分配了一个listen_sock数据结构,并且根据系统调用传入的backlog值为这个结构分配了nr_table_entries(这个值并不一定是系统调用传入的原始值,从代码中看经过了很多的周折和取舍)个数的request_sock指针(这里只是动态分配了指针,但是没有分配实体)。
    然后这个侦听的套接口icsk->icsk_accept_queue.listen_opt指针指向这个listen_sock结构的起始地址,而listen_sock则是用来保存大量的request_sock指针。当然,由于指针结构全部都是知道的,所以得到这些实体也不是难事。
    注意的是:listen不会因为发送和接受而阻塞,它只是设置自己的状态,并申明自己可以等待的资源数,也就是更多的来说只是一个圈地的过程。
    三、accept系统调用
    这个函数是可能进行阻塞的,也可能定时阻塞,也可能无限阻塞。如果说这个accept系统调用的flags中设置了非阻塞模式,那么如果没有远端连接请求到达,那么此时就直接返回了;如果希望等待一定的时间,可以通过setsockopt中的SO_RCVTIMEO来修改该值;在大多数的默认情况下,这将是一个无限等待的过程。
    1、同步等待及accept系统调用返回值的确定
    sys_accept--->>inet_accept--->>>inet_csk_accept
    其中核心的代码为
    /* Find already established connection */
        if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
            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); 这个函数将会在sk->sk_sleep队列上进行休眠,具体的唤醒时机之后分析。
            if (error)
                goto out_err;
        }

        newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);从这里可以看到,此时是从这个icsk->icsk_accept_queue中提取一个远端的连接,加上上面我们看到的wait_for函数,可以看出这个套接口的创建并不是由这个接受函数来实现的,而是在网络报文到达之后由TCP协议栈创建的。由于协议栈不可能无限多的为这个侦听套接口执行来者不拒的接受,所以就要套接口提供一个接受的上限,而这个上限就是之前listen系统调用的第二个参数提供的内容。
    2、accept系统调用int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)第二三个参数的确定
    sys_accept-->>newsock->ops->getname(newsock, (struct sockaddr *)address,&len, 2)
    这里调用的是新创建的套接口的方法而不是原始套接口的getname方法,传入的参数也是新创建的套接口,不过我可以先透个剧,新创建的套接口的getname函数指针就是inet_getname函数,这个函数的实现比较简单,也就是把新套接口中的目标地址和目标端口赋值给sockaddr_in结构,同时把协议簇赋值给sin->sin_family 。从inet_getname代码中看到的
            sin->sin_port = inet->dport;
            sin->sin_addr.s_addr = inet->daddr;
    这里的dport表示服务器端的这个套接口发送的目的,所以是新接入的客户端的IP地址和端口号。
    四、server对客户端connect系统调用的响应
    大家注意了,这就是最为经典的“三次握手”了。大致的思想就是:客户端和服务器两侧都要发送一个SYN,并且要收到这个SYN的ACK。这个SYN中一个关键的内容就是它包含了一个开始的序列号,这个序列号的选取也有大学问,例如syncookie就是利用这个序列号来防止synflood攻击的,这是后话。由于这个过程太经典了,随便google了一下,发现了这个图片很nice,就用了一下,图片来自http://www3.gdin.edu.cn/jpkc/dzxnw/jsjkj/chapter3/35.htm <Figure 3.5-10: TCP three-way handshake: segment exchange>

    网络协议栈(3)server与connect的交互 - Tsecer - Tsecer的回音岛

     从这个图片可以看到,除了syn ack标志位之外,就是两端友好的交换了一下序列号。
    1、服务器接收到SYN报文
    此时假设套接口已经通过listen侦听在了套接口上,此时侦听套接口处于TCP_LISTEN状态,然后又一个同步连接报文翩翩而至。这里我觉得画图比较麻烦,但是退而求其次的贴一下调用链,大家将就着当半个图片看就好了:
    (gdb) bt
    #0  reqsk_queue_hash_req (timeout=750, req=0xc1270660, hash=12, 
        queue=0xcfafdb98) at include/net/request_sock.h:252
    #1  inet_csk_reqsk_queue_hash_add (timeout=750, req=0xc1270660, hash=12, 
        queue=0xcfafdb98) at net/ipv4/inet_connection_sock.c:395
    #2  0xc0755db6 in tcp_v4_conn_request (sk=0xcfafd9c0, skb=0xcfaf15c0)
        at net/ipv4/tcp_ipv4.c:1393
    #3  0xc07f76f9 in tcp_v6_conn_request (sk=0xcfafd9c0, skb=0xcfaf15c0)
        at net/ipv6/tcp_ipv6.c:1243
    #4  0xc074550d in tcp_rcv_state_process (sk=0xcfafd9c0, skb=0xcfaf15c0, 
        th=0xcfa55034, len=40) at net/ipv4/tcp_input.c:4422
    #5  0xc0757022 in tcp_v4_do_rcv (sk=0xcfafd9c0, skb=0xcfaf15c0)
        at net/ipv4/tcp_ipv4.c:1584
    #6  0xc0757fc0 in tcp_v4_rcv (skb=0xcfaf15c0) at net/ipv4/tcp_ipv4.c:1683
    #7  0xc071678b in ip_local_deliver_finish (skb=0xcfaf15c0)
        at net/ipv4/ip_input.c:236
    #8  ip_local_deliver (skb=0xcfaf15c0) at net/ipv4/ip_input.c:275
    #9  0xc07175db in dst_input (skb=0xcfaf15c0) at include/net/dst.h:242
    #10 ip_rcv_finish (skb=0xcfaf15c0) at net/ipv4/ip_input.c:363
    #11 ip_rcv (skb=0xcfaf15c0) at net/ipv4/ip_input.c:434
    #12 0xc06eab0e in netif_receive_skb (skb=0xcfaf15c0) at net/core/dev.c:1840
    #13 0xc06eac56 in process_backlog (backlog_dev=0xc1205200, budget=0xc09f3df4)
        at net/core/dev.c:1874
    这里值得注意的是:
    ①、tcp_v4_conn_request函数中执行了
    req = reqsk_alloc(&tcp_request_sock_ops);
    函数,这个函数的重要意义就在于它分配了一个重要的实体,就是一个request_sock结构实例,回想一下sys_listen中创建的ics结构,它的icsk->icsk_accept_queue.listen_opt志向的是一个listen_sock结构,而结构中包含了大连各的request_sock指针,此时分配的结构就会填充到这个结构中的指针成员中,从而供accept函数查询,更为重要的是它还要供供服务器接收到客户端ACK回应的时候查询。
    这里有一个事实需要注意:就是当客户端connect发送syn之后,服务器只是创建了一个request_sock结构,但是并没有创建真正的网络栈结构sock结构,这个结构的创建将会被推迟。具体什么时候分配这个结构在之后说明。
    ②、分配的request_sock结构进入队列tcp_v4_conn_request-->>inet_csk_reqsk_queue_hash_add--->>>reqsk_queue_hash_req(&icsk->icsk_accept_queue, h, req, timeout)
    static inline void reqsk_queue_hash_req(struct request_sock_queue *queue,
                        u32 hash, struct request_sock *req,
                        unsigned long timeout)
    {
        struct listen_sock *lopt = queue->listen_opt;
    …………
        req->dl_next = lopt->syn_table[hash];
    …………
        lopt->syn_table[hash] = req; 这里可以看到,相同的一个套接口上的请求套接口通过request_sock结构中的dl_next指针连接在一起。
    ……
    }
    ③服务器端构造序列号
    这个其实没什么好说的,只是图中有显示,也是三次握手中一个重要参数,所以可以顺便带过。
    tcp_v4_conn_request-->>>tcp_v4_init_sequence
    ④向客户端回应SYN/ACK报文
    tcp_v4_conn_request--->>tcp_v4_send_synack
    这里服务器向客户端发送连接请求的回应报文,从上面的图中可以看到,这个报文的SYN和ACK均置位。这样服务器就开始等待客户端对自己的SYN进行回应,如果服务器收到对方的ACK,那这是就算成了,两方可以开始真正连接了。由于此时真正的sock结构尚未创建,所以此时仍然使用套接口爸爸的状态,也就是还是处于TCP_LISTEN状态。
    2、服务器受到客户端回应的ACK报文
    真正sock创建时的调用连
    #0  inet_csk_clone (sk=0x0, req=0xc09f3028, priority=3231657964)
        at net/ipv4/inet_connection_sock.c:495
    #1  0xc075bfcd in tcp_create_openreq_child (sk=0xcfafc9e0, req=0xcf9e4680, 
        skb=0xcfae65c0) at net/ipv4/tcp_minisocks.c:379
    #2  0xc0755ea6 in tcp_v4_syn_recv_sock (sk=0xcfafc9e0, skb=0xcfae65c0, 
        req=0xcf9e4680, dst=0xc133eb60) at net/ipv4/tcp_ipv4.c:1426
    #3  0xc07f8492 in tcp_v6_syn_recv_sock (sk=0xcfafc9e0, skb=0xcfae65c0, 
        req=0xcf9e4680, dst=0x0) at net/ipv6/tcp_ipv6.c:1335
    #4  0xc075cb7d in tcp_check_req (sk=0xcfafc9e0, skb=0xcfae65c0, 
        req=0xcf9e4680, prev=0xcfa0f65c) at net/ipv4/tcp_minisocks.c:652
    #5  0xc075681e in tcp_v4_hnd_req (sk=0xcfafc9e0, skb=0xcfae65c0)
        at net/ipv4/tcp_ipv4.c:1492
    #6  0xc0756fc6 in tcp_v4_do_rcv (sk=0xcfafc9e0, skb=0xcfae65c0)
        at net/ipv4/tcp_ipv4.c:1570
    #7  0xc0757fc0 in tcp_v4_rcv (skb=0xcfae65c0) at net/ipv4/tcp_ipv4.c:1683
    #8  0xc071678b in ip_local_deliver_finish (skb=0xcfae65c0)
        at net/ipv4/ip_input.c:236
    #9  ip_local_deliver (skb=0xcfae65c0) at net/ipv4/ip_input.c:275
    ①此次连接通讯(worker)sock的创建
    在 inet_csk_clone函数中,创建了为此次连接而准备的套接口,这个也就是sys_accept将会返回的套接口,这个套接口复制了套接口爸爸的大部分内容。在这个inet_csk_clone函数中,其中设置了这个新创建的套接口为TCP_SYNC_RECV状态,这个是不同于套接口爸爸的TCP_LISTEN状态的。
           newsk->sk_state = TCP_SYN_RECV;
    这个函数中同时还记录了对方的端口号
    inet_sk(newsk)->dport = inet_rsk(req)->rmt_port;
    ②、从TCP_SYN_RECV到TCP_ESTABLISED的转变
    tcp_v4_do_rcv-->>tcp_child_process--->>>tcp_rcv_state_process
    /* step 5: check the ACK field */
        if (th->ack) {
            int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);

            switch(sk->sk_state) {
            case TCP_SYN_RECV: 由于之前已经设置了新创建的套接口为TCP_SYN_RECV状态,所以此时经过这条路径,此时连个人终于对上暗号,接上头了
                if (acceptable) {
                    tp->copied_seq = tp->rcv_nxt;
                    smp_mb();
                    tcp_set_state(sk, TCP_ESTABLISHED);
                    sk->sk_state_change(sk);此时的这个sk是新创建的sock,所以它执行sock_def_wakeup时判断if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))不会满足,所以不做任何操作就返回了
    ③将新创建的sock添加到accept_queue中
    tcp_check_req--->>>inet_csk_reqsk_queue_add(sk, req, child)--->>>reqsk_queue_add(&inet_csk(sk)->icsk_accept_queue, req, sk, child)
        req->sk = child;
    ④对sys_accept的唤醒
    tcp_child_process
        if (state == TCP_SYN_RECV && child->sk_state != state)
                parent->sk_data_ready(parent, 0);
    注意,这里判断的是套接口爸爸,也就是sys_accept的第一个参数对应的sock,而不是新创建的sock,所以执行了sock_def_readable函数,从而将sys_accept系统调用唤醒。
    五、sys_accept被唤醒之后
    此时sys_accept函数在下面等待处被唤醒inet_csk_accept-->>inet_csk_wait_for_connect
        if (reqsk_queue_empty(&icsk->icsk_accept_queue))
                timeo = schedule_timeout(timeo);
    然后对于新的套接口的获取路径为,
    inet_csk_accept--->>>reqsk_queue_get_child(&icsk->icsk_accept_queue, sk)
        struct request_sock *req = reqsk_queue_remove(queue);
        struct sock *child = req->sk;
    此时获得了一个刚才安装好的sock结构,并把这个结构返回给用户。
    六、注意的一个细节
    即使在执行sys_accept之后,套接口爸爸依然是出于TCP_LISTEN状态,这里所说的套接口状态变化都是在新套接口中执行的,这一点尤为重要,因为它关系着
    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) {
                if (tcp_child_process(sk, nsk, skb)) {
                    rsk = nsk;
                    goto reset;
                }
                return 0;
            }
        }
    流程的走向,而这个流程正式儿子套接口创建的关键路径。
    六、例子
    我们可以看到一个系统中可以在同一个端口上有很多套接口,这也就是说:当server创建一个socket的时候,服务器端的端口号和服务器的地址和原始的侦听地址相同,这些连接将会根据客户端的IP和端口进行区分。回头看看这个模式,和伪终端的实现有些相似,也是每次通过一个系统掉用来生成多个不同的实例,只是伪终端是通过对ptmx设备文件的打开实现,而此处由网络协议栈实现
    [tsecer@Harry ~]$ netstat -anp | sort | grep -e "<23>"
    (Not all processes could be identified, non-owned process info
     will not be shown, you would have to be root to see it all.)
    tcp        0      0 127.0.0.1:23                127.0.0.1:42974             ESTABLISHED -                   
    tcp        0      0 127.0.0.1:23                127.0.0.1:42976             ESTABLISHED -                   
    tcp        0      0 127.0.0.1:23                127.0.0.1:60355             ESTABLISHED -                   
    tcp        0      0 127.0.0.1:42974             127.0.0.1:23                ESTABLISHED -                   
    tcp        0      0 127.0.0.1:42976             127.0.0.1:23                ESTABLISHED 14768/telnet        
    tcp        0      0 127.0.0.1:60355             127.0.0.1:23                ESTABLISHED 14860/telnet        
    tcp        0      0 192.168.203.155:23          192.168.203.155:48224       ESTABLISHED -                   
    tcp        0      0 192.168.203.155:48224       192.168.203.155:23          ESTABLISHED 14681/telnet        
    tcp        0      0 :::23                       :::*                        LISTEN      -                   


    七、遗留问题
    如果客户端在发送了SYNC并接收到服务器的SYN+ACK之后不再给予服务器任何回应,此时服务器将会有什么行为。
    syn-flood攻击的原理以及服务器的syn-cookie原理。

  • 相关阅读:
    读书笔记——读《构建之法:现代软件工程》第13~17章
    读《构建之法》十一,十二章有感
    男神女神配 社区交友网 —— 之 主页 详细解说
    典型用户和场景
    最终版本
    用户调研
    第9-11组总体评价
    第二次sprint
    读《构建之法》8-10章
    第一个Sprint
  • 原文地址:https://www.cnblogs.com/tsecer/p/10485954.html
Copyright © 2011-2022 走看看