NetFilterを利用したパケットフィルタリングプログラミング

はじめに

iptablesについて調べていると,どうやらNetFilterの仕組みを自分でも利用できるということを知りました.
実際にそれを用いてパケットフィルタリングのプログラムを作成します.

作成プログラム

8.8.8.8に対するICMPトラフィックをフィルタリングするプログラム

関連知識

LKM (Loadable Kernel Module)

LinuxのNetFilterの仕組みを用いて自分なりの機能を作成するには,NetFilterに関するLKM(Loadable Kernel Module)を新たに作成してカーネルにロードする必要があります.

これにより,ネットワークスタック関連の機能をフック(プログラムのある特定の箇所に独自の機能を追加・挿入)できるため,マシンにパケットが到着したり,出ていったりする時に動作させたいコールバック関数を実装してフィルタリングすることができます.

LKMはカーネルの機能を拡張するための追加モジュールです.

Linuxシステムプログラムに新たに機能を追加・変更しようとすると,カーネルのプログラム全体をリビルドすることになり,効率が悪くなってしまいます.

そこで,変更頻度が少ない基本的なモジュールは元々カーネルに組み込んでおいて,変更頻度の高いモジュールはLKMを用いて導入することにします.LKMでは動的にモジュールを追加できるため,カーネルのプログラム全体をリビルドする必要が無くなります.

LKMをロードすることで,ロードされたモジュールは特権モードで実行され,システムのハードウェアを利用することができます.アンロードも同様に可能なので,必要なくなったら削除することもできます.

LKMについては下記が詳しいです.
www.atmarkit.co.jp

NetFilter

NetFilterはネットワークスタック関連の機能をフックできるLinuxカーネルフレームワークで,主にパケットフィルタリングに利用されます.

Linux関連でのファイアウォールのツールとして一般にiptablesが利用されていますが,それはNetFilterの機能を利用して実装されています.

NetFilterと,フック可能なタイミングについては下記が詳しいです.
qiita.com
wiki.bit-hive.com

実装

プログラムがカーネルに組み込まれるため,あんまり変なフィルタリングの設定をしてしまうと普段使いの際に悪影響を及ぼします.
また,できる限り自前で持っているプログラムを用いて動作確認できる方が望ましいと考えました.
そのため,自システムから8.8.8.8に対して送信されるICMPトラフィックをフィルタリングするようにして,pingコマンドを用いてそれを確認します.

環境

カーネルのバージョン

5.4.0-47-generic

OSのバージョン

Ubuntu 20.04 LTS

ソースコード

#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>

MODULE_LICENSE("GPL");

//登録するコールバック関数の定義
//sk_buff型の変数skbが引数として渡される。
static unsigned int handle_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
        // skbからネットワークヘッダを取得
        struct iphdr *iph = (struct iphdr *)skb_network_header(skb);

        // パケットの宛先IPアドレスが8.8.8.8であり、IPヘッダのプロコトル情報が0x01(ICMP)の場合
        if((be32_to_cpu(iph->daddr) & 0xffffffff) == 0x08080808 && iph->protocol == 0x01){
                return NF_DROP //パケットをDROP(破棄);
        }

        return NF_ACCEPT; //パケットをACCEPT(送信)
}

// 登録するフックのルール
static struct nf_hook_ops hook_ops = {
        .hook   = handle_hook, //コールバック関数
        .pf     = PF_INET, //IPV4
        .hooknum = NF_INET_LOCAL_OUT, //フックのタイミングはローカスシステムから外部にパケットが送信されるとき
        .priority = NF_IP_PRI_FILTER, //フィルタのフック処理
};

//モジュールのロード時の処理
//モジュールのロード時にnf_register_net_hook関数でフックを登録
int init_module(){
        int err;
        err = nf_register_net_hook(&init_net, &hook_ops); //フックを登録

        if(err < 0){
                return err;
        }

        return 0;
}

//モジュールのアンロード時の処理
void cleanup_module(){
        nf_unregister_net_hook(&init_net,&hook_ops); //フックの登録を解除
}
        

実験

pingコマンドを用いて作成したLKMがきちんと動作しているか確認することにします.
mymodule.cというファイル名でコードを作成したことを想定します.

コンパイル方法

1. LKMのためのMakefileの作成

obj-m := mymodule.o

LKMを作成するための最小限の内容にしています。
2. makeによるコンパイル

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

コンパイルに成功すると、mymodule.koファイルが作成されます。
3. モジュールをロード

 $ sudo insmod mymodule.ko

4. モジュールがロードされたか確認

$ lsmod
Module                  Size  Used by
mymodule               16384  0
:
:
:

5. モジュールをアンロードする場合

$ sudo rmmod mymodule

pingコマンドによる動作確認

$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) バイトのデータ
ping: sendmsg: 許可されていない操作です
ping: sendmsg: 許可されていない操作です
ping: sendmsg: 許可されていない操作です
$ ping 192.168.10.1
PING 192.168.10.1 (192.168.10.1) 56(84) バイトのデータ
64 バイト応答 送信元 192.168.10.1: icmp_seq=1 ttl=63 時間=21.2ミリ秒
64 バイト応答 送信元 192.168.10.1: icmp_seq=2 ttl=63 時間=10.8ミリ秒
64 バイト応答 送信元 192.168.10.1: icmp_seq=3 ttl=63 時間=9.37ミリ秒

8.8.8.8に対してのICMPトラフィックは送信されず、その他のIPアドレスに対してのものは送信していることがわかります。

まとめ

NetFilterを用いたパケットフィルタリングプログラムの実装方法を調べました。

NetFilterの機能を利用するには、NetFilterに関するLKMを作成してフック処理を登録する必要があります。

実験用のプログラムとして、8.8.8.8に対するICMPトラフィックをフィルタリングするプログラムを作成し、pingコマンドによる簡単な実験を行いました。