Ubuntuのiptablesとnftables、実際に動くのはどちら?
Ubuntu 20.04以降ではiptablesがnftablesへルールを書き込みます。自分のサーバーでバックエンドを確認し、ufwやDockerとの競合をnative rulesetで検証します。
Ubuntu の iptables と nftables: サーバーではどちらが動作しているか
Ubuntu 20.04 以降では、iptables コマンドは nftables ルールを書き込むフロントエンドです。カーネル内で動作するパケットフィルターは nftables 1 つであり、それを設定するユーザー空間のコマンドが 2 つあります。iptables -A INPUT の行は従来どおり機能し、そこから作成されるルールは nftables ルールとして nft で表示できます。
その情報を信じる前に、自分のサーバーで確認してください。
iptables -V
sudo update-alternatives --display iptables
sudo nft list rulesetUbuntu 24.04(2026 年 8 月時点の iptables 1.8.10)では、iptables -V が iptables v1.8.10 (nf_tables) を表示します。角括弧内の名前がバックエンドです。(nf_tables) は、そのコマンドが nftables と通信することを示します。(legacy) は旧式の x_tables バックエンドを示します。Ubuntu は現在もこれを iptables-legacy として提供しており、カーネル内では完全に分離された別のルールセットとして保持されます。update-alternatives は、その選択の背後にあるシンボリックリンクを link currently points to /usr/sbin/iptables-nft と表示します。
ファイアウォールを設定していない新しい VPS では、sudo nft list ruleset は何も表示しません。この空の出力が基準になります。従来の方法でルールを 1 つ追加し、もう一度確認してください。
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo nft list ruleset# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain INPUT {
type filter hook input priority filter; policy accept;
tcp dport 22 counter packets 0 bytes 0 accept
}
chain FORWARD {
type filter hook forward priority filter; policy accept;
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
}作成された iptables ルールは nftables ルールです。iptables-nft は作成するテーブルを識別し、nft はその識別情報を検出すると警告を表示します。このようなテーブルを nft で編集すると、同じルールを 2 つのツールが管理することになるためです。1 つのコマンドが作成した内容を確認してください。指定していないテーブルと、要求していないチェーンが作成されています。これが従来のモデルです。nftables を直接記述すると、最初に変わるのはこの部分です。
ユーザーから見えない iptables -L の内容
iptables -L は filter テーブルだけを表示します。NAT(ネットワークアドレス変換)ルールには iptables -t nat -L が必要で、mangle ルールには -t mangle が必要です。IPv6 は別のコマンドである ip6tables にあり、すべてのルールが独自に複製されています。そのため、1 つの一覧では問題がないように見えても、確認していないテーブルのルールによってパケットが破棄または書き換えられている場合があります。
sudo nft list ruleset は、すべてのアドレスファミリー、すべてのテーブル、すべてのチェーン、すべてのルールを 1 つの出力に表示します。自分で構築していないサーバーでは、このコマンドを実行するのが、実際に読み込まれている内容を確認する最も速い方法です。ルールハンドルを表示するには -a を追加します。ルールハンドルは、チェーン全体ではなく 1 つのルールだけを削除するときに必要です。
ここで、2 つの操作上の習慣も見直してください。iptables -L はアドレスとポートを名前に解決するため、名前解決が壊れているサーバーではハングしたように見えます。iptables -nvL を使用してください。また、sudo iptables-legacy -nvL で従来のバックエンドが空であることを確認します。両方のバックエンドにルールが存在すると、カーネルは両方を評価するため、どちらの一覧にも全体の状況は表示されません。
作成したテーブルとチェーンは継承されません
nftables は空の状態から始まります。作成するまで filter テーブルは存在せず、filter という語も指定した名前にすぎません。チェーンがパケットを受け取るには、type、hook、priority を指定して base chain にする必要があります。これらを指定しないチェーンは、明示的な jump または goto によってのみ到達されるため、そこへジャンプする処理がなければ負荷は発生しません。
もう1つの大きな変更点は inet ファミリーです。1つの inet テーブルで IPv4 と IPv6 に同じルールを適用できるため、iptables ではポートを閉じているのに ip6tables では完全に開いている、といった種類のバグをなくせます。この不一致は、ufw を使用するサーバーで発生する独自の障害パターンとして 扱われるほどよくあります。
以下は、完全なサーバー用ルールセットです。/etc/nftables.conf に配置します。
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set admin_ips {
type ipv4_addr
flags interval
elements = { 203.0.113.5, 198.51.100.0/24 }
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
ip protocol icmp accept
meta l4proto ipv6-icmp accept
tcp dport 22 ip saddr @admin_ips counter accept
tcp dport { 80, 443 } counter accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}2 行目は2回確認してください。flush ruleset は、ufw と Docker がそれぞれ作成したテーブルを含め、サーバー上のすべてのテーブルを削除します。実運用中のサーバーで実行する前に、先を読み進めてください。
input チェーンの最初のルールが、処理の大部分を担います。ct state established,related accept により、自分で開始した接続への応答が戻れるため、チェーンの残りでは新しい接続だけを判断すれば済みます。ct state invalid drop は、既知の接続に一致せず、有効な開始パケットでもないパケットを破棄します。それ以外は明示的に許可する通信であり、policy drop が残りを処理します。
読み込む前にファイルを検証し、その間は SSH の2つ目のセッションを開いたままにしてください。policy drop に加えて SSH ルールに1か所でも誤りがあると、自分のサーバーから締め出されます。
sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo nft list rulesetnft -c -f はファイルを解析し、読み込みを行わずにエラーを報告します。解析に成功した場合、出力はありません。
長いルール一覧は set で置き換えます
tcp dport { 80, 443 } は匿名 set です。ポートごとに 1 つずつルールを作る代わりに、1 つのルールと 1 回の検索で処理します。admin_ips のような名前付き set なら、ファイアウォールの稼働中に内容を変更できます。
sudo nft add element inet filter admin_ips { 203.0.113.9 }
sudo nft list set inet filter admin_ips
sudo nft delete element inet filter admin_ips { 203.0.113.9 }reload もルール番号の振り直しも不要です。set の要素が 5 個でも 50000 個でも、照合は 1 回の検索のままです。flags interval により、set には 198.51.100.0/24 のような範囲や CIDR(classless inter-domain routing)プレフィックスを格納できます。この flag がない場合、set は単一のアドレスだけを受け付けるため、プレフィックスの読み込みに失敗します。
set では、要素を自動的に期限切れにすることもできます。
set banned {
type ipv4_addr
flags timeout
timeout 1h
}ip saddr @banned drop を指定したルールでは、各要素が追加から 1 時間後に自動的に削除されます。Ubuntu 24.04 の fail2ban における nftables action は、この仕組みでアドレスを ban します。ルールを追加するのではなく、set に要素を追加します。ポートの仕組みがまだ不明な場合は、Linux でポートが実際に何を意味するか から始めてください。
移行時に注意すべき違いが 1 つあります。nftables は、明示的に指定しない限りパケットをカウントしません。iptables -nvL は常にすべてのルールの counter を表示します。一方、nftables で番号が付くのは counter keyword を持つルールだけです。そのため、後で debug する可能性があるルールには counter を指定してください。
フックと優先度で実行順序が決まる
ベースチェーンにはフックを指定します。フックは、パケットの経路上でチェーンが実行される位置です。preroutingはルーティング判断の前に実行されます。inputはこのマシン宛てのパケットに対して実行されます。forwardはこのマシンを経由してルーティングされるパケットに対して実行されます。outputはローカルプロセスから送信されたパケットに対して実行されます。postroutingは最後に、パケットが送信される直前に実行されます。
優先度によって、1 つのフック内でチェーンを実行する順序が決まります。番号が小さいものから実行されます。nftables では、一般的な優先度に名前が付けられています。rawは -300、mangleは -150、dstnatは -100、filterは 0、srcnatは 100 です。priority filter;と記述することは、priority 0;と記述することと同じです。
次に、ツールを併用できるかどうかを決める部分を説明します。フックに登録されたすべてのベースチェーンが、優先度順に実行されます。チェーン内でパケットを accept しても、処理は完了しません。acceptはそのチェーンだけを終了し、パケットは同じフック上にある次のベースチェーンへ進みます。dropはどこでも最終的な判定となり、パケットを直ちに停止させます。したがって、どちらが先に実行されても、自分のテーブルにある許可ルールで ufw のテーブルにある drop を取り消すことはできません。また、後から実行されるチェーンに対して、acceptを設定しても保護にはなりません。
同じフック上に同じ優先度のベースチェーンが 2 つある場合、サービスの登録順に実行されます。登録順は、どのサービスが先に起動したかによって決まります。この順序は再起動後に変わることがあります。ufw と並行して自分のテーブルを実行する必要がある場合は、異なる優先度を指定してください。そうすれば、順序を起動競合に任せず、設定として明示できます。
逆 NAT ルールを追加する必要がないのはなぜですか?
この質問は最も間違えられやすいため、先に直接答えます。コネクション追跡が逆方向の変換を自動的に記録します。2 つ目のルールを追加する必要はありません。
一般的な VPS の役割を両方向で処理する nat テーブルは、次のようになります。
table inet nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "enp1s0" tcp dport 8080 dnat ip to 10.0.0.5:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.0.0/24 oifname "enp1s0" masquerade
}
}接続の最初のパケットだけが nat チェーンに対して評価されます。ルールが一致すると、kernel はその変換を接続のエントリとともにコネクション追跡テーブルへ保存します。以後のパケットは両方向とも保存されたエントリに基づいて書き換えられ、ルールが再度読み込まれることはありません。conntrack ツールをインストールし、現在のエントリを確認してください。
sudo apt install -y conntrack
sudo conntrack -L -p tcptcp 6 431999 ESTABLISHED src=198.51.100.20 dst=203.0.113.10 sport=54321 dport=8080 src=10.0.0.5 dst=198.51.100.20 sport=80 dport=54321 [ASSURED] mark=0 use=12 つのタプルとして読み取ります。最初の 4 つのフィールドは、クライアントが送信した接続です。接続先は、公開アドレスである 203.0.113.10:8080 です。後半の 4 つは、kernel が想定する返信です。すでに逆方向へ並べ替えられ、変換済みで、実際のバックエンドである 10.0.0.5:80 から来る形になっています。この 2 つ目のタプルが、逆方向のルールそのものです。最初のパケットが一致した時点で、kernel がこれを記録しています。
したがって、戻り方向のルールは記述しないでください。戻りパケットは確立済みの接続に属し、nat チェーンには到達しないため、一致させることができません。仮に到達して一致したとしても、kernel がすでに処理したパケットを再度変換することになります。
同じ仕組みから、書き換えを配置すべき場所も決まります。宛先変換はルーティング決定の前に行う必要があるため、prerouting で実行します。ルーティングが新しい宛先を認識しなければ、パケットは誤った場所へ送られます。box 自身が生成するトラフィックは、同じ理由で output フックで処理します。送信元変換は、送信元ポートの書き換えを含め、ルーティングによって送信インターフェースが選択された後の postrouting で実行する必要があります。masquerade はそのインターフェースからアドレスを取得します。ルーティングが実行されるまで、インターフェースは確定しません。
そのため、次のようなルールはパスの最後に置きます。それ以外の場所には置きません。
ip saddr 10.0.0.0/24 oifname "enp1s0" snat ip to 203.0.113.10:20000-30000ポート範囲を指定すると、送信元アドレスとともに送信元ポートも書き換えられます。これは、多数の内部クライアントが 1 つの公開アドレスを共有し、送信元ポートが競合する場合に必要な動作です。返信はその範囲内のポート宛てに到着し、conntrack がエントリと照合します。その後、パケットが配信される前に元の送信元ポートへ戻されます。ここでも、2 つ目のルールは必要ありません。
実運用上の重要な点として、NAT ルールを変更しても、既存の接続は移動しません。変換がすでに保存されているためです。エントリの有効期限が切れるまで、既存の接続は古い動作を続けます。sudo conntrack -D -p tcp --dport 8080 は一致するエントリを削除し、sudo conntrack -F はすべてのエントリを削除します。NAT box では、後者の実行に注意してください。保存された変換によって現在の接続が維持されているため、すべての接続が一度に切断されます。
ufw と Docker はそれぞれ独自にルールを書き込みます
ufw は iptables のフロントエンドであり、Ubuntu では iptables が nftables のフロントエンドです。そのため、ufw を使用するホストには、ufw-before-input、ufw-user-input などの名前が付いたチェーンを多数持つ ip filter テーブルと、同じ構造の ip6 filter コピーがあります。sudo nft list ruleset | grep ufw で確認できます。これらのチェーンは /etc/ufw 内のファイルから生成され、ufw reload が毎回ゼロから書き直します。そのため、手動で追加した iptables ルールは次回の reload で消えます。VPS での ufw の基本では、このファイル構成を説明しています。
Docker はファイアウォールを自ら設定し、ufw の設定は参照しません。-p 80:80 でポートを公開すると、nat テーブルに DNAT ルールが書き込まれ、forward 処理に accept ルールが追加されます。どちらも ufw のユーザーチェーンより先に適用されます。その結果、誰もが一度は驚く事態になります。ufw deny 80 を読み込んだ後も、コンテナにインターネットから接続できます。対処には、Docker がユーザー定義ルール用に残している DOCKER-USER チェーンを使用します。Docker コンテナが ufw を無視する理由で手順を説明しています。ホスト上の内容は sudo nft list ruleset | grep -i docker で確認できます。
ここで、先ほどの設定にある flush ruleset の行を読み直してください。この行は、2 つのツールが管理するテーブルを含め、すべてのテーブルを削除します。Docker ホストでは、sudo systemctl restart docker がチェーンを再構築するまで、公開したポートが機能しなくなります。この 1 行が、ファイアウォールを整理する際に自分のサービスを停止させる最も一般的な原因です。
再起動後も維持されるルール
どちらのルールセットも、それだけでは永続化されません。シャットダウンするとカーネルはすべてを破棄するため、それぞれ別のパッケージで対処する必要があります。
nftables では、/etc/nftables.conf が nftables.service によって読み込まれます。Ubuntu ではこのサービスが無効になっているため、信頼する前に状態を確認してください。
systemctl is-enabled nftables
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftablesiptables では、パッケージは iptables-persistent です。これにより netfilter-persistent がインストールされ、/etc/iptables/rules.v4 と /etc/iptables/rules.v6 に保存されます。
sudo apt install -y iptables-persistent
sudo netfilter-persistent save両方を実行しないでください。ファイアウォールを保持すると主張するファイルが 2 つあると、内容に差異が生じます。どちらのファイルを見ても予測できない形で、最後に読み込まれた方が有効になります。
実行中のルールセットをダンプする場合にも、関連する落とし穴があります。sudo nft -s list ruleset > /etc/nftables.conf は、その時点で読み込まれているすべての内容を取得します。これには ufw のテーブルと Docker のテーブルも含まれます。これを起動時に復元すると、各ツールが自動生成するはずのルールの固定コピーが作成されます。その後、ツールの起動時に 2 つ目のコピーも作成されます。自分のテーブルだけを sudo nft -s list table inet filter でダンプしてください。-s フラグを付けると、設定ファイルに不要なカウンターを除外できます。
VPS で native firewall に切り替えるべきか
ufw で表現できない機能が必要でない限り、ufw はそのまま使用します。ufw は一般的な VPS の用途に対応しています。デフォルトで拒否し、いくつかのポートだけを開放する構成です。これを手書きのルールセットに置き換えても、得られるのは同じ firewall と、追加の保守対象だけです。
必要な機能が ufw のモデルの範囲外であれば、native に切り替えます。たとえば、NAT とポート転送、実行時に更新するセット、両方のアドレスファミリーを 1 つのルールで扱う機能、チェーンの優先順位を自分で指定する機能です。これらは明確な理由になります。ufw では、いずれも指定できません。
native に切り替える場合は、完全に native へ移行します。sudo ufw disable と sudo systemctl disable --now ufw を実行し、sudo nft list ruleset で ufw のテーブルが削除されたことを確認してから、自分のファイルを読み込みます。ufw と手書きのテーブルを同時に使用するホストでも、ネットワークトラフィックは通過します。しかし、実際に適用されるポリシーは 2 つのルールセットの和集合になります。評価順序はサービスの起動順序によって決まります。そのため、どちらか一方のファイルだけを読んでも、そのホストが実際にどのような動作をするかは判断できません。
既存の iptables ルールセットを移行する
iptables-translate は 1 つのルールを変換し、nftables 形式で出力します。サーバー上の設定は変更しません。
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPTnft add rule ip filter INPUT tcp dport 22 counter acceptiptables-restore-translate -f /etc/iptables/rules.v4 は保存済みのルールセット全体に対して同じ処理を行います。出力は最初の草案として扱ってください。変換は機械的にルール単位で行われるため、従来のテーブル名とチェーン名がそのまま残り、IPv4 と IPv6 用に 2 つの個別ルールセットが生成されます。また、移行の価値を高めるセットは含まれません。手作業で 1 つの inet テーブルに書き直し、稼働中のサーバーに適用する前に nft -c -f で確認してください。
これらの例のアドレスには、ドキュメント用の範囲である 203.0.113.0/24 と 198.51.100.0/24 を使用しています。enp1s0 はインターフェース名です。実際の環境では、私の例をそのままコピーせず、ip route show default と ip -br addr から自分の値を取得してください。現在の Ubuntu イメージでは、インターフェース名が eth0 になることはほとんどないためです。
FAQ
Ubuntu では iptables は非推奨ですか?
このコマンドは廃止されておらず、Ubuntu 24.04 でも引き続き動作します。変更されたのは内部の処理です。iptables は、iptables-nft バックエンドを通じて nftables ルールを書き込むフロントエンドです。iptables -V で現在の構成を確認できます。Ubuntu 24.04 では iptables v1.8.10 (nf_tables) と表示されます。従来の x_tables バックエンドも iptables-legacy として提供されており、完全に別のルールセットを保持します。そのため、ルールは一方のバックエンドだけに設定してください。
戻り方向の NAT を解除するために、別のルールが必要ですか?
いいえ。接続追跡は、接続の最初のパケットが nat ルールに一致した時点で変換内容を保存します。その後のすべてのパケットは、両方向とも保存されたエントリに基づいて書き換えられます。sudo conntrack -L では、1 つの接続について、元の方向と、すでに反転された応答方向の 2 つのタプルとして表示されます。戻り方向用のルールを記述しても役に立ちません。戻りパケットは nat chain に到達しないためです。
ufw と独自の nftables ルールを同時に実行できますか?
実行できますが、問題の原因になります。hook に接続されたすべての base chain が実行されるため、実際のポリシーは両方のルールセットを組み合わせたものになります。評価順序は priority に従い、priority が同じ場合は先に起動したサービスによって決まります。どちらか一方にある drop は最終的な判定となります。また、自分のルールセットにある accept は、もう一方のルールセットが同じパケットを drop することを止めません。使用するツールは 1 つに決めてください。nftables を使う場合は、先に ufw を無効化し、sudo nft list ruleset で ufw の table が削除されたことを確認します。
Ubuntu で nftables ルールを再起動後も維持するにはどうすればよいですか?
ルールセットを /etc/nftables.conf に配置し、sudo nft -c -f /etc/nftables.conf で確認してから、sudo systemctl enable --now nftables を実行します。このサービスはデフォルトで有効になっていないため、systemctl is-enabled nftables を 1 回実行しておくとよいでしょう。このファイルを生成するときは、sudo nft -s list table inet filter を使用して自分の table だけをダンプしてください。完全な list ruleset のダンプには、ufw と Docker がそれぞれ管理する table も含まれるためです。