NPF(NetBSD Packet Filter)の拡張機能の実装をソースコードから読み解いてみる

[NetBSD Advent Calendar 2022](https://qiita.com/advent-calendar/2022/netbsd) 20日目の記事です。今日はNPF(NetBSD Packet Filter)の拡張機能を作成する手順を、ソースコードから調べてみようと思います。

## NPF拡張機能のソースコード

[18日目の記事](https://qiita.com/furandon_pig/items/c41e59209938534e377f)で紹介した、NPFのrndblock拡張機能の実装を例にソースコードを見てみます。

ソースツリーに対し、"npf"と"rndblock"という文字列を含むファイル名で検索をかけてみます。どうやら `npfext_rndblock.c` と `npf_ext_rndblock.c` という2つのC言語ソースファイルが存在しているようです。

```sh
$ cd /usr/src
find . -type f | grep npf | grep rndblock | grep -v CVS
./lib/npf/ext_rndblock/Makefile
./lib/npf/ext_rndblock/npfext_rndblock.c
./lib/npf/ext_rndblock/shlib_version
./sys/modules/npf_ext_rndblock/Makefile
./sys/net/npf/npf_ext_rndblock.c
```

それぞれのファイルの役割はどうなっているのでしょうか？ `Makefile` があるので実際にビルドしてみます。

まずは `/usr/src/lib/npf/ext_rndblock/` で `make` を実行してみます。 `/usr/src/lib/libnpf/` の内容(オブジェクトファイル)を参照するため、あらかじめ `libnpf` ディレクトリで `make` を実行しておきます。

```sh
# cd /usr/src/lib/libnpf/
# make
...
# cd /usr/src/lib/npf/ext_rndblock/
# ls
CVS/               Makefile           npfext_rndblock.c  shlib_versio
# make
...
# ls -l
total 42
drwxrwxr-x  2 root  wsrc    512 Aug  5 01:10 CVS/
-rw-rw-r--  1 root  wsrc    112 Mar 11  2013 Makefile
lrwxr-xr-x  1 root  wsrc     19 Dec 21 13:16 ext_rndblock.so@ -> ext_rndblock.so.0.0
lrwxr-xr-x  1 root  wsrc     19 Dec 21 13:16 ext_rndblock.so.0@ -> ext_rndblock.so.0.0
-rwxr-xr-x  1 root  wsrc   8296 Dec 21 13:16 ext_rndblock.so.0.0*
-rw-r--r--  1 root  wsrc  12916 Dec 21 13:16 ext_rndblock.so.0.map
-rw-r--r--  1 root  wsrc   3792 Dec 21 13:16 ext_rndblock_pic.a
-rw-rw-r--  1 root  wsrc   2959 Sep 29  2018 npfext_rndblock.c
-rw-r--r--  1 root  wsrc   3496 Dec 21 13:16 npfext_rndblock.pico
-rw-rw-r--  1 root  wsrc     80 Dec 10  2012 shlib_version
```

`make` が完了すると、 `ext_rndblock.so.0.0` という共有ライブラリが作成されています。

もう一方の `/usr/src/sys/modules/npf_ext_rndblock/` でも `make` してみます。

```sh
# cd /usr/src/sys/modules/npf_ext_rndblock/
# ls
CVS/      Makefile
# make
#   compile  npf_ext_rndblock/npf_ext_rndblock.o
/usr/src/tooldir.NetBSD-9.3-amd64/bin/x86_64--netbsd-gcc -O2 -g   -std=gnu99    -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare  -Wsystem-headers   -Wno-traditional   -Wa,--fatal-warnings  -Wreturn-type -Wswitch -Wshadow -Wcast-qual -Wwrite-strings -Wextra -Wno-unused-parameter -Wno-sign-compare -Wold-style-definition -Wsign-compare -Wformat=2  -Wno-format-zero-length  -Werror   -ffreestanding  -fno-strict-aliasing -Wno-pointer-sign -mno-red-zone -mno-mmx -mno-sse -mno-avx -msoft-float -mcmodel=kernel -fno-omit-frame-pointer   -I/usr/src/common/include --sysroot=/ -I/usr/src/sys/external/bsd/libnv/dist -I/usr/src/common/include  -nostdinc -I. -I/usr/src/sys/modules/npf_ext_rndblock -isystem /usr/src/sys -isystem /usr/src/sys/arch -isystem /usr/src/sys/../common/include -D_KERNEL -D_LKM -D_MODULE -DSYSCTL_INCLUDE_DESCR -c    /usr/src/sys/net/npf/npf_ext_rndblock.c
/usr/src/tooldir.NetBSD-9.3-amd64/bin/nbctfconvert -L VERSION npf_ext_rndblock.o
#      link  npf_ext_rndblock/npf_ext_rndblock.kmod
/usr/src/tooldir.NetBSD-9.3-amd64/bin/x86_64--netbsd-gcc  --sysroot=/ -Wl,--warn-shared-textrel -Wl,-z,relro -nostdlib -r -Wl,-T,/usr/src/sys/../sys/modules/xldscripts/kmodule,-d  -Wl,-Map=npf_ext_rndblock.kmod.map  -o npf_ext_rndblock.kmod npf_ext_rndblock.o
/usr/src/tooldir.NetBSD-9.3-amd64/bin/nbctfmerge -t -L VERSION -o npf_ext_rndblock.kmod npf_ext_rndblock.o
#
# ls -l
total 70
drwxrwxr-x  2 root  wsrc    512 Aug  5 01:10 CVS/
-rw-rw-r--  1 root  wsrc    245 Sep 29  2018 Makefile
lrwxr-xr-x  1 root  wsrc     31 Dec 21 13:19 amd64@ -> /usr/src/sys/arch/amd64/include
lrwxr-xr-x  1 root  wsrc     30 Dec 21 13:19 i386@ -> /usr/src/sys/arch/i386/include
lrwxr-xr-x  1 root  wsrc     31 Dec 21 13:19 machine@ -> /usr/src/sys/arch/amd64/include
-rw-r--r--  1 root  wsrc  21448 Dec 21 13:19 npf_ext_rndblock.kmod
-rw-r--r--  1 root  wsrc   1919 Dec 21 13:19 npf_ext_rndblock.kmod.map
-rw-r--r--  1 root  wsrc  41768 Dec 21 13:19 npf_ext_rndblock.o
lrwxr-xr-x  1 root  wsrc     29 Dec 21 13:19 x86@ -> /usr/src/sys/arch/x86/include
```

こちらでは `npf_ext_rndblock.kmod` が生成されます。パケットフィルタなので、カーネル内で動作するはずであり、 `npf_ext_rndblock.kmod` というカーネルモジュールが必要なのは理解できます。しかし、もう一方の `ext_rndblock.so.0.0` は何のために利用されるのでしょうか？

実際に[npfext_rndblock.c](https://github.com/NetBSD/src/blob/trunk/sys/net/npf/npf_ext_rndblock.c)のソースコードを見てみましょう。

```c
/usr/src/lib/npf/ext_rndblock/npfext_rndblock.c
 58 int
 59 npfext_rndblock_param(nl_ext_t *ext, const char *param, const char *val)
 60 {
 61     enum ptype { PARAM_U32 };
 62     static const struct param {
 63         const char *    name;
 64         enum ptype  type;
 65         signed long min;
 66         signed long max;
 67     } params[] = {
 68         { "mod",    PARAM_U32,  1,  LONG_MAX    },
 69         { "percentage", PARAM_U32,  1,  9999        },
 70     };
 71
 72     if (val == NULL) {
 73         return EINVAL;
 74     }
 75     for (unsigned i = 0; i < __arraycount(params); i++) {
 76         const char *name = params[i].name;
 77         long ival;
 78
 79         if (strcmp(name, param) != 0) {
 80             continue;
 81         }
 82
 83         /*
 84          * Note: multiply by 100 and convert floating point to
 85          * an integer, as 100% is based on 10000 in the kernel.
 86          */
 87         ival = (i == 1) ? atof(val) * 100 : atol(val);
 88         if (ival < params[i].min || ival > params[i].max) {
 89             return EINVAL;
 90         }
 91         assert(params[i].type == PARAM_U32);
 92         npf_ext_param_u32(ext, name, ival);
 93         return 0;
 94     }
 95
 96     /* Invalid parameter, if not found. */
 97     return EINVAL;
 98 }
```

"mod" や "percentage" といったパラメータらしきものがあります。 `rndblock` のフィルタ設定は以下のように行っており、設定ファイルに指定したパラメータ値をこの `npfext_rndblock_param()` で取得しています。つまり、 `ext_rndblock.so.0.0` は指定されたパラメータ値をユーザランド側で取得するという役割があるようです。

```
procedure "rndblock" {
  rndblock: percentage 50.0
}
```

さらにコードを見てみると、 `npfext_rndblock_construct()` という関数があります。最終的に `npf_ext_construct("rndblock")` を呼び出す形になっています。

```c
 51 nl_ext_t *
 52 npfext_rndblock_construct(const char *name)
 53 {
 54     assert(strcmp(name, "rndblock") == 0);
 55     return npf_ext_construct(name);
 56 }
```

`npf_ext_construct()` は[/usr/src/lib/libnpf/npf.c](https://github.com/NetBSD/src/blob/trunk/lib/libnpf/npf.c)で以下のように定義されています。 `nvlist_add_string()` がポイントなのでしょうが、このコードだけだとどういった処理が走るのか分からないですね…。

```c
 625 /*
 626  * NPF EXTENSION INTERFACE.
 627  */
 628
 629 nl_ext_t *
 630 npf_ext_construct(const char *name)
 631 {
 632     nl_ext_t *ext;
 633
 634     ext = malloc(sizeof(*ext));
 635     if (!ext) {
 636         return NULL;
 637     }
 638     ext->ext_dict = nvlist_create(0);
 639     nvlist_add_string(ext->ext_dict, "name", name);
 640     return ext;
 641 }
```

視点を変えて、拡張機能の機能名(この場合は"rndblock")から調べてみましょう。 `rndblock` カーネルモジュールの実装である[/usr/src/sys/net/npf/npf_ext_rndblock.c](https://github.com/NetBSD/src/blob/trunk/sys/net/npf/npf_ext_rndblock.c)を見ると、 `npf_ext_rndblock.kmod` が `modload` コマンドでロードされた際に、 `npf_ext_rndblock_init()` 関数が呼ばれています。

```c
137 __dso_public int
138 npf_ext_rndblock_init(npf_t *npf)
139 {
140     static const npf_ext_ops_t npf_rndblock_ops = {
141         .version    = NPFEXT_RNDBLOCK_VER,
142         .ctx        = NULL,
143         .ctor       = npf_ext_rndblock_ctor,
144         .dtor       = npf_ext_rndblock_dtor,
145         .proc       = npf_ext_rndblock
146     };
147
148     /*
149      * Initialize the NPF extension.  Register the "rndblock" extension
150      * calls (constructor, destructor, the processing routine, etc).
151      */
152     npf_ext_rndblock_id = npf_ext_register(npf, "rndblock",
153         &npf_rndblock_ops);
154     return npf_ext_rndblock_id ? 0 : EEXIST;
155 }
...
171 static int
172 npf_ext_rndblock_modcmd(modcmd_t cmd, void *arg)
173 {
174     npf_t *npf = npf_getkernctx();
175
176     switch (cmd) {
177     case MODULE_CMD_INIT:
178         return npf_ext_rndblock_init(npf);
179     case MODULE_CMD_FINI:
180         return npf_ext_rndblock_fini(npf);
181     case MODULE_CMD_AUTOUNLOAD:
182         /* Allow auto-unload only if NPF permits it. */
183         return npf_autounload_p() ? 0 : EBUSY;
184     default:
185         return ENOTTY;
186     }
187     return 0;
188 }
```

`static const npf_ext_ops_t npf_rndblock_ops` で各処理の実体となる関数ポインタをセットしています。 `npf_ext_ops_t.proc` に設定されている `npf_ext_rndblock()` が `rndblock` 機能のメインとなる処理になっています。それ以外の `.ctor` や `.dtor` はどんな役割を持っているのでしょうか？

`.ctor` に設定されている、 `npf_ext_rndblock_ctor()` を見てみます。どうやら"ctor"は"constructor"の略語のようです。ただ、この関数はカーネルモジュール内から直接呼び出している個所がありません…。

```c
 65 /*
 66  * npf_ext_rndblock_ctor: a constructor to parse and store any parameters
 67  * associated with a rule procedure, which is being newly created.
 68  */
 69 static int
 70 npf_ext_rndblock_ctor(npf_rproc_t *rp, const nvlist_t *params)
 71 {
 72     npf_ext_rndblock_t *meta;
 73
 74     /*
 75      * Allocate and a associate a structure for the parameter
 76      * and our meta-data.
 77      */
 78     meta = kmem_zalloc(sizeof(npf_ext_rndblock_t), KM_SLEEP);
 79     meta->mod = dnvlist_get_number(params, "mod", 0);
 80     meta->percentage = dnvlist_get_number(params, "percentage", 0);
 81     npf_rproc_assign(rp, meta);
 82
 83     return 0;
 84 }
```

`npf_ext_rndblock_init()` のコードをよく見ると、以下のようなコメントがあります。これによると、 `rndblock` 拡張機能が「登録」された時にコンストラクタ処理が走る、と書かれています。つまり、カーネルモジュールをロードした時ではなく、パケットフィルタのルール設定時に `rndblock` を使う、としたときにコンストラクタが呼ばれ、フィルタのルール設定のパースは `ext_rndblock.so.0.0` 内で行われるため、 `npf_ext_rndblock_ctor()` はユーザランド側から呼び出されるという挙動になっています。

```c
137 __dso_public int
138 npf_ext_rndblock_init(npf_t *npf)
139 {
...
148     /*
149      * Initialize the NPF extension.  Register the "rndblock" extension
150      * calls (constructor, destructor, the processing routine, etc).
151      */
152     npf_ext_rndblock_id = npf_ext_register(npf, "rndblock",
153         &npf_rndblock_ops);
154     return npf_ext_rndblock_id ? 0 : EEXIST;
155 }
```

そう考えると、ユーザランド側で `npf_ext_construct("rndblock")` となる呼び出しになるというのも納得ができます。
(カーネルモジュールのロード後に、ユーザランドからのトリガーでコンストラクタを呼び出す、というのは一瞬「？」となる処理フローではありますが…)

## まとめ

NPFの `rndblock` のソースコードを例にして、NPF拡張機能の実装を読み解いてみました。必要最低限の処理フローは見えてきたので、このノウハウをもとに簡単な拡張機能を作れそうです。