NetBSD Advent Calendar 2022 23日目の記事です。今日はNPF(NetBSD Packet Filter)の拡張機能において、フィルタルールで設定した値がカーネルに渡されるまでを追いかけてみようと思います。
NPFの拡張機能における設定値
18日目の記事の rndblock
で紹介したように、NPFではフィルタルール側で拡張機能にパラメータを渡すことができます。以下の例では rndblock
のパラメータとして、 50.0
を設定しています。
procedure "rndblock" {
rndblock: percentage 50.0
}
拡張機能の実体(実際の処理)はカーネル空間になるため、ユーザランド側で設定したフィルタールール内の値を何らかの方法でカーネル側に渡す必要があります。これは21日目の記事で紹介した、 libnv
の機能を利用して実現されています。
フィルタルールの値がカーネルに渡されるまでを追いかけてみる
ユーザランド側で npfctl reload
を実行すると、その時点のフィルタルールがロードされ、拡張機能の定義が行われた場合は対応するパラメータがパースされます。先述した rndblock: percentage 50.0
はsrc/lib/npf/ext_rndblock/npfext_rndblock.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 }
具体的には、以下の個所で拡張機能のパラメータ値が設定されます。
94 npf_ext_param_u32(ext, name, ival);
npf_ext_param_u32()
は/usr/src/lib/libnpf/npf.cで定義されており、21日目の記事で紹介した libnv
の関数が呼ばれています。
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()
で処理されます。
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()
が発行され、ユーザランドからカーネル側に値が渡されるように見えます。
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で定義されています。この関数はデータの送受信の両方を受け持っており、引数の const nvlist_t *nvl
nvlist_t **nvlp
のそれぞれの値により、データの受信・送信を行います。
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()
でひとかたまりのデータとして受信する、というフローになっているようです。
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
から値を取得するだけです。
/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()
経由でカーネル側に送信、デシリアライズして値を取得という処理フローになっていることが分かりました。