2020年5月23日 星期六

Linux network stack: UDP接收端的工作流程

由於發生UDP掉包,於是想要複習一下UDP的程式。先看一下UDP的封包架構:


Linux Kernel UDP的部份的程式碼不多,所以極為容易了解大致的工作原理。

UDP的接收端大致工作模型分為四部份:

  1. Link Layer (就是IP模組),接收封包之後,如果是本機要處理,就將封包送給UDP/TCP層來處理。
  2. 如果是UDP就呼叫Kernel UDP模組來處理。
  3. UDP模組對封包進行簡單的檢查,如果沒有問題,就把封包放到接收QUEUE當中。等待使用者模式的程式來處理。
  4. User Space程式呼叫read()或Socket等system call,讀取已經放入UDP Receive Queue。接收佇列中的資料。
下面的Trace基本上就是看步驟二是如何工作的? 

udp_rcv() 是整個 UDP 模組的進入點。程式在 ./net/udp.c 當中。

這個Call函式是在AF_INET protocol init 的時候,由UDP註冊給 Network Layer 的Call Back 。

./net/af_inet.c
...

static const struct net_protocol udp_protocol = {
        .handler =      udp_rcv,
        .err_handler =  udp_err,
        .gso_send_check = udp4_ufo_send_check,
        .gso_segment = udp4_ufo_fragment,
        .no_policy =    1,
        .netns_ok =     1,
};

...

網路層程式碼處理完一個輸入的資料封包後,如果該封包是發往本機的,並且其上層協議是UDP,那麼會呼叫這個被註冊的Call Back。

int udp_rcv(struct sk_buff *skb)
{
      return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

//
// All we need to do is get the socket, and then do a checksum.
// __udp4_lib_rcv() 這個程式幾乎就是整個UDP工作的中控,其他程式都是圍繞著這個程式來服務他。
// 先看他的三個輸入:
// *skb : 就是由上層傳下來的封包。
// udp_table: 這個資料結構就是UDP要傳送給應用程式的BUFFER。由於等著要收封包的應用程式,不是一個,所以這是一個很大的Hash Table,UDP模組要分辨出,封包屬於哪一個應用程式,將封包放入這個應用程式的QUEUE。
// proto: 他有兩個L4 Protocol選項: IPPROTO_UDP 或 IPPROTO_UDPLITE

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
                   int proto)
{
        struct sock *sk;
        struct udphdr *uh;
        unsigned short ulen;
        struct rtable *rt = skb_rtable(skb);
        __be32 saddr, daddr;
        struct net *net = dev_net(skb->dev);

        /*
         *  Validate the packet.
         */
        // pskb_may_pull() 這程式很簡單,只是看一下送進來的封包,如果SIZE比UPD Header的SIZE還小,顯然是沒有用的封包,那就沒有處理的必要了。
        // 這是一個inline function,位於 ./include/linux/skbuff.h 之中。
        //
        if (!pskb_may_pull(skb, sizeof(struct udphdr)))
                goto drop;              /* No space for header. */

        // 把封包當中要用的參數拿出來
        uh   = udp_hdr(skb);
        ulen = ntohs(uh->len);
        
        // 取出封包的來源地址與目的地址
        saddr = ip_hdr(skb)->saddr;
        daddr = ip_hdr(skb)->daddr;
        
        // skb中的資料長度不能小於UDP Header指示的資料包長度,如果太小就到短封包處理,如果大於,就是說封包之中是有資料的,需要繼續處理。
        if (ulen > skb->len)
                goto short_packet;

        if (proto == IPPROTO_UDP) {
                /* UDP validates ulen. */
                // 這裡做了兩件事:
                // UDP的資料長度必須大於HEADER。
                // 由於IP模組對於不合理的UDP短封包,會填入一些垃圾資訊,所以 pskb_trim_rcsum()會處理掉垃圾資訊,重新計算CHECK SUM。 
                if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
                        goto short_packet;
                // 由於SIZE有可能改變,所以又重新指定了一次uh變數。
                uh = udp_hdr(skb);
        }
        // 對封包做CHECK SUM檢查 
        if (udp4_csum_init(skb, uh, proto))
                goto csum_error;
        
        // 如果是Multcasting,在這裡就離開,做特殊的處理。 
        if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
                return __udp4_lib_mcast_deliver(net, skb, uh,
                                saddr, daddr, udptable);
        // 如果是一般封包,就要尋找應用程式QUEUE的位置,準備將資料往應用程式傳送
        // 根據封包的 source port 和 object port 查詢 udptable,尋找應該接收該資料包的傳輸控制塊
        sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

        if (sk != NULL) {
                // 如果找到了,呼叫 udp_queue_rcv_skb (),將資料放入Queue當中
                int ret = udp_queue_rcv_skb(sk, skb);
                // Call sock_put, 如果 Queue 已經沒有東西 -> call sk_free() 
                sock_put(sk);

                /* a return value > 0 means to resubmit the input, but
                 * it wants the return to be -protocol, or 0
                 */
                if (ret > 0)
                        return -ret;
                return 0;
        }
        // 接下來,就要處理這些沒有要人要處理的封包。
        // 先做一些 IPSec的規則檢查,做了甚麼,這裡沒有細看。
        if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
                goto drop;
        nf_reset(skb);

        /* No socket. Drop packet silently, if checksum is wrong */
        if (udp_lib_checksum_complete(skb))
                goto csum_error;
        //累計輸入資料包錯誤統計值,
        UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
        
        // 回覆ICMP,這個PORT,封包無法送達
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

        /*
         * Hmm.  We got an UDP packet to a port to which we
         * don't wanna listen.  Ignore it.
         */
        // 與這些無法處理、送達的封包說再見。
        kfree_skb(skb);
        return 0;

short_packet:
        // 短封包,就在LOG當中顯示一下訊息
        LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n",
                       proto == IPPROTO_UDPLITE ? "-Lite" : "",
                       &saddr,
                       ntohs(uh->source),
                       ulen,
                       skb->len,
                       &daddr,
                       ntohs(uh->dest));
        goto drop;

csum_error:
        /*
         * RFC1122: OK.  Discards the bad packet silently (as far as
         * the network is concerned, anyway) as per 4.1.3.4 (MUST).
         */
        LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n",
                       proto == IPPROTO_UDPLITE ? "-Lite" : "",
                       &saddr,
                       ntohs(uh->source),
                       &daddr,
                       ntohs(uh->dest),
                       ulen);
drop:
        // 在 /proc/net/ipv4/snmp 當中,紀錄UDPLITE的InErrors 加一 
        UDP_INC_STATS_BH(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE);
        kfree_skb(skb);
        return 0;
}


以上大致上可以知道,下一步的處理程式,兵分二路,其中 _udp4_lib_mcast_deliver() 處理Multicasting,其他的封包送到 udp_queue_rcv_skb () 來處理,規屬於UDPLITE 。


static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
                                    struct udphdr  *uh,
                                    __be32 saddr, __be32 daddr,
                                    struct udp_table *udptable)
{
        struct sock *sk, *stack[256/sizeof(struct sock *)];
        struct udp_hslot *hslot = udp_hashslot(udptable, net, ntohs(uh->dest));
        int dif;
        unsigned int i, count = 0;

        spin_lock(&hslot->lock);

        sk = sk_nulls_head(&hslot->head);
        dif = skb->dev->ifindex;

        // Multicasting 的封包比較長,所以要一筆一筆處理到結束
        sk = udp_v4_mcast_next(net, sk, uh->dest, daddr, uh->source, saddr, dif);
        while (sk) {
                // 如果有需多封包,就一筆一筆放到STACK當中
                stack[count++] = sk;
                // 放好之後,就讀下一筆來處理
                sk = udp_v4_mcast_next(net, sk_nulls_next(sk), uh->dest,
                                       daddr, uh->source, saddr, dif);

                // 如果STACK滿了封包還沒有結束,必須要離開迴圈。離開之前,還是要把Buffer處理到USER SPACE (Call flush_stack)
                if (unlikely(count == ARRAY_SIZE(stack))) {
                        if (!sk)
                                break;
                        flush_stack(stack, count, skb, ~0);
                        count = 0;
                }
        }

        /*
         * before releasing chain lock, we must take a reference on sockets
         */

        for (i = 0; i < count; i++)
                sock_hold(stack[i]);

        spin_unlock(&hslot->lock);

        /*
         * do the slow work with no lock held
         */
        if (count) {
                // call flush_stack 把PACKET送到 SOCKET 去。
                flush_stack(stack, count, skb, count - 1);
                // 把stack的記憶體Free調 
                for (i = 0; i < count; i++)
                        sock_put(stack[i]);
        } else {
                kfree_skb(skb);
        }
        return 0;
}

// 接下來看 flush_stack(),這就是將Stack中的資料,一筆筆COPY到 SOCKET BUFFER。

static void flush_stack(struct sock **stack, unsigned int count,
                        struct sk_buff *skb, unsigned int final)
{
        unsigned int i;
        struct sk_buff *skb1 = NULL;
        struct sock *sk;

        for (i = 0; i < count; i++) {
                sk = stack[i];
                if (likely(skb1 == NULL))
                        skb1 = (i == final) ? skb : skb_clone(skb, GFP_ATOMIC);
                
                // 上面的skb_clone() 先依照 sk 的內容,建立一個資料結構。
                // 如果 socket buffer 之中的記憶體已經用光,就做不下去了,於是紀錄一個Error。

                if (!skb1) {
                        atomic_inc(&sk->sk_drops);
                        UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,
                                         IS_UDPLITE(sk));
                        UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS,
                                         IS_UDPLITE(sk));
                }
                // 如果可以拿到Buffer,就可以完成最終完成資料傳送的Call
                if (skb1 && udp_queue_rcv_skb(sk, skb1) <= 0)
                        skb1 = NULL;
        }
        if (unlikely(skb1))
                kfree_skb(skb1);
}

大致就這樣,所以 udp_mem 這個 BUFFER的SIZE,的確對於 INERRORS 會有影響。

沒有留言:

張貼留言