monolithic kernel

netfilterでパケットの送受信を制御する

February 23, 2015

    netfilterは、LinuxカーネルのネットワークスタックのいろいろをフックしてあれこれするためのAPIです。身近なところだと、iptablesやnftablesの実装に利用されています。カーネル内のAPIなので敷居が高そうに思われるかもしれませんが、簡単なLoadable Kernel Module (LKM) を書くだけで利用でき、カーネルに直接変更を加える必要がないので、かなりお手軽です。それでいて、iptablesやnftablesでも利用されているだけあってできることの範囲は広く、パケットの監視やファイアウォールを始めとしてパケットを読み書きするような処理なら割と何でもできます。

    この記事では、netfilterを使うカーネルモジュールの基本的な書き方を紹介します。なお、動作確認はKernel 3.14で行いました。カーネルのバージョンによってメンバや定数の名称が変化している場合があるようなので、コンパイルが通らない場合には適当に頑張ってみてください。

    コード例

    以下がnetfilterを使ったLKMの例です。モジュールロード時にnf_register_hook関数でフックを登録することで、指定したタイミングでフックハンドラが呼ばれるようになります。ハンドラには対象パケットのsk_buffが引数として渡されるので、これを煮るなり焼くなり好きにします。ハンドラの戻り値で、パケットをそのまま通すのか、あるいはドロップさせるかなどその後の扱いを指定できます。

    mymodule.c

    ここでは、一例として特定の相手に対して送信しようとしたSYNパケットをランダムでドロップさせています。ロードするとパフォーマンスがとても悪くなります。

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/ip.h>
    #include <linux/inet.h>
    #include <linux/netdevice.h>
    #include <linux/netfilter_ipv4.h>
    #include <linux/skbuff.h>
    #include <linux/tcp.h>
    #include <linux/random.h>
    #include <net/ip.h>
    #include <net/tcp.h>
    
    MODULE_LICENSE("GPL");
    
    static unsigned handle_hook(const struct nf_hook_ops *ops,
                    struct sk_buff *skb,
                    const struct net_device *in,
                    const struct net_device *out,
                    int (*okfn)(struct sk_buff*))
    {
        struct iphdr *iph = ip_hdr(skb);
        struct tcphdr *tcph;
        char tmp;
    
        // IPでないパケットは通過させる
        if (!iph) {
            return NF_ACCEPT;
        }
    
        // IPv4でないパケットは通過させる
        if (iph->version != 4) {
            return NF_ACCEPT;
        }
    
        // TCPでないパケットは通過させる
        if (iph->protocol != IPPROTO_TCP) { 
            return NF_ACCEPT;
        }
    
        // IPアドレスが10.0.x.xでないパケットは通過させる
        if ((be32_to_cpu(iph->saddr) & 0xffff0000) != 0x0a000000) {
            return NF_ACCEPT;
        }
        if ((be32_to_cpu(iph->daddr) & 0xffff0000) != 0x0a000000) {
            return NF_ACCEPT;
        }
    
        tcph = tcp_hdr(skb);
    
        // 宛先ポートが10000でないパケットは通過させる
        if (be16_to_cpu(tcph->dest) != 10000) {
            return NF_ACCEPT;
        }
    
        // SYNフラグの立っているパケットの場合
        if (tcph->syn) {
            // 50%の確率でドロップさせる
            get_random_bytes(&tmp, sizeof(tmp));
            if (tmp & 1) {
                return NF_DROP;
            }
        }
    
        return NF_ACCEPT;
    }
    
    static struct nf_hook_ops hook_ops = {
        .hook     = handle_hook,
        .pf       = PF_INET,
        .hooknum  = NF_INET_LOCAL_OUT, // パケット送信をフック
        .priority = NF_IP_PRI_FILTER,
    };
    
    int init_module() {
        int err;
    
        err = nf_register_hook(&hook_ops);
        if (err < 0) {
            return err;
        }
        return 0;
    }
    
    void cleanup_module() {
        nf_unregister_hook(&hook_ops);
    }

    Makefile

    Makefileは普通にLKMを作る時と同様の内容でOKです。

    obj-m := mymodule.c

    以下のコマンドでmakeとLKMのロードが可能です。

    make -C <カーネルソースへの絶対パス> M=<カレントディレクトリへの絶対パス> modules
    sudo insmod mymodule.ko