‹ hrntknr's blog

nftablesのあれこれ

Oct 10, 2025

TL;DR

  • nftables はルールを命令として変換し、専用 VM 内でパケット評価を行っている。
  • –debug=netlink オプションを使うことで、変換後の命令コードを見ることができる。
    • たとえばポート番号を評価する時、{80-80}と{80}では異なる命令コードになる。
  • netlink 経由で流し込まれる命令コードは nft monitor で見ることができる。
    • NFTA_GEN_PROC_PID 属性により、どのプロセス(PID)が流し込んだかもわかる。

nftables VM

https://wiki.nftables.org/wiki-nftables/index.php/Ruleset_debug/VM_code_analysis

> $ sudo nft --check --debug=netlink add rule ip filter FORWARD tcp sport 80 accept
ip filter FORWARD
  [ meta load l4proto => reg 1 ]
  [ cmp eq reg 1 0x00000006 ]
  [ payload load 2b @ transport header + 0 => reg 1 ]
  [ cmp eq reg 1 0x00005000 ]
  [ immediate reg 0 accept ]

たとえば上記のコマンドから nftables vm の命令コードを受け取った場合、以下のように解釈できる

  1. meta load l4proto => reg 1
    • メタデータから L4 プロトコルを読み込み、レジスタ 1 に格納
  2. cmp eq reg 1 0x00000006
    • レジスタ 1 の値が 0x00000006(TCP)と等しいか比較
  3. payload load 2b @ transport header + 0 => reg 1
    • トランスポートヘッダのオフセット 0 から 2 バイトを読み込み、レジスタ 1 に格納
  4. cmp eq reg 1 0x00005000
    • レジスタ 1 の値が 0x00005000(ポート 80)と等しいか比較
    • host cpu のエンディアンに依存するため、0x5000 になる
  5. immediate reg 0 accept
    • 条件がすべて満たされた場合、パケットを受け入れる

命令コードの例・違い

  1. 80-80 のように同じ値を指定しても律儀に範囲チェックを行う
> $ sudo nft --check --debug=netlink "add rule ip filter FORWARD tcp sport {80-80} accept"
ip filter FORWARD
  [ meta load l4proto => reg 1 ]
  [ cmp eq reg 1 0x00000006 ]
  [ payload load 2b @ transport header + 0 => reg 1 ]
  [ cmp gte reg 1 0x00005000 ]
  [ cmp lte reg 1 0x00005000 ]
  [ immediate reg 0 accept ]
  1. {80,80} のように同じ値を複数指定しても set を作成してから評価する
> $ sudo nft --check --debug=netlink "add rule ip filter FORWARD tcp sport {80,80} accept"
__set%d filter 3 size 2
__set%d filter 0
        element 00005000  : 0 [end]     element 00005000  : 0 [end]
ip filter FORWARD
  [ meta load l4proto => reg 1 ]
  [ cmp eq reg 1 0x00000006 ]
  [ payload load 2b @ transport header + 0 => reg 1 ]
  [ lookup reg 1 set __set%d ]
  [ immediate reg 0 accept ]
  1. 単一の値の場合は{}で括っていても eq チェックとして最適化される
> $ sudo nft --check --debug=netlink "add rule ip filter FORWARD tcp sport {80} accept"
ip filter FORWARD
  [ meta load l4proto => reg 1 ]
  [ cmp eq reg 1 0x00000006 ]
  [ payload load 2b @ transport header + 0 => reg 1 ]
  [ cmp eq reg 1 0x00005000 ]
  [ immediate reg 0 accept ]
  1. raw packet match の場合でもやっていることは同じ。
> $ sudo nft --debug=netlink "add rule ip filter FORWARD ip protocol ip @th,0,8 0x45 accept"
ip filter FORWARD
  [ payload load 1b @ network header + 9 => reg 1 ]
  [ cmp eq reg 1 0x00000000 ]
  [ payload load 1b @ transport header + 0 => reg 1 ]
  [ cmp eq reg 1 0x00000045 ]
  [ immediate reg 0 accept ]

nft monitor

nft monitor コマンドを使うことで、netlink 経由で流し込まれた命令コードをリアルタイムで拾うことができる

> $ sudo nft monitor
add rule ip filter FORWARD tcp sport 80 accept
# new generation 90 by process 823461 (nft)

Q. netlink 経由では命令セットで流れているはずだが、なぜ human readable な形で出力されるのか? A. nft monitor は流れてきた命令コードを解釈し、human readable な形に変換して出力している

そのため、nft monitor の出力結果は必ずしも元の命令コードと完全に一致するわけではない。 (nft list ruleset などの表示系も同様のことを行なっている)

NFTA_GEN_PROC_PID / NFTA_GEN_PROC_NAME

Linux 4.14 からは netlink メッセージに NFTA_GEN_PROC_PID / NFTA_GEN_PROC_NAME 属性が追加され、どのプロセスがルールを流し込んだかを知ることができる。

# new generation 90 by process 823461 (nft)

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=784b4e612d42a2b7578d7fab2ed78940e10536bc