LoginSignup
4
3

More than 5 years have passed since last update.

Linux kernel v2.6.20 network stackを読んでみる - UDP受信編

Last updated at Posted at 2018-08-19

はじめに

Linux kernel v2.6.20 network stackを読んでみる - IP受信編 の続きです。
今度はUDP層の処理です。

UDP層

UDPヘッダのフォーマット
39CDD778-AE4A-4276-87DC-8B5FEDBEF6A0.jpeg

UDPヘッダ構造体の定義は以下です。
https://elixir.bootlin.com/linux/v2.6.20/source/include/linux/udp.h#L22

struct udphdr {
    __be16  source;
    __be16  dest;
    __be16  len;
    __sum16 check;
};

UDP層のcallgraph

D748F45C-CF22-4ADC-BBD7-32FF2628BA5D.jpeg

IP層の説明であったように、受信時のUDP層のエントリーポイントは udp_rcv() となります。
UDP自体は特有の処理があまりないシンプルなものなので、ユーザが待ち受けている socket を見つけてデータを紐づけ、ユーザ側を起動させることが仕事になります。

udp_rcv()

//#define UDP_HTABLE_SIZE       128
struct hlist_head udp_hash[UDP_HTABLE_SIZE];

__inline__ int udp_rcv(struct sk_buff *skb)
{
    return __udp4_lib_rcv(skb, udp_hash, 0);
}

/*
 *  All we need to do is get the socket, and then do a checksum. 
 */
int __udp4_lib_rcv(struct sk_buff *skb, struct hlist_head udptable[], int is_udplite)
{
    struct sock *sk;
    struct udphdr *uh = skb->h.uh;
    unsigned short ulen;
    struct rtable *rt = (struct rtable*)skb->dst;
    __be32 saddr = skb->nh.iph->saddr;
    __be32 daddr = skb->nh.iph->daddr;

    // UDPヘッダに書かれているlengthと実際のパケットデータ大きさに矛盾がないか調べます。
    (省略)

    // 受信したパケットと対応するsocketのインスタンスを探す.socketはユーザとやり取りするインターフェース.
    sk = __udp4_lib_lookup(saddr, uh->source, daddr, uh->dest, skb->dev->ifindex, udptable);

    if (sk != NULL) {
        // socketとsk_buffを紐づけ、ユーザがsocketを通じデータにアクセスできるようにします。この中でユーザコンテキストの起動も行います.
        int ret = udp_queue_rcv_skb(sk, skb);
        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;
    }

    // ユーザがlistenしていないポートへの通信なので本パケットを破棄します。
    kfree_skb(skb);
    return(0);
}

__udp4_lib_lookup()

kernel内部で使用している、受信したパケットを管理する sk_buff と、ユーザが使用するsocketはデータ構造が異なります。

socketのデータ型は以下を参照のこと。
struct sock

ここでは受信したUDPパケットを待ち受けているsocketのインスタンスを探すことが関数の目的となります。

net/ipv4/udp.c
/* UDP is nearly always wildcards out the wazoo, it makes no sense to try
 * harder than this. -DaveM
 */
static struct sock *__udp4_lib_lookup(__be32 saddr, __be16 sport,
                      __be32 daddr, __be16 dport,
                      int dif, struct hlist_head udptable[])
{
    struct sock *sk, *result = NULL;
    struct hlist_node *node;
    unsigned short hnum = ntohs(dport); // Destination Port
    int badness = -1;

    read_lock(&udp_hash_lock);
    // Destination Portの下位7bitが等しいものを探す.
    sk_for_each(sk, node, &udptable[hnum & (UDP_HTABLE_SIZE - 1)]) {
        struct inet_sock *inet = inet_sk(sk);

        // 探してきたsocketはまだ今受信しているパケットと同じ種類のものか不明なので、一致するものを探す。
        // 通信の起点はクライアント側なので、socketのsourceとdestinationと受信したパケットのそれは逆に対応している。
        // つまりsocketのsourceとパケットのdestinationが対応するということに注意。
        // 探すものは Source/Destinationのアドレスとポート。
        if (inet->num == hnum && !ipv6_only_sock(sk)) {
            int score = (sk->sk_family == PF_INET ? 1 : 0);
            if (inet->rcv_saddr) {
                if (inet->rcv_saddr != daddr)
                    continue;
                score+=2;
            }
            if (inet->daddr) {
                if (inet->daddr != saddr)
                    continue;
                score+=2;
            }
            if (inet->dport) {
                if (inet->dport != sport)
                    continue;
                score+=2;
            }
            if (sk->sk_bound_dev_if) {
                if (sk->sk_bound_dev_if != dif)
                    continue;
                score+=2;
            }
            if(score == 9) {
                // 対応するsocketを見つけたので、そのインスタンスを返す.
                result = sk;
                break;
            } else if(score > badness) {
                result = sk;
                badness = score;
            }
        }
    }
    if (result)
        sock_hold(result);
    read_unlock(&udp_hash_lock);
    return result;
}

udp_queue_rcv_skb

対応するsocketのインスタンスを入手したので処理が進みます。

net/ipv4/udp.c
/* returns:
 *  -1: error
 *   0: success
 *  >0: "udp encap" protocol resubmission
 *
 * Note that in the success and error cases, the skb is assumed to
 * have either been requeued or freed.
 */
int udp_queue_rcv_skb(struct sock * sk, struct sk_buff *skb)
{
    struct udp_sock *up = udp_sk(sk);
    int rc;

    // socketがカプセル化されている場合
    // (参考) https://wa3.i-3-i.info/word12035.html
    if (up->encap_type) {
        省略
    }

    /*
     *  UDP-Lite specific tests, ignored on UDP sockets
     */
    // https://ja.wikipedia.org/wiki/UDP-Lite
    // 主にインターネットで使用されるプロトコルの1つで音声や動画の伝送を目的にしている。
    // 従来のUDPパケットでは1ビットでもデータが損傷すれば破棄されるのに対して、UDP-Liteはそのまま伝送される。
    if ((up->pcflag & UDPLITE_RECV_CC)  &&  UDP_SKB_CB(skb)->partial_cov) {
        省略
    }

    // checksumの確認
    if (sk->sk_filter && skb->ip_summed != CHECKSUM_UNNECESSARY) {
        省略
    }

    // いよいよsocketとsk_buffを結びつけます.
    if ((rc = sock_queue_rcv_skb(sk,skb)) < 0) {
        /* Note that an ENOMEM error is charged twice */
        goto drop;
    }

    return 0;
}

sock_queue_rcv_skb

int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
    int err = 0;
    int skb_len;

    skb->dev = NULL;
    skb_set_owner_r(skb, sk); // skb->sk = sk; とsk_buffにsocketを結びつける.

    /* Cache the SKB length before we tack it onto the receive
     * queue.  Once it is added it no longer belongs to us and
     * may be freed by other threads of control pulling packets
     * from the queue.
     */
    skb_len = skb->len;

    // sockのインスタンスが管理するsk_buffのlistに本パケットのsk_buffのインスタンスをつなぐ。
    skb_queue_tail(&sk->sk_receive_queue, skb);

    if (!sock_flag(sk, SOCK_DEAD)) {
        // socketの準備が整ったのでユーザ側を起動する.
        sk->sk_data_ready(sk, skb_len);
    }
out:
    return err;
}

最後に
sk->sk_data_ready()
ですが、関数ポインタ自体は sock_init_data()の中で
sk->sk_data_ready = sock_def_readable;
とセットされています。

sock_def_readable()

net/core/sock.c
static void sock_def_readable(struct sock *sk, int len)
{
    read_lock(&sk->sk_callback_lock);
    // socketが起こされ、ユーザに制御が行く.
    if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))
        wake_up_interruptible(sk->sk_sleep);
    sk_wake_async(sk,1,POLL_IN);
    read_unlock(&sk->sk_callback_lock);
}

感想

UDPは簡単だろうと思いましたが、複雑ですね。。

次にTCPを読み、IPのデフラグに戻り、今回調査のきっかけとなった脆弱性の解説をしていきたいですが、なかなか骨が折れますね。。linux kernelのnetwork stackは、、

参考

【書籍】Professional Linux Kernel Architecture

RFC3828 The Lightweight User Datagram Protocol (UDP-Lite)

callback function in socket as sk_data_ready()

履歴

2018/08/19 初版

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3