nftablesを使って、2回に1回pingが落ちるサーバを作る

created at
updated at
technology Linux nftables iptables

/api/assets/7eda04505b21c6145883fde798936180a3596cbd.png

先日、会社の勉強会でiptables を使って、 2回に1回 ping が落ちる サーバを作る / Use iptables to create a server that pings once every two times - Speaker Deckの再演があったので、じゃあ6年後ならどうやるのかという事で調べて作った。

結論から言うと、以下の設定を投入することで実装できた。
drop-ping.nft
ip protocol icmp icmp type echo-request icmp sequence & 1 == 1 drop;
ip protocol icmp icmp type echo-request accept;
実際に投入する時は、このルールより上でICMPのecho requestをacceptしてはいけない
  • 先にacceptされるとルールの評価順の関係で1回しか落ちなくなったりする
  • ct state established, related accept; もダメで、ICMP自体はステートレスのはずだけど後続のecho requestが同じIDな場合にestablishedかrelated扱いされてしまっているらしく、drop/acceptのルールより上にあるとそちらが優先されて落ちなくなる

以下は解説なので気になる人だけどうぞ

nftablesとは
nftables is the modern Linux kernel packet classification framework. New code should use it instead of the legacy {ip,ip6,arp,eb}_tables (xtables) infrastructure. For existing codebases that have not yet converted, the legacy xtables infrastructure is still maintained as of 2021. Automated tools assist the xtables to nftables conversion process.
What is nftables? - nftables wiki
iptablesなどのxtables群を置き換えるパケットフィルタリングフレームワークで、Linux 3.13以降のカーネルで使えるようになっている。ちなみに iptables-nft という互換レイヤーも用意されており、iptablesを使ってるつもりだけど実は裏側はnftablesになってたりすることもある。

どのようにしてパケットを判別するか
iptables-extentions に含まれる u32 についてのご紹介 - do_su_0805's blog を読むと、iptablesではu32モジュールでパケットを直接解析しているらしい。
順序としては以下のとおり。
  • ICMPパケットかどうか
  • IPヘッダのプロトコル番号で判別
  • ICMPの通知内容がエコー要求通知かどうか
  • ICMPパケットのタイプで判別
  • pingの回数が奇数かどうか
  • ICMPパケットのシーケンス番号で判定

これをnftablesで再現する必要がある。上2つはいいとして最後のシーケンス番号を取ってくるのは厄介そうに思える。
しかし、Man page of NFT を読むと、PAYLOAD EXPRESSIONSセクションのICMP HEADER EXPRESSIONに sequence の記載がある。
なんとnftables側でICMPヘッダをパースしてくれて、利用者はマッチ条件を書くだけで済むという事が分かった。
あとは3つの条件を全て満たすパケットのみフィルタすればいいという訳だ。
nftablesでは条件を並べるだけでANDを構成してくれるので、以下の3つを並べて書くだけで良い。
  • ICMPパケットかどうか
  • ip protocol icmp
  • ICMPの通知内容がエコー要求通知かどうか
  • icmp type echo-request
  • pingの回数が奇数かどうか
  • icmp sequence & 1 == 0

評価順の問題
さて、肝となるルールが書けたのであとは投入するだけと思いがちだが、適当にルールを投入するだけでは上記のコードは動かない。
nftablesを有効にしている場合、たいていは以下のような設定が投入されている。
default.nft
# https://wiki.nftables.org/wiki-nftables/index.php/Simple_ruleset_for_a_workstation
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # accept any localhost traffic
        iif lo accept

        # accept traffic originated from us
        ct state established,related accept

        # accept neighbour discovery otherwise IPv6 connectivity breaks
        icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
    }
}
ここでミソとなるのが、ct state established,related accept というルールになる。
このルールはコメントのとおり、自身から開始した通信の返りを許可するというものである。
ICMP自体はステートレスだが、エコー要求通知ではIDとシーケンス番号を付与している。
nftablesでは同じIDを持つエコー要求通知のパケットを established または related として扱っているらしく、一度でも他のルールでacceptすると以降は ct state established,related accept のルールによってacceptされてしまう。
一度acceptされたパケットは以降のルールで評価される事はないため、ct state established,related accept より下にdropするルールを書いても、初回のパケットしかdropしてくれない。
普通のルールなら通したり通さなかったりを繰り返す事はないので問題にならないが、今回のようなケースでは問題になってしまう。
というわけで、今回のルールは ct state established,related accept より上に書かないといけない。
もちろん、もし他にキャッチオール的にacceptしているルールがあるなら、それよりも上に書く必要がある。

おわりに
という訳で、nftablesなら自力でパケットを解析しなくても、ヘッダの内容に応じた複雑なルールを簡単に定義できる事が分かった。
いやいや自分でパケット解析したいよという人は、Man page of NFT のRAW PAYLOAD EXPRESSIONを読んでほしい。nftablesなら、パケットを自力で解析する場合でもiptablesに比べて便利になっているのだから。