Linuxにおける新たなパケットフィルタリングツール「nftables」入門

Linuxにおいて広く使われているパケットフィルタリングツールは「iptables」だが、ここ数年このiptablesに代わる新たなパケットフィルタリングツール「nftables」の開発が進んでおり、昨今では標準でnftablesを採用するLinuxディストリビューションも登場している。本記事ではこのnftablesの概要と、nftablesを使ったパケットフィルタリングの設定について紹介する。

DebianやRHELで標準採用されたnftables

Linuxにおいては、ファイアウォールのようなパケットフィルタリングやパケットの転送、ルーティングなどの機能を設定するツールとして長らく「iptables」が使われていた。iptablesはLinuxカーネル2.4系の時代から提供されており、現在でも多くのLinuxディストリビューションで標準搭載されている。しかし、昨今ではiptablesの設計の古さや効率の悪さなども指摘されるようになっている。そこで新たに開発されたのが今回紹介する「nftables」だ。

最近ではRed Hat Enterprise Linux 8やDebian 10など、iptablesに変わってnftablesをパケットフィルタリングのためのデフォルトバックエンドとして採用するディストリビューションも増えており、今後nftablesの利用シーンは増えていくと予想される。そこで本記事では、このnftablesの概要や設定コマンドの使い方、基本的な設定例などを紹介していく。

nftablesの特徴

nftablesは、2014年1月にリリースされたLinuxカーネル3.13以降で利用できるパケットフィルタリング機能だ。従来利用されていたiptablesとの相違点としては、以下が挙げられる。

ebtablesやarptables、iptables、ip6tablesを統合

Linuxにおいてはネットワークパケットのフィルタリング設定ツールとしてiptablesのほか、Ethernetフレームを対象とした「ebtables」、ARPパケットを対象とした「arptables」、IPv6を対象とした「ip6tables」と、プロトコルごとに異なるツールが提供されていた。nftablesではこれらの機能をすべて単一のフロントエンドで管理できるようになっている。

性能の向上

nftablesではフィルタリングルールをリスト形式のデータではなく、プログラムのような形で表現するようになった。ユーザーが作成したパケットフィルタリングルールは、nftablesのフロントエンドによってコンパイルされて命令の集合に変換され処理される。これによってパケットの処理が高速化され、より複雑なルールを簡潔に記述できるようにもなっている。

また、iptablesでは、ルールセットの更新時にはカーネル空間内にあるすべてのルールセットをいったん削除し、その後新たなルールセットを読み込む、という方式でルールの設定や変更を行っている。こういった構造から大規模なルールセットを更新する際には時間がかかるという問題があった。また、変更がアトミックには行われない点も問題とされていた。nftablesではこういった問題も解決されており、カーネル内に読み込まれたルールを個別に指定して削除したり、新たなルールを追加したりできるようになった。さらに、こういったルールの追加/削除処理はアトミックに実行されるようになっている。

より簡潔・柔軟にルールを設定できる

nftablesではルールをより柔軟に定義できるようになった。たとえば、1つのルール内でマッチしたパケットに対する処理を複数定義することが可能にあった。

また、iptablesではIPやイーサネットなど、対象のプロトコルごとに異なる流儀があったが、nftablesではこれらプロトコルを統一的に扱えるようになり、複雑さが減っている。

カーネルモジュールへの依存が少ない

iptablesではコネクション追跡を行うためのモジュールやNATモジュール、MACアドレスによるマッチを行うためのモジュールなど、個別機能がカーネルモジュールで実装されている。そのため、機能を追加するためにはカーネルもしくはカーネルモジュールのコンパイルが必要だった。一方nftablesではプロトコルに依存する処理などがユーザーランドで実装されており、これによってカーネル/カーネルモジュールの再コンパイルなしで新たな機能を導入できるようになっている。

iptablesとの互換レイヤーも提供される

nftablesではiptablesとの互換性を提供する仕組みがあり、バックエンドとしてnftablesが導入されている環境でもこの互換レイヤを使用することで、従来のiptablesと同様にiptablesコマンドでまったく同じようにパケットフィルタリング設定を行うことが可能だ。

nftables、iptablesとNetfilterとの関係

nftablesを理解するには、Linuxのネットワーク関連処理フレームワークであるNetfilterについての知識も若干ではあるが必要だ。そのため、簡単ではあるがNetfilterについてもここで解説しておこう。

Netfilterでは、Linuxカーネルがさまざまなネットワーク処理を実行する際に、そのタイミングで事前に登録しておいた処理を実行できるようにするための「フック」と呼ばれる仕組みが提供されている。Netfilterでは入力(input)や転送(forward)、出力(output)といったネットワークパケット処理の各段階においてフックを使って登録された関数を実行することで、送信先/送信元などの情報やパケットの中身を操作したり、パケットを破棄したりすることができるようになっている。

nftablesではこのNetfilterの各フックが実行されるタイミングで、あらかじめ登録しておいたルールに従ってパケットを処理することで、パケットの操作やフィルタリングを実現している。

nftablesを利用できる環境

nftablesを利用するには、まずLinuxカーネルでのサポートが必要だ。前述の通り、Linuxカーネル3.13以降ではnftablesのサポートが組み込まれており、カーネルモジュールとしてビルドされるのが一般的だ。この場合、nftablesは「nf_tables」というモジュール名で提供されており、このモジュールが組み込まれていればnftablesが利用できる。組み込まれているモジュール一覧はlsmodコマンドで確認できる。

$ lsmod | grep nf_tables

また、nftablesが有効になったカーネルに対し各種設定などを行うための「libmnl」や「libnftnl」といったフロントエンドも必要となる。

nftablesのWikiでは、対応しているLinuxディストリビューションとしてDebianおよびArch Linux、Ubuntu、Fedoraが挙げられている。ただし、デフォルトでiptablesではなくnftablesが利用されるよう設定されているディストリビューションはDebian 10(buster)やRed Hat Enterprise Linux(RHEL) 8などまだ一部に限られている。デフォルトでiptablesを利用するよう設定されているディストリビューションでは、多くの場合、必要なバイナリを、パッケージマネージャを使ってインストールすることで、nftablesが利用できるようになる。

ちなみに、Debian 10ではバックエンドはnftablesを使用しているものの、前述の互換レイヤが採用されており、デフォルトではiptablesコマンドでパケットフィルタリング設定を行うようになっている。そのため、後述するnftコマンドによるパケットフィルタリング設定を行うには別途nftablesパッケージのインストールが必要となる。

iptablesコマンドがiptablesを利用しているのか、それともnftablesの互換レイヤを使用しているかは、iptablesを「--version」オプション付きで実行することで確認できる。次のようにバージョン番号のあとに「nf_tables」という文字列が表示された場合、nftablesを使ってパケットフィルタリングが実行されている。

# iptables --version
iptables v1.8.2 (nf_tables)

nftablesのアーキテクチャと設定方法

nftablesを使ったパケットフィルタリング設定を行うには、まずnftablesがどのような形でフィルタリングルールを管理しているかを理解しておく必要がある。続いてはそれらについて解説しよう。

nftablesの設定や管理を行う「nft」コマンド

nftablesでは、「nft」というコマンドを使って各種設定や管理を行う。nftコマンドにはさまざまな機能があり、次のように実行する処理と処理対象を引数で指定する。

# nft <コマンド> <処理対象> <設定パラメータなど>...

指定できるコマンドには規則性があり、たとえば処理対象の一覧を表示する場合は「list」、処理対象の作成や追加を行う場合は「create」や「add」、削除を行う場合は「delete」などを指定する。なお、nftコマンドの実行には通常は管理者権限が必要だ。

また、nftコマンドを「-i」オプション付きで実行すると対話的モード(interactiveモード)となり、対話的にコマンドを実行できるようになる。

# nft -i
nft>

対話的モードを抜けるには、Ctrl+Dを入力すれば良い。

対象とするプロトコルを指定する「アドレスファミリ」

nftablesでは、対象とするプロトコルを「アドレスファミリ(Address Family)」、もしくは単に「ファミリ(Family)」と呼び、各プロトコル毎に「テーブル(tables)」と呼ばれる設定を作成することでフィルタリングルールを設定する。nftablesが扱えるアドレスファミリは、現時点では次の表1のとおりだ。

表1 nftablesが対象とするアドレスファミリ
名称 説明
ip IPv4
ip6 IPv6
inet IPv4およびIPv6
arp ARP
bridge ネットワークブリッジ
netdev ネットワークインターフェイスなどのネットワークデバイス

たとえば、かつてiptablesで設定していたIPv4に関連するパケットフィルタリング設定は、nftablesにおいては「ip」というアドレスファミリに紐付けたテーブルを作成することで設定できる。同様に、ip6tablesやarptables、ebtablesで設定していたものはそれぞれ「ip6」や「arp」、「bridge」というアドレスファミリに紐付けたテーブルで設定する。

「inet」「および「netdev」アドレスファミリはnftablesで新たに導入されたもので、まず「inet」はIPv4およびIPv6の両方を対象にした設定を行えるアドレスファミリだ。従来IPv4とIPv6の両方にまたがったフィルタリングルールを設定したい場合はiptablesとip6tablesの両方で同じようなルールを追加する必要があったが、nftablesではinetアドレスファミリで指定することで単一の設定で両プロトコルを対象とした設定が可能になっている。

また、netdevアドレスファミリはネットワークデバイスに入ってきたすべてのネットワークトラフィックを対象に設定を行えるもので、特に特定のパケットをドロップさせるような設定で効果を発揮する。また、ロードバランシングなどにも活用できるそうだ。

フィルタリングルール全体の確認や削除

nftablesでは、フィルタリングルール設定全体のことを「ルールセット(ruleset)」と呼ぶ(図1)。ルールセットにはアドレスファミリ毎に作成できる「テーブル」が含まれており、さらにテーブル内にはルールを管理するための入れ物である「チェーン」、そしてパケットフィルタリングをどのように行うかを記述した「ルール」が含まれるという構造となっている。

図1 nftablesにおける設定構造

テーブルやチェーン、ルールといった言葉はiptablesでも使われており、基本的にはこれらはnftablesでも同様の意味で使われる。ただし、いくつか相違点もある。まず、iptablesでは、基本的にはユーザーは、テーブルを自由に作成できないが、nftablesではいくつでもテーブルを作成することができる。たとえば図1のように、「ip」アドレスファミリに対し「テーブルA」と「テーブルC」のように複数のテーブルを作成することも可能だ。また、チェーンについてもユーザーが自由に作成できる。

さて、nftコマンドではこれらルールセットを対象とするコマンドとして、「list」や「flush」が用意されている。たとえばルールセット全体を表示するには、次のように「list」コマンドを使用する。

# nft list ruleset

アドレスファミリを指定してそのアドレスファミリに関する設定のみを出力することもできる。たとえばIPv4に関するフィルタリング設定を出力するには、次のように実行する。

# nft list ruleset ip

「flush」コマンドでは、ルールセット全体を消去できる。

# nft flush ruleset

listコマンドと同様、アドレスファミリを指定することも可能だ。その場合、指定したアドレスファミリに関する設定のみが消去される。たとえばIPv4に関するフィルタリング設定をすべて消去するには、次のように実行する。

# nft flush ruleset ip

なお、この際に特に確認プロンプトなどは表示されないので注意したい。

ルールセットの保存と読み出し

nftコマンドでは、テキストファイルに記述されたルールセットを読み込む「-f」オプションが用意されている。

# nft -f <ファイル名>

「nft -f」コマンドでは、「nft list ruleset」コマンドで出力されたルールセットと同じフォーマットを使ってルールセットを認識する。そのため、設定したルールセットを後から再設定できるように保存したい場合、単に「nft list ruleset」の出力結果をリダイレクトするなどしてファイルに保存しておけば良い。

たとえば、ルールセットを「ruleset.txt」というファイルに保存しておくには次のようにする。

# nft list ruleset > ruleset.txt

このファイルから設定を復元するには、次のようにする。

# nft -f ruleset.txt

テーブルの作成

iptablesではデフォルトでテーブルが用意されていたが、nftablesではデフォルトのテーブルは存在しない。そのため、各アドレスファミリを対象としたパケットフィルタリングルールを設定するためには、まずテーブルを作成する必要がある。テーブルの作成は「add」もしくは「create」コマンドで行える。

add table [<アドレスファミリ>] <テーブル名> [<フラグ>]
create table [<アドレスファミリ>] <テーブル名> [<フラグ>]

addおよびcreateはどちらも基本的に同じ処理を行うが、addの場合はすでに同名のテーブルが存在する場合には処理が無視される一方、createの場合はエラーとなる点が異なる。また、アドレスファミリおよびフラグの指定は省略可能だ。アドレスファミリ指定が省略された場合、「ip」が指定されたものとして処理される。また、フラグは次の形式で指定する。

{ flags <フラグ> }

現在サポートされているフラグは「dormant」のみだ。このフラグが付与されたテーブルでは、テーブル内で定義された処理が実行されなくなる。これは、テーブルを削除せずに一時的に無効化するといった用途に利用できる。

ちなみに、同じアドレスファミリを対象にした複数のテーブルを作成した場合、実行順序は後述するテーブル内のチェーンの優先度によって決定される。

さて、たとえばinetアドレスファミリを対象とする「foo」というテーブルを作成するには、次のように実行する。

# nft create table inet foo

テーブル一覧の取得は「list」コマンドで実行できる。

list tables [<アドレスファミリ>]

listコマンドでは、アドレスファミリ指定を省略するとすべてのテーブルが表示される。

# nft list tables
table inet foo

また、すでに作成済みのテーブルを「dormant」フラグを付けて無効化するには、「add」コマンドを利用する。

# nft add table inet foo { flags dormant \; }

フラグについては「list tables」コマンドでは確認できないが、「list ruleset」コマンドで確認できる。

# nft list tables
table inet foo

# nft list ruleset
table inet foo {
        flags dormant
}

テーブルの削除は「delete」コマンドで実行できる。

delete table [<アドレスファミリ>] <テーブル名>

deleteコマンドではアドレスファミリの指定が必要で、アドレスファミリを省略した場合は「ip」が指定されたものとして処理される。

# nft delete table inet foo

deleteコマンドはテーブル名だけでなく、「ハンドル」で対象を指定することもできる。

delete table [<アドレスファミリ>] handle <ハンドル>

ハンドルはテーブルなどのオブジェクトに対し内部的に一意に割り振られた数値で、「-a」オプション付きで「list ruleset」コマンドを実行することで確認できる。

# nft -a list ruleset
table inet foo { # handle 25
}

この場合、inetアドレスファミリの「foo」テーブルには「25」というハンドルが割り当てられていることが分かる。この場合、次のようにしてテーブルを削除できる。

# nft delete table inet foo handle 25

チェーンの作成

nftablesでは、処理対象とするパケットを指定するための条件と、その条件に合致したパケットに対して実行する処理を記述したものを「ルール(rule)」と呼ぶ。そして、このルールを実行する順に格納する入れ物を「チェーン(chain)」と呼ぶ。ルールを作成するには、まずそれを入れるためのチェーンを作成する必要がある。

チェーンには、Netfilterのフックによって呼び出される「ベースチェーン(base chain)」と、ほかのチェーンから呼び出される「レギュラーチェーン(regular chain)」の2種類がある。ベースチェーンは作成時にそのタイプや紐付けるフック、デバイス、プライオリティなどが指定されたチェーンで、Nefilterがそのチェーンに紐付けたフックを呼び出すタイミングでそのチェーン内に登録されたルールが順に実行される。

いっぽう、レギュラーチェーンはルール内の「jump」や「goto」といったステートメントによって呼び出されるもので、これらのステートメントによって呼び出されない限りは実行されない。プログラミング言語におけるサブルーチンのようなものと考えれば分かりやすいだろう。

さて、チェーンの作成は、addもしくはcreateコマンドで行う。

add chain [<アドレスファミリ>] <テーブル名> <チェーン名> [<ベースチェーンのパラメータ>]
create chain [<アドレスファミリ>] <テーブル名> <チェーン名> [<ベースチェーンのパラメータ>]

テーブル作成の場合と同様、addコマンドでは登録先テーブルにすでに同名のチェーンが存在していた場合でもエラーにならず、createコマンドの場合はエラーになる。また、アドレスファミリ指定を省略した場合には「ip」が指定されたとみなされる。

「ベースチェーンのパラメータ」はベースチェーンを作成する際にのみ必須となるパラメータで、次のような形式で指定する。

{ type <タイプ> hook <フック> [device <デバイス>] priority <プライオリティ> ; [policy <ポリシー> ;] }

このうち「タイプ」「フック」「プライオリティ」は必須のパラメータだ。まず「タイプ」はそのチェーンがどのような動作を行うためのものかを指定するもので、また「フック」では紐付けるNetfilterのフックを指定する。各アドレスファミリごとに指定できるタイプは異なり、またタイプ毎に指定できるフックも異なる(表2、3)。さらに、たとえばNATに関連するステートメントはタイプとしてnatが指定されたチェーンでしか利用できないといった制約もある。

表2 指定できるタイプ
タイプ そのタイプを使用できるアドレスファミリ そのタイプで指定できるフック 説明
filter すべて すべて 汎用的に使用可能
nat ip、ip6 prerouting、input、output、postrouting NATを実行するために使用する
route ip、ip6 output パケットのルーティングに使用する
表3 指定できるフック
フック 対応するアドレスファミリ 説明
prerouting ip、ip6、inet、bridge システムがパケットを受信した際、ルーティング前に実行される
input ip、ip6、inet、bridge、arp パケットがローカルマシン向けのものだと判断された際に実行される
forward ip、ip6、inet、bridge パケットが別のホストに転送/ルーティングされるものだと判断された際に実行される
output ip、ip6、inet、bridge、arp ローカルプロセスがパケットを生成した際に実行される
postrouting ip、ip6、inet、bridge システムがパケットを送信する際に実行される
ingress netdev システムがパケットを受信した際に実行される

「デバイス」ではそのチェーンを適用するデバイスを指定する。デバイスが指定されなかった場合は、すべてのデバイスが対象となる。

「プライオリティ」は、同じフックが指定されたチェーンが複数存在する場合に、どのチェーンから実行するかを決めるためのパラメータだ。符号付き整数で指定し、この値が小さい順にチェーンが実行される。

「ポリシー」はそのチェーン内で指定されたルールによって処理対象のパケットのaccept(受け入れ)もしくはdrop(遮断)が決定されなかった場合、そのパケットをどう処理するかを指定するものだ。「accept」もしくは「drop」(遮断)が指定できる。省略された場合はacceptが指定されたものとみなされる。

たとえば、IPv4向けの「foo」というテーブルに対し、タイプとして「filter」、フックとして「input」を指定し、優先度が1、ポリシーとして「accept」を指定して「hoge」という名前のベースチェーンを作成する場合、次のように実行する。なお、シェルでは「;」はコマンドの終端を示す記号なので、ここではその前に「\」を入れてエスケープしている。

# nft add chain ip foo hoge { type filter hook input priority 1 \; policy accept \; }

前述のとおり、タイプやフック、プライオリティなどの指定を行わずにチェーンを作成することもできる。その場合、そのチェーンはレギュラーチェーンとして扱われ、後述する「jump」や「goto」などのコマンドで明示的に指定しない限り実行されることはない。

# nft add chain inet bar the_chain

作成されているチェーン一覧は、「list chains <アドレスファミリ>」コマンドで確認できる。アドレスファミリを省略した場合、すべてのチェーンが対象として表示される。

# nft list chains
table ip foo {
        chain hoge {
                type filter hook input priority 1; policy accept;
        }
}
table inet bar {
        chain the_chain {
        }
}

指定したテーブル内のチェーンを一覧表示するには「list table [<アドレスファミリ>] <テーブル名>」コマンドを実行すれば良い。

# nft list table foo
table ip foo {
        chain hoge {
                type filter hook input priority 1; policy accept;
        }
}

この場合、アドレスファミリを省略すると「ip」が指定されたとみなされる。

チェーンの削除は「delete」コマンドで、リネームは「rename」コマンドで実行できる。

delete chain [<アドレスファミリ>] <テーブル名> <チェーン名>
delete chain [<アドレスファミリ>] <テーブル名> handle <ハンドル>
rename chain [<アドレスファミリ>] <テーブル名> <チェーン名> <変更後のチェーン名>

たとえば、ipアドレスファミリの「foo」テーブルから「hoge」チェーンを削除するには、次のように実行する。

# nft delete chain ip foo hoge

deleteコマンドではテーブルの場合と同様、ハンドルで対象を指定することも可能だ。ハンドルは-aオプション付きで「list chains」コマンドを実行することで確認できる。

# nft -a list chains
table ip foo {
        chain hoge { # handle 1
                type filter hook input priority 1; policy accept;
        }
}

この場合、「hoge」というチェーンを削除するには次のように実行する。

nft delete chain ip foo handle 1

ルールの作成

ルールは、各パケットをどのように処理するかを定義するものだ。前述のとおり、nftablesではパケットの処理の各段階で、対応するフックに紐付けられているチェーンのうちもっともプライオリティが低いものを探し、そのチェーンに登録されているルールを順に実行する。すべてのルールの評価が完了するか、そのチェーン内でのルール評価を終了させるステートメントを実行した場合、次のチェーンを実行するようになっている。ただし、後述する「drop」ステートメントでパケットの破棄を行った場合はその時点でパケットの処理は終了し、チェーン内の残りのルールや残りのチェーンは実行されない。

ルールの作成は、「add」もしくは「insert」コマンドを使用する。書式としては次のようになる。

add rule [<アドレスファミリ>] <テーブル名> <チェーン名> <ステートメント>
insert rule [<アドレスファミリ>] <テーブル名> <チェーン名> <ステートメント>

addとinsertとの違いは、addがチェーンの最後に新たなルールを追加するのに対し、insertではチェーンの最初に追加するという点だ。また、詳しくは後述するが、「ステートメント(statement)」ではパケットに対して実行する処理を定義する。

「add/insert rule」コマンドでは、追加する場所を0から始まるインデックスやハンドルで指定することもできる。

add rule [<アドレスファミリ>] <テーブル名> <チェーン名> index <インデックス> <ステートメント>
insert rule [<アドレスファミリ>] <テーブル名> <チェーン名> index <インデックス> <ステートメント>

add rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル> <ステートメント>
insert rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル> <ステートメント>

addの場合、インデックスで指定した位置にあるルールもしくはハンドルで指定されたルールの後ろ、insertの場合は前に新たなルールが追加される。

ルールの更新は「replace」、削除は「delete」コマンドで行える。

replace rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル> <ステートメント>
delete rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル>

なお、ハンドルについては次の「list chain」コマンドを-aオプション付きで実行することで確認できる。

list chain [<アドレスファミリ>] <テーブル名> <チェーン名>

たとえばアドレスファミリが「ip」、テーブル名が「foo」、チェーン名が「hoge」の場合、次のようになる。

# nft -a list chain ip foo hoge
table ip foo {
        chain hoge { # handle 1
                type filter hook input priority 1; policy accept;
                tcp dport http drop # handle 2
        }
}

この場合、「tcp dport http drop」というルールのハンドルは「2」となる。

ステートメントと式

「add rule」コマンドや「insert rule」コマンドなどでは、「ステートメント」として実行する処理を指定する。ステートメントは実行する処理を指定するものと、「式(expression)」と呼ばれる、条件式を記述するものがある。ステートメントは複数を指定することができ、次のように式の後に実行する処理を続けることで、式で指定した条件に合致するパケットのみに対し処理を実行できる。

<式1>[<式2>...] <実行する処理1> [<実行する処理2>...]

また、式を複数指定した場合は、そのすべてを満たす場合に処理が実行される。実行する処理を複数指定した場合は、その順に処理が実行される。たとえば、「log drop」のように指定すると、そのパケットの情報をログに記録したうえでdropできる。

利用できるステートメントとしてはさまざまなものが用意されているが、代表的なものとしては表4のものがある。

表4 代表的なステートメント
ステートメント 説明
accept パケットをacceptしてルールセットの評価を終了する
drop パケットをdropしてルールセットの評価を終了する
queue ユーザースペースのnfnetlink_queueハンドラにパケットを渡す
continue ルールセットの評価を継続し次のルールを評価する
return 現在のチェーンから抜け、一つ前のチェーン(呼び出し元チェーン)の次のルールの評価を実行する。ベースチェーンの場合はacceptと同義
jump <チェーン名> 指定したチェーンに遷移し、そのチェーン内の最初のルールを評価する。遷移後のチェーンでreturnが実行される、もしくはすべてのルールの評価が終了した場合、この次のルールの評価が実行される
goto <チェーン名> 指定したチェーンに遷移し、そのチェーン内の最初のルールを評価する。jumpと違い、遷移後のチェーンから現在のチェーンには戻らない
set パケットを改変する
log ログに指定した文字列やパケットの情報を記録する
reject パケットをrejectし、送信元にその情報を返送する。withキーワードを併用することで、返送する情報を指定できる
counter パケット数やバイト数をカウンターに記録する
ct パケットにconntrackラベルやマークを付与する
meta パケットにメタ情報を付与する
snat、dnat、masquerade、redirect NATやIPマスカレード、パケットのリダイレクトを行う
dup パケットを複製して別のアドレスやデバイスに送信する
fwd パケットを指定したデバイスに転送する

また、式ではどの情報を条件判断に指定するかをキーワードで指定する。次の表5のものがある。

表5 式で利用できる代表的なキーワード
キーワード 評価対象
meta パケットのメタデータ
fib forwarding information base(fib)
rt ルーティング情報
ether イーサネットヘッダ
vlan VLANヘッダ
arp ARPヘッダ
ip IPv4ヘッダ
icmp ICMPヘッダ
ip6 IPv6ヘッダ
tcp TCPヘッダ
udp UDPヘッダ
ct コネクションの状態(Conntrack)

各式では引数として、その対象のうちどの情報を対象に条件判断を行うかを指定できる。たとえば「meta」式では次の表6のキーワードが指定できる。

表6 「meta」式で指定できるキーワード(抜粋)
キーワード 説明
length パケットのサイズ(バイト数)
nfproto IPv4なら4、IPv6なら6
l4proto レイヤ4におけるプロトコル番号
protocol EtherTypeの値
priority パケットの優先度
mark パケットマーク
iif 入力インターフェイスのインデックスもしくはインターフェイス名(インターフェイス名を指定した場合、nftコマンド実行時にインデックスに変換される)
iifname 入力インターフェイス名
iiftype 入力インターフェイスタイプ
oif 出力インターフェイスのインデックスもしくはインターフェイス名(インターフェイス名を指定した場合、nftコマンド実行時にインデックスに変換される)
oifname 出力インターフェイス名
oiftype 出力インターフェイスタイプ
ibrname 入力ブリッジインターフェイス名
obrname 出力ブリッジインターフェイス名
pkttype パケットタイプ
cpu パケットを処理するCPUのCPU番号
iifgroup 入力デバイスグループ
oifgroup 出力デバイスグループ
skuid ソケットのuid
skgid ソケットのgid
cgroup cgroup ID
random 疑似乱数

なお、このうち「length」「nfproto」「l4proto」「protocol」「priority」以外を指定する場合は「meta」キーワード自体を省略できる。

たとえば、入力インターフェイスが「eth0」のパケットを指定するには次のようにする。

meta iif eth0

もしくは

iif eth0

また、「ip」式では、表7の各ヘッダをキーワードとして指定できる。

表7 「ip」式で指定できるキーワード
キーワード 説明
version IPヘッダのバージョン
hdrlength オプションも含めたIPヘッダの長さ
dscp Differentiated Services Code Point
ecn Explicit Congestion Notification
length パケットの長さ
id ID
frag-off フラグメントのオフセット
ttl TTL
protocol プロトコル
checksum チェックサム
saddr 発信元IPアドレス
daddr 送信先IPアドレス

たとえば「発信元が192.0.2.1のパケット」を指定するには次のように指定する。

ip saddr 192.0.2.1

「tcp」式では、表8の各ヘッダをキーワードとして指定できる。

表8 「tcp」式で指定できるキーワード
キーワード 説明
sport 発信元ポート番号
dport 送信先ポート番号
sequence シーケンス番号
ackseq Acknowledgement番号
doff データオフセット
reserved Reservedエリア
flags TCPフラグ
window ウィンドウ
checksum チェックサム
urgptr Urgent pointer

たとえば送信先ポートが80のパケットを指定したい場合、次のような表現となる。

tcp dport 80

「ct」式では、表9のようなキーワードが利用できる。

表9 「ct」式で指定できるキーワード(抜粋)
キーワード 説明
state コネクションの状態(invalid/established/related/new/untracked)
direction コネクションの方向(original/reply)
status コネクションのステータス(expected/seen-reply/assured/confirmed/snat/dnat/dying)
mark コネクションに付与されたマーク
expiration コネクションの有効期限
label コネクションに付与されたラベル
saddr 発信元アドレス
daddr 送信先アドレス
packets パケットカウント
bytes バイト数カウント
avgpkt パケット当たりの平均バイト数

たとえば、コネクション確立済みのパケットは許可するという条件は次のような表現となる。

ct state established

よくあるnftablesの設定例

ここまででnftrablesの基本的な概念やコマンドを説明してきたが、これらを使った具体的な設定例を紹介していこう。

すべての設定をリセットする

始めに、すべての設定をリセットする方法を再度確認しておこう。これは先に述べたとおり、「flush ruleset」コマンドで実行できる。

# nft flush ruleset

実際に設定がすべてリセットされているかどうかは、「list ruleset」コマンドで確認できる。ここで何も表示されなければ設定がリセットされているということだ。

# nft list ruleset

ホスト宛の特定パケットをdropするルールを作成する

まずは設定例として、ホスト宛の特定のパケットをdropするルールを作成してみよう。

なお、SSHなどでリモートログインしてパケットフィルタリングの設定を行う場合、設定を誤るとSSHのパケットがdropされて接続が行えなくなる場合もある。そのため、必ず別途ログインできる手段を用意して作業を行うことをおすすめする。

さて、nftablesでパケットフィルタリングを設定するには、まず「create table」コマンドで設定対象とするアドレスファミリに対応したテーブルを作成する必要がある。テーブル名は任意のものを指定できるが、たとえばパケットフィルタリングのためのルールなら「filter」など、分かりやすいものを指定すべきだろう。

# nft create table ip filter

続いて、ルールを定義するためのチェーンを作成する。チェーンについても分かりやすいような名前を定義しよう。次の例は、ローカルホスト宛のパケットをフィルタリングするルールを定義するチェーンを「input」という名前で作成するものだ。

# nft create chain ip filter input { type filter hook input priority 0 \;}

ここではフィルタリングを行うためtypeとして「filter」を、ローカルホスト宛のパケットを対象とするためhookとして「input」を指定している。priorityは優先度が高い「0」を指定した。

なお、前述のようにチェーンのポリシーとして「drop」を指定することで、チェーン内のルールで明示的にacceptされたパケット以外をすべてdropすることもできるが、その場合チェーンを作成した時点で指定したフックを通過する全パケットがdropされるようになる。この例では「input」フックを対象にしているため、チェーンを作成した時点でSSHなどの接続も不可能になってしまうので注意したい。

チェーンを作成したら、そこに実際に適用するルールを追加していく。たとえば22番ポート向けのパケットをすべてacceptするには、次のように指定する。

# nft add rule ip filter input tcp dport 22 accept

また、最後にパケットをdropするルールを追加することで、明示的にacceptしたパケット以外のパケットをすべてdropできる。

# nft add rule ip filter input drop

ここまでの設定を「list ruleset」コマンドで確認すると、次のようになる。

# nft -a list ruleset
table ip filter { # handle 17
        chain input { # handle 2
                type filter hook input priority 0; policy accept;
                tcp dport ssh accept # handle 3
                drop # handle 4
        }
}

なお、ハンドル表示については実行時の状況によって変化するため、必ずしもこれと同じ結果になるわけではない。この後で紹介する実行例でハンドルを指定しているものがあるが、手元の環境で試す場合は適宜環境に応じたハンドルの値に置き換えて欲しい。

さて、これでSSH以外の通信をすべてブロックすることが可能となったが、この設定ではDNSによる名前解決が行えず、またローカルホストから外部へのIPv4でのTCP通信も基本的にすべて遮断されてしまう。たとえばwgetコマンドで外部のWebサーバーに接続しようとした場合、次のように名前解決の段階で止まってしまう。

# wget -O - http://example.com/
--2019-08-27 19:20:56--  http://example.com/
Resolving example.com (example.com)...

これは、22番ポート宛以外のパケットをすべてブロックした結果、DNSサーバーからの応答パケットや、ローカルホストからのコネクション接続要求に対し応答するパケットもブロックされてしまうためだ。

そこで、次のように「ct state」式を使い、パケットのステート(状態)が「established」(すでに確立したコネクションでの通信)もしくは「related」(確立したコネクションに関連するもの)の場合にそのパケットをacceptするよう指定するルールを追加する。

# nft add rule ip filter input handle 3 ct state related,established accept

ここではハンドルとして「3」を指定し、その後ろに新たなルールを追加するよう指定している。追加後のルールセットは次のようになる。

# nft -a list ruleset
table ip filter { # handle 17
        chain input { # handle 2
                type filter hook input priority 0; policy accept;
                tcp dport ssh accept # handle 3
                ct state established,related accept # handle 6
                drop # handle 4
        }
}

これでホスト内からの通信が可能となる。

ローカルホストからのパケットを許可する

この設定では、パケットの送信元がホスト内外を問わず、このホスト向けの22番ポート宛以外のパケットをすべてdropする。もしパケットの送信元がローカルホストだった場合にそのパケットをすべてacceptしたい場合、次のようにローカルホストインターフェイス(lo)からのパケットをすべて許可するルールを追加すれば良い。

# nft insert rule ip filter input handle 3 iif lo accept

追加後のルールセットは次のようになる。

# nft -a list ruleset
table ip filter { # handle 17
        chain input { # handle 2
                type filter hook input priority 0; policy accept;
                iif "lo" accept # handle 7
                tcp dport ssh accept # handle 3
                ct state established,related accept # handle 6
                drop # handle 4
        }
}

ここでは「insert」コマンドを使い、ハンドルとして「3」を指定することでチェーンの最初にこのルールを追加している。

pingを通す

この設定では、サーバーの死活確認やネットワークの導通を調べるために使われるpingコマンドによるICMPエコー要求パケットやエコー応答パケットについてもブロックしてしまう。これらを許可するには、次のように「icmp type」式で対象のパケットを指定し、それらをacceptすれば良い。下記では「echo-request」(エコー要求)および「echo-reply」(エコー応答)を対象にaccpetするよう設定している。

# nft add rule ip filter input handle 6 icmp type { echo-request, echo-reply } accept

なお、nftコマンドではこのように複数の値をカンマで連結し、{}で囲むことで複数の値の集合(set)を表現できる。

NATの設定

続いては、NATの設定について紹介しよう。なお、NATを行う際には事前にsysctlコマンドでLinuxカーネルがパケットの転送(フォワーディング)を許可しているかも確認しておこう。

# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

ここで「net.ipv4.ip_forward =」以下が0だった場合、次のようにしてパケット転送を許可する必要がある。なお、この設定はカーネルを再起動するとリセットされてしまうので、設定を永続化させたい場合はお使いのディストリビューションに応じた設定を行って欲しい。

# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

さて、nftablesでNATの設定を行うには、typeが「nat」のチェーンを作成しそこでnatの設定を行う必要がある。設定を見返した際に簡潔になるよう、テーブルも個別に作成しておくと良いだろう。今回は「nat」というテーブルを作成した。

# nft create table ip nat

nftablesではNAT関連の処理を行うためのステートメントとして「snat」、「dnat」、「masquerade」、「redirect」の4つが用意されている(表10)。これらはtypeが「nat」のチェーンでのみ利用でき、さらにそれぞれ利用できるフックが制限されている。

表10 NAT関連の処理を行うためのステートメント
ステートメント 処理内容 利用できるフック
snat 発信元アドレス変換 postrouting、input
dnat 送信先アドレス変換 prerouting、output
masquerade IPマスカレード postrouting
redirect 単純なパケット転送 prerouting、output

また、これらNAT関連のステートメントを利用するには、preroutingフックに紐付けたチェーンと、postroutingフックに紐付けたチェーンの両方が存在している必要がある。たとえばsnatステートメントをpostroutingフックに紐付けたチェーン内で使用した場合、必ずpreroutingに紐付けたチェーンも作成しておかなければならない(チェーンの中身は空でも構わない)。

そのため、まずは次のようにこれらのチェーンを作成しておく。

# nft create chain ip nat prerouting { type nat hook prerouting priority 0 \;}
# nft create chain ip nat postrouting { type nat hook postrouting priority 0 \;}

NATの利用例の1つとして、特定ポート向けのパケットをネットワーク内の別のホストに転送する、といったものがある。nftコマンドでは、これは次のようにして設定できる。

add rule nat prerouting iif <対象インターフェイス> ip daddr <ホストのIPアドレス> tcp dport <対象ポート> dnat to <転送先IPアドレス>:<ポート>

たとえば、「198.51.100.1」というIPアドレスが割り当てられているホストで、eth0インターフェイスから入ってきた8080番ポート向けのパケットを192.0.2.100というマシンの80番ポートに転送したい場合、次のように指定すれば良い。

# nft add rule nat prerouting iif eth0 ip daddr 198.51.100.1 tcp dport 8080 dnat to 192.0.2.100:80

また、IPマスカレードを使ってインターネットに接続されていないプライベートネットワークからインターネットへのアクセスを行えるようにするには、次のように設定すれば良い。

add rule nat postrouting ip saddr <プライベートネットワークのIPアドレス範囲>oif <インターネット側ネットワークインターフェイス> masquerade

たとえばプライベートネットワークのIPアドレスが192.0.2.0/24で、インターネット側のネットワークインターフェイスがeth0だった場合、次のようになる。

# nft add rule nat postrouting ip saddr 192.0.2.0/24 oif eth0 masquerade

レギュラーチェーンを使った設定

前述の通り、チェーンには指定したフックに応じて実行されるベースチェーンと、ほかのチェーンからの呼び出しに応じて実行されるレギュラーチェーンがある。レギュラーチェーンの利用はnftablesを利用する上で必須ではないが、これを利用することで簡潔でメンテナンスしやすいフィルタリングルールを作成しやすくなる。続いてはこのレギュラーチェーンの使い方を紹介しよう。

前述の通り、レギュラーチェーンはアドレスファミリとテーブル名、チェーン名のみを指定して「add chain」や「create chain」コマンドを実行することで作成できる。

# nft add chain ip filter input_eth0
# nft add chain ip filter input_eth1

レギュラーチェーン内のルールは、別のチェーン内で「jump」もしくは「goto」ステートメントを使ってそのチェーンを呼び出すことで実行できるようになる。jumpとgotoでは基本的な動作は同じだが、呼び出したチェーン内のルールをすべて実行した場合、もしくはacceptやreturnなどのルールの評価を終了するステートメントを実行された場合、jumpでは呼び出し元チェーンに戻るが、gotoではそのまま処理が終了するという点が異なる。

次のルールは、パケットが入力されたネットワークインターフェイスが「eth0」だった場合「input_eth0」というチェーンを、「eth1」だった場合「input_eth1」というチェーンを呼び出すものだ。

nft insert rule ip filter input iif eth0 jump input_eth0
nft insert rule ip filter input iif eth1 jump input_eth1

次の設定例は、これを使って入力インターフェイスに応じた処理を別チェーンで定義したものだ。

#  nft -a list table ip filter
table ip filter { # handle 19
        chain input { # handle 1
                type filter hook input priority 0; policy accept;
                iif "eth0" jump input_eth0 # handle 5
                iif "eth1" jump input_eth1 # handle 6
                iif "lo" accept # handle 7
                tcp dport ssh accept # handle 8
                ct state established,related accept # handle 9
                icmp type { echo-reply, echo-request } accept # handle 10
                drop # handle 11
        }

        chain input_eth0 { # handle 2
                tcp dport http accept # handle 12
        }

        chain input_eth1 { # handle 3
                tcp dport ssh drop # handle 13
        }

このルールでは、まずパケットの入力インターフェイスがeth0だった場合、「input_eth0」チェーンを呼び出している。ここでは宛先ポートがhttp(80番)だった場合にパケットをacceptするよう指定している。また、入力インターフェイスがeth1だった場合「input_eth1」チェーンを呼び出している。ここでは宛先ポートがssh(22番)だった場合、そのパケットをdropするよう指定している。なお、ベースチェーン(「input」チェーン)では宛先ポートがssh(22番)の場合にacceptするというルールがあるが(「# handle 8」のルール)、これよりも先にinput_eth1チェーンが呼び出されて実行されているため、eth1から入ってきた22番ポート宛のパケットはすべてdropされる。

セットの利用

nftablesでは、複数の値の集合を定義する「set」や、キーと値の組み合わせを定義する「map」などを定義して利用できる機能もある。詳しくはドキュメントなどを参照して欲しいが、これは特定のIPアドレスに対し特定の処理を実行したい、といった際に便利だ。

セットについては、次のように「add set」コマンドで定義できる。

add set [<アドレスファミリ>] <テーブル名> <セット名> { type <タイプ> ; [<そのほかパラメータ>]}

タイプはセットに格納するデータの型を指定するもので、「ipv4_addr」(IPv4アドレス)、「ipv6_addr」(IPv6アドレス)、「ether_addr」(イーサネットアドレス)、「inet_proto」(プロトコル)、「inet_service」(サービス)、「mark」(マーク)のいずれかから選択する。

たとえば、IPv4アドレスを格納する「block_ip_list」という名前のセットを作成するには、次のように実行する。

# nft add set ip filter block_ip_list { type ipv4_addr \; }

作成したセットに値を追加するには、「add element」コマンドを使用する。値は複数を指定することも可能だ。

add element [<アドレスファミリ>] <テーブル名> <セット名> { <値1> [, <値2>, ...]}

たとえばこのセットに「192.0.2.10」と「192.0.2.11」というIPアドレスを追加するには、次のように実行する。

# nft add element ip filter block_ip_list { 192.0.2.10, 192.0.2.11 }

作成済みのリスト一覧は「list sets」コマンド、リストに格納されている値は「list set」コマンドで確認できる。

# nft list sets
table ip filter {
        set block_ip_list {
                type ipv4_addr
        }
}

# nft list set ip filter block_ip_list
table ip filter {
        set block_ip_list {
                type ipv4_addr
                elements = { 192.0.2.10, 192.0.2.11 }
        }
}

作成したセットは「@<セット名>」という形式で任意のルールから参照できる。たとえば「block_ip_list」セットに登録されているIPアドレスからの接続をdropするルールは、次のように定義できる。

# nft insert rule ip filter input ip saddr @block_ip_list drop

セットを参照するルールでは、ルールが評価されるたびにセットの中身を参照してマッチングを行う。たとえばこの例では、ルールの追加後に「block_ip_list」セットに新たなIPアドレスを追加したり、セット内のIPアドレスを削除したりした場合、それに応じてdropする対象のIPアドレスも動的に変更される。

なお、セット作成時にテーブルを指定していることからも分かるとおり、セットはそれを登録したテーブル内からしか参照できない点には注意したい。

慣れてしまえばiptablesよりも簡潔かつ分かりやすいルール設定が可能

このように、nftablesはiptablesから仕様が大きく変わっているため、初めのうちは構造が理解がしにくいかもしれない。一方でルールはより柔軟かつ簡潔に定義できるようになっていることが分かる。設定対象やできることが増えた分、コマンドなどが複雑になり、やや冗長になっているものの、慣れてしまえばこちらのほうが使いやすいだろう。

まだ多くのLinuxディストリビューションではiptablesをパケットフィルタリングに使用しているが、今後より多くのLinuxディストリビューションがnftblesに移行していくと思われる。処理性能の向上といった恩恵もあるため、今のうちに試して慣れてみてはいかがだろうか。