NPF(NetBSD Packet Filter)で拡張機能のパラメータ値がカーネル側に渡されるまでを追いかけてみる

[NetBSD Advent Calendar 2022](https://qiita.com/advent-calendar/2022/netbsd) 23日目の記事です。今日はNPF(NetBSD Packet Filter)の拡張機能において、フィルタルールで設定した値がカーネルに渡されるまでを追いかけてみようと思います。

## NPFの拡張機能における設定値

[18日目の記事](https://qiita.com/furandon_pig/items/e37c4b7ff08d8b77fec6)の `rndblock` で紹介したように、NPFではフィルタルール側で拡張機能にパラメータを渡すことができます。以下の例では `rndblock` のパラメータとして、 `50.0` を設定しています。

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

拡張機能の実体(実際の処理)はカーネル空間になるため、ユーザランド側で設定したフィルタールール内の値を何らかの方法でカーネル側に渡す必要があります。これは[21日目の記事](https://qiita.com/furandon_pig/items/66d8c2c5118d97c9f999)で紹介した、 `libnv` の機能を利用して実現されています。

## フィルタルールの値がカーネルに渡されるまでを追いかけてみる

ユーザランド側で `npfctl reload` を実行すると、その時点のフィルタルールがロードされ、拡張機能の定義が行われた場合は対応するパラメータがパースされます。先述した `rndblock: percentage 50.0` は[src/lib/npf/ext_rndblock/npfext_rndblock.c](https://github.com/NetBSD/src/blob/trunk/lib/npf/ext_rndblock/npfext_rndblock.c)内の以下の関数でパースされます。

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

具体的には、以下の個所で拡張機能のパラメータ値が設定されます。

```c
 94         npf_ext_param_u32(ext, name, ival);
```

`npf_ext_param_u32()` は[/usr/src/lib/libnpf/npf.c](https://github.com/NetBSD/src/blob/trunk/lib/libnpf/npf.c)で定義されており、[21日目の記事](https://qiita.com/furandon_pig/items/66d8c2c5118d97c9f999)で紹介した `libnv` の関数が呼ばれています。

```c
 643 void
 644 npf_ext_param_u32(nl_ext_t *ext, const char *key, uint32_t val)
 645 {
 646     nvlist_add_number(ext->ext_dict, key, val);
 647 }
```

設定された値は、 `npf.c` の `npf_config_submit()` (これは `npfctl reload` の処理から呼ばれる)内からさらに呼ばれる `_npf_xfer_fd()` で処理されます。

```c
 338 int
 339 npf_config_submit(nl_config_t *ncf, int fd, npf_error_t *errinfo)
 340 {
 ...
 347     error = _npf_xfer_fd(fd, IOC_NPF_LOAD, ncf->ncf_dict, &resp);
 348     if (error) {
 349         return error;
 350     }
 ...
 354 }
```

`_npf_xfer_fd()` の中では、 `nvlist_xfer_ioctl()` が呼ばれます。関数名からすると、この中で `ioctl()` が発行され、ユーザランドからカーネル側に値が渡されるように見えます。

```c
 239 static int
 240 _npf_xfer_fd(int fd, unsigned long cmd, nvlist_t *req, nvlist_t **resp)
 241 {
 ...
 272 #if !defined(_NPF_STANDALONE)
 273     case S_IFBLK:
 274     case S_IFCHR:
 275         if (ioctl(fd, IOC_NPF_VERSION, &kernver) == -1) {
 276             goto err;
 277         }
 278         if (kernver != NPF_VERSION) {
 279             errno = EPROGMISMATCH;
 280             goto err;
 281         }
 282         if (nvlist_xfer_ioctl(fd, cmd, req, resp) == -1) {
 283             goto err;
 284         }
 285         break;
 286 #else
```

`nvlist_xfer_ioctl()` は[/usr/src/sys/external/bsd/libnv/dist/nv_kern_netbsd.c](https://github.com/NetBSD/src/blob/05082e19134c05f2f4b6eca73223cdc6b5ab09bf/sys/external/bsd/libnv/dist/nv_kern_netbsd.c)で定義されています。この関数はデータの送受信の両方を受け持っており、引数の `const nvlist_t *nvl` `nvlist_t **nvlp` のそれぞれの値により、データの受信・送信を行います。

```c
143 int
144 nvlist_xfer_ioctl(int fd, unsigned long cmd, const nvlist_t *nvl,
145     nvlist_t **nvlp)
146 {
147     nvlist_ref_t nref;
...
152     if (nvl) {
153         /*
154          * Sending: serialize the name-value list.
155          */
...
174     if (nvlp) {
175         nvlist_t *retnvl;
176
177         /*
178          * Receiving: unserialize the nvlist.
179          *
180          * Note: pages are mapped by nvlist_kern_copyout() for us.
181          */
```

今回はデータを受信する場合の処理フローを追いかけてみます。 `ioctl()` 経由で受信したデータは `nvlist_ref_t` 型の変数に格納されます。コメントを見ると"Receiving: unserialize the nvlist."とあるので、 `libnv` で複数のname/valueリストをシリアライズしておいて、 `ioctl()` でひとかたまりのデータとして受信する、というフローになっているようです。

```c
165     /*
166      * Exchange the nvlist reference data.
167      */
168     if (ioctl(fd, cmd, &nref) == -1) {
169         free(buf);
170         return -1;
171     }
172     free(buf);
173
174     if (nvlp) {
175         nvlist_t *retnvl;
176
177         /*
178          * Receiving: unserialize the nvlist.
179          *
180          * Note: pages are mapped by nvlist_kern_copyout() for us.
181          */
182         if (nref.buf == NULL || nref.len == 0) {
183             errno = EIO;
184             return -1;
185         }
186         retnvl = nvlist_unpack(nref.buf, nref.len, nref.flags);
187         munmap(nref.buf, nref.len);
188         if (retnvl == NULL) {
189             errno = EIO;
190             return -1;
191         }
192         *nvlp = retnvl;
193     }
194     return 0;
195 }
```

ここまででユーザランドからカーネル側に値が渡されてきたので、あとはカーネルモジュール側の実装で `dlvlist_t` から値を取得するだけです。

```c
/usr/src/sys/net/npf/npf_ext_rndblock.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の `rndblock` において、フィルタルールで設定した値がカーネル内の拡張機能側で取得されるまでの流れを追いかけてみました。 `libnv` でname/valueのリストを作成し、データをシリアライズしてひと固まりにしたのち、 `ioctl()` 経由でカーネル側に送信、デシリアライズして値を取得という処理フローになっていることが分かりました。