iptables から nftables へコンバート
Debian OSでは iptables から nftables への移行が進んでおり、バージョン11(bullseye)ではデフォルトで iptables コマンドを互換していましたが、12(bookworm)ではそれもなくなってしまったようです。
そこで今回は、Linuxにおける新たなパケットフィルタリングツール「nftables」入門などを参考に、nftコマンドを学習したいと思います。
ちなみに公式のマニュアルはnftables wikiにあります。
netfilter
まず、iptables についてのおさらいをすると、これはこれはカーネルに存在するネットワークのパケット処理とフィルタリングを行う機能である netfilterフレームワークをコントロールするためのインターフェースでした。
パケットフィルタリングや、NATといった機能は netfilter によるものす。少し前まではその定義づけを iptables によって行っていました。
現行のバージョンの Debian では iptables にかわり nftables がその役割を担っています。
nftablseは、iptables の欠点だった設定ファイルが膨らみやすいという点を改善しています。また、ip6tables、arp、ebなどあった xtables インターフェースをひとつに統合しています。
Linux用の firewall というと他には firewalld があげられると思いますが、これは nftables よりユーザーフレンドリーなフロントエンドとなっています。Debianではデフォルトでは採用されていませんが、より簡単に設定したいなら選択肢となると思います。firewalld は iptables や nftables の上に構築されています。ですので設定の柔軟性という点においては nftables の方に分があります。また、後で紹介しますが、nftables を使う場合は、iptables の設定(コマンド)を変換することができますので、とりあえず iptables から移行させたいという場合は、nftables を採用したほうが早そうです。
nftables
まず、状態の確認からです。
debian11では、次のように iptables のバージョンを確認することで、バックグラウンドが iptables なのかそれとも nftables なのかを確認することができました。
# iptables -V iptables v1.8.7 (nf_tables)
(nf_tables)の表記は、iptablesで発行したコマンドが nftables 用に変換されて運用されていることを示します。Debianにおいては10(buster)と11(bullseye)が、このような構成でした。12(bookworm)では、デフォルトでは iptables コマンドが利用できません。
ただし、標準的にインストールされていないだけで iptables を別途インストールする事で同様の状態とすることができます。
この時インストールされる iptables には、iptables-nft という nftコマンドに変換するコマンドと、 iptables-legacy という古いカーネル用に旧来のiptablesコマンドを伝えるふたつのコマンドが存在します。
ふたつの使い分けは内部的に設定されていて、通常 iptablesとコマンドを実行した場合は、前者のモードとなりますが、alternativesで変更することが可能です。
Debian で、iptables のコマンドが実行できない場合、代替である nftables がインストール済みだと思います。nftables でバージョンを確認するには nft -V とします。
# nft -V nftables v1.0.6 (Lester Gooch #5) cli: editline json: yes minigmp: no libxtables: yes
バージョンの後の出力は、 構成を示しています。テキスト編集時のクライアントライブラリ、JSONに対応しているか、算術演算(GNU Multiple Precision Arithmetic Library)があるかどうか、libxtablesの存在の有無を示していると思います。
また、nftables の利用にはカーネルモジュールが必要です。この状態の確認をするには lsmodを使います。
lsmod はカーネルのモジュール状態を表示するプログラムで、/proc/modulesの内容を整形して表示します。
どんなか―ネールモジュールがロードされているかを示すもので、ip_tables の環境では、ip_tables や x_tables というモジュールがロードされています。
$ lsmod | grep nf_tables nf_tables 299008 0 nfnetlink 20480 1 nf_tables libcrc32c 16384 1 nf_tables
出力の中に nf_tables の表記がコマンドで表示されない場合はロードされていません。
そのような場合は、まずモジュールが存在するか確認します。
存在したら、modprobeでロードします。
モジュールを、システム起動時に自動でロードするようにするには、先の modprobe コマンドを起動時に実行する方法の他、次のような方法があります。
- initramfs に組み込む
initramfsを使って、初期イメージに nftables モジュールを組み込むには /etc/initramfs-tools/modlues にある initramfsイメージ作成時の追加設定用のファイルに、モジュール名( nf_tables )を追加します。
ここでは利用しませんが、モジュール名に続けてオプションを渡すことも可能です。
; initramfsの設定ファイルに nf_tables(nf_tables.ko)を追加 # echo "nf_tables" | tee -a /etc/initramfs-tools/modules nf_tables ; initramfsを更新 # update-initramfs -u ; 再起動 # systemctl reboot
- /etc/modules-load.d/に記述
/etc/modules-load.d/ に設定する方法もあります。
ここに設定を拡張子を .conf にした任意のファイルを配置して、その中にモジュール名を記述します( nf_tables )。先ほどと同様にオプションも渡すことができます。筆者の環境ではこの中に modules.conf という名前で /etc/modulesへシンボリックリンクが貼られていました。ですので、/etc/modules ファイルに記述しても同様の結果が得られます。
逆に、ip_tables モジュールを読み込みたくないという場合は、/etc/modprobe.dに 任意の .conf ファイルを置くことで可能です。
そのファイル内に blacklist という接頭を使うことで、読み込まないモジュールの指定ができます。
次の設定は例示の為に紹介しますが、Debianの apt でインストールする nftables は先のバージョン表記にあったように libxtables yes の構成になっているので、ip_tables の読み込みを止めると問題がおきると思われます。
/etc/modprobe.d/sample.conf
blacklist ip_tablesちなみに、先のファイルに options という接頭をつけた記述をすれば、指定したモジュールを読み込む際の追加のオプションを指定しておくこともできます。
modprobe.dディレクトリは modprobe(カーネルモジュールの追加・削除を行うコマンド)の設定ディレクトリという位置づけです。ただ、ディストリビューションによっては機能しない場合もあるようです。
追記
近年のDebianでは、nf_table や関連する nf_conntrack をモジュールを明示してロードしなくても、カーネルに組み込まれていたり、自動でロードするようになっているようです。
nftablesの構造
nftables は複数の「テーブル」を持ち、その中に「チェーン」というフィルタリングルールのまとまりを入れる領域を作り、ルールを設定するという構造となっています。
それらの全体を「ルールセット」と呼びます。
従来 iptables、ip6tablesとしてコマンド毎に区別されていたプロトコルが、「アドレスファミリ」という概念になり統合されています。
テーブル毎に指定する アドレスファミリに、ip を指定すれば IPv4 を対象に、ip6を指定すれば、IPv6 を対象にすることができます。また、inetとすることで、IPv4とv6の両方を対象とすることもできます。さらに、netdiv というネットワークデバイスに入ってきたすべてのネットワークトラフィックを対象にできるものもあります。他、arp、bridge もあります。
どの時点でフィルタリングを行うか(input や output, forward 等) というのは「フック」(netfilter のそれぞれの段階からフックして呼び出されるのでそう呼ばれています)という概念になり、任意に名付けたチェーンに設定するようになりました。チェーンに設定できるフックはアドレスファミリ毎に設定可能な値が変わります。
iptables では INPUT、OUTPUT、FORWARDチェーンがデフォルトで存在しましたが、nftablesではそれらは存在しませんので、ユーザーがチェーンとフックを設定したかたちでそれらを作成するようになります。
また、「タイプ」として、[ filter :フィルタリング ]、[ nat :アドレス変換]、[ route : 転送 ]を指定するようになっており、このあたりも iptablesと異なる点となります。
nftコマンド
nftables の設定は、 nft コマンドを通して行います。設定は最終的にはテキストファイルに変換が可能なため、テキストファイルを編集して先の -fオプションで読み込むことも可能ですが、iptalbes より複雑な構造になるため 慣れないうちはコマンドから操作した方が安全だと思います。
nft
nft [ -nNscaeSupyjtT ] [ -I <ディレクトリ> ] [ -f <ファイル名>| -i | cmd ...]
この中で、よく使う書式は、「 - f <ファイル名> 」として、エクスポート済みの設定ファイルを読み込むコマンドと、下線部の「 nft コマンド パラメータ 」としてファイアウォールを構築していくコマンドになります。
「 nft -f ファイル名 」とすることで、ルールファイルの読み込みをします。このフォーマットは、先の list の出力と同じなので、先のコマンドをリダイレクトして生成したファイルからルールセットを復元することができます。
-fで読み込む場合には、すべての中身が読み込まれた後一度に反映されるようになっています。iptables でファイルを読み込む際には段階的に反映されていました。
コマンドを設定する場合は、最初にコマンドのタイプを指定して、その後に対象、具体的なコマンドと続きます。例えば ruleset(全体)に対して listを実行する場合は次のようになります。
適用されたルールを削除する場合は、flushコマンドを使います。
対話モードも用意されており nft -i で入れます。このモードでは設定毎の nft の入力が省けます。対話モードから抜けるにはCTRL+D(EOFシグナル)を利用します。
ポリシーの設定
実際に、設定をするまえに事前準備をします。ここではIPv6を利用しないことを前提とするファイアウォールを構築しますので、練習としてIPv6でポリシー dorp のチェ―ンを作成します。その後IPv6のフィルターに設定作業用の例外設定を入れます。
リモート環境の設定をする際は、設定作業を始めるまえにどのように接続状態を確保しておくかのプランが必要になります。ここでは、IPv4とv6どちらでも接続可能で同一ネットワークにいる状態を前提に、次のような方法でリモート作業でのフィルター作成時の接続状態を維持します。実際に自身の接続環境に応じた設定にしておかないと接続が失われ、再接続できなくなりますので注意してください。
-
フィルター無しの状態でIPv4から接続します。
-
IPv6のフィルターの設定をしリンクローカルアドレス(ローカルネットワーク)からのSSH(TCP:22)を許可設定を入れます。
(同一ネットワークにいない時は、グローバルなIPv6アドレスで例外設定を入れて下さい)
-
以降はSSHをIPv6で利用し、IPv4の設定をします。
リンクローカルアドレスを使うので接続時にはスコープIDも必要になります。
補足: IPv6におけるスコープID(ゾーンID)
IPv6のリンクローカルアドレスを使う場合、スコープID(ゾーンID)の概念が必要になります。
リンクローカルアドレスはfe80::/10プレフィックスのついたローカルエリアのみ有効なアドレスですが、複数のNICがあるPCから特定のアドレスに向けた通信を行おうとした場合どちらのNICから送信すればいいかそのままでは判断できません。
NIC-Aに付与されているプレフィックスも、NIC-Bに付与されているプレフィックスも同じなので、宛先アドレスから送信元のNICを特定できない為です。
LinuxではPingの -I オプションで送信元のNICを指定できますが、Windowsではその機能がありません。
その際に用いられるのが、スコープIDです。送信元のNICを指定するもので、Windowsでは ipconfig を実行すると出てきます。
cmd> ipconfig ... リンクローカル IPv6 アドレス. . . . .: fe80::29ee:b71e:4226:70a1%12 ...
この%12の部分がスコープIDです。ipconfigで表示させた時は自身のIPに対して付与されていますが、自身のNICを指定するためのものなので、宛先においても同じ値を使います。
cmd> ping <宛先のIPv6アドレス>%12LinuxにおけるスコープIDは、eth0 や ens32 などの値がそのまま使われるようです。
# スコープIDを使う場合 $ ping <宛先のIPv6アドレス>%ens32 # オプションで指定する場合 $ ping -I ens32 <宛先のIPv6アドレス>
どのNICからデータを送信すればいいかわからなくなるのはリンクローカルアドレスに限ったことなので、このスコープIDが使われるのはリンクローカルアドレスを使った通信の時だけです。
IPv6側の設定を始めます。
まず、念のためすべての設定をクリアして、listになにも出力されないことを確認します。
# nft flush ruleset # nft list ruleset
nftables ではデフォルトのテーブルやチェーンが存在しないため、まずテーブルを作成しないといけません。
書式
# nft [ create | add ] table [<アドレスファミリ>] <テーブル名>アドレスファミリに ip6(IPv6)を指定して次のようにテーブルを作成します。名前は [ ipv6table ]としました。
テーブルの作成では、createまたはaddを使えます。createは設定が既存の場合エラーとなるだけであとはaddと同じです。
アドレスファミリを省略すると IPv4(ip)となりますので注意してください。
間違えて作成してしまった場合は、delete コマンドで削除します。削除時もアドレスファミリ―の指定は必要で、こちらも省略するとIPv4(ip)になります。(以下多くのコマンドで同様です)
また nft -a list ruleset の結果などから割り当てられているハンドルが判明している場合は、ハンドルを指定しての削除もできます。
書式
# nft delete table [<アドレスファミリ>] <テーブル名> # nft delete table [<アドレスファミリ>] handle<ハンドル>どのようなテーブルを作成したかわからなくなってしまった場合は、ruleset を対象にリストを出力してもいいですが、テーブルのみの確認も可能です。
書式
# nft list tables [<アドレスファミリ>]つぎにチェーンを作成します。書式は次の通りです。
書式
{ create | add } chain [<アドレスファミリ>] <テーブル名> <チェーン名> [<ベースチェーンのパラメータ>] (delete | list | flush) chain [<アドレスファミリ>] <テーブル名> <チェーン名> list chains [<アドレスファミリ>] delete chain [<アドレスファミリ>] <テーブル名> handle <ハンドル> rename chain [<アドレスファミリ>] <テーブル名> <現チェーン名> <新チェーン名>
チェーンには、input や output といったnetfilterから直接呼び出される「ベースチェーン」と、事前に定義しておきベースチェーンから呼び出して使う「レギュラーチェーン」があります。
ベースチェーンには次のパラメータを付与します。※印のついたものは必須です。
- type ※
チェーンのタイプです、先に紹介したように、filter、nat、route がありますが、ここでは filter にします。
- フック ※
フックに input、output、forward を指定した3種のチェーンを作ることで、iptablesの初期状態と同じになります。
- デバイス
任意の項目で、デバイスの条件を指定することができます。指定のない時はすべてが対象となります。
- プライオリティ ※
同じフックが指定されたチェーンが複数あった場合の優先順位を指定します。値の小さなものが優先度が高いですが、filter(ファイアウォールとして)で同じフックを持った複数のチェーンを持つことはバックアップ的なものを除けばないと思います。
- ポリシー
iptablesの際と同様です。チェーン内で処遇が決まらなかった場合の挙動で accept か dropを指定します。指定しない場合は acceptとなります。
間違えやすいのは、{} で囲んだ中の type と hook の後には;を付けないのに対して、priority と policyの後には必要だという点です。
また、通常のシェル上では;がコマンドの終わりと認識されてしまうので、エスケープする必要もあります。
nft -i で nftシェルを起動させると、先頭の nftや;のエスケープが不要なので、少し楽になります。
# nft add chain ip6 ipv6table input_chain { type filter hook input priority 1 \; policy drop \; } # nft add chain ip6 ipv6table output_chain { type filter hook output priority 1 \; policy drop \; } # nft add chain ip6 ipv6table forward_chain { type filter hook forward priority 1 \; policy drop \; }
table 同様に チェ―ンにも delete が用意されています。これは中にルールがある時は削除できません。中のルールをすべて削除するには flushを使います。
こうして設定したものを出力すると次のように表示されました。
chain output_chain { type filter hook output priority filter + 1; policy drop; }
この filterは standard priority names と言って、ファミリとフック毎に設定されている定数のようなものです。ここでの filter の値は0なので 1=fileter+1という表記になっているようです。
fileterの他、mangle(-150)やsecurity(50)といったものがあります。
先の例ではプライオリティに1を設定しましたが、通常は0でいいようです。またマイナスの値も設定可能です。
ルールの追加の詳細は後述しますが、設定入力時にリモート接続が切れないようにIPv6接続許可を指定します。
この設定はすべての設定を終えたらこの項目は削除する前提です。
#nft add rule ip6 ipv6table input_chain ip6 saddr fe80::/32 tcp dport 22 accept #nft add rule ip6 ipv6table output_chain ip6 daddr fe80::/32 accept
IPv4
次にIPv4の設定をしていきます。接続が失われないように、IPv6からの接続に切り替えます。
さくらのナレッジにある、iptablesで設定してある次のようなVPS用のファイアウォールの設定を、nftablesに置き換えてみたいと思います。
https://knowledge.sakura.ad.jp/4048/ より抜粋
# (1) ポリシーの設定 OUTPUTのみACCEPTにする *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] # (2) ループバック(自分自身からの通信)を許可する -A INPUT -i lo -j ACCEPT # (3) データを持たないパケットの接続を破棄する -A INPUT -p tcp --tcp-flags ALL NONE -j DROP # (4) SYNflood攻撃と思われる接続を破棄する -A INPUT -p tcp ! --syn -m state --state NEW -j DROP # (5) ステルススキャンと思われる接続を破棄する -A INPUT -p tcp --tcp-flags ALL ALL -j DROP # (6) icmp(ping)の設定 # hashlimitを使う # -m hashlimit hashlimitモジュールを使用する # —hashlimit-name t_icmp 記録するファイル名 # —hashlimit 1/m リミット時には1分間に1パケットを上限とする # —hashlimit-burst 10 規定時間内に10パケット受信すればリミットを有効にする # —hashlimit-mode srcip ソースIPを元にアクセスを制限する # —hashlimit-htable-expire 120000 リミットの有効期間。単位はms -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-name t_icmp --hashlimit 1/m --hashlimit-burst 10 --hashlimit-mode srcip --hashlimit-htable-expire 120000 -j ACCEPT # (7) 確立済みの通信は、ポート番号に関係なく許可する -A INPUT -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT # (8) 任意へのDNSアクセスの戻りパケットを受け付ける -A INPUT -p udp --sport 53 -j ACCEPT # (9) SSHを許可する設定 # hashlimitを使う # -m hashlimit hashlimitモジュールを使用する # —hashlimit-name t_sshd 記録するファイル名 # —hashlimit 1/m リミット時には1分間に1パケットを上限とする # —hashlimit-burst 10 規定時間内に10パケット受信すればリミットを有効にする # —hashlimit-mode srcip ソースIPを元にアクセスを制限する # —hashlimit-htable-expire 120000 リミットの有効期間。単位はms -A INPUT -p tcp -m state --syn --state NEW --dport 22 -m hashlimit --hashlimit-name t_sshd --hashlimit 1/m --hashlimit-burst 10 --hashlimit-mode srcip --hashlimit-htable-expire 120000 -j ACCEPT # (10) 個別に許可するプロトコルとポートをここに書き込む。 # この例では、HTTP(TCP 80)とHTTPS(TCP 443)を許可している。 -A INPUT -p tcp --dport 80 -j ACCEPT -A INPUT -p tcp --dport 443 -j ACCEPT COMMIT
同様にIPv4用のテーブルとチェーンを作成します。IPv4の場合は output の policyは acceptにしておきます。
# nft create table ip ipv4table # nft add chain ip ipv4table input_chain { type filter hook input priority 0 \; policy drop \; } # nft add chain ip ipv4table output_chain { type filter hook output priority 0 \; policy accept \; } # nft add chain ip ipv4table forward_chain { type filter hook forward priority 0 \; policy drop \; }
ルールの設定
テーブルとチェーンを作成したら、チェーンにルールを追加していきます。
書式
{ add | insert } rule [<アドレスファミリ>] <テーブル名> <チェーン名> [handle <ハンドル> | index <インデックス>] <ステートメント> [comment <コメント>] replace rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル> <ステートメント> [comment <コメント>] delete rule [<アドレスファミリ>] <テーブル名> <チェーン名> handle <ハンドル>
add は既存のルールの最後に追加、isnert は最初に追加します。
ステートメントの部分に、ルールを指定していきます。
先に提示した iptables のコマンドを1行ずつコンバートしていきます。
共通するコマンド部分を変数 $cmdに格納します。
- (2)
ローカルホストからの接続を許可します。入りインターフェースを指定する場合には 「 iifname(Input InterFace NAME) 」を使います。
Debian(Linux)ではローカルホストのインターフェースに lo という名前がついているのでこれを基準に許可ルールを作成します。
# $cmd iifname "lo" accept - (3)
有効なTCPフラグがセットされていない場合は破棄します。
# $cmd tcp flags ! fin,syn,rst,psh,ack,urg droptcp flagsで TCPヘッダ内のフラグフィールドを検査する指示です。! は否定を示し、そのあとのfin,syn,rst,psh,ack,urgはTCPフラグの全種を列挙しています。
全体で、ヘッダにフラグがついていない不正なパケットは drop ということになります。
- (4)
新規セッションなのに、synフラグがついていない不正なパケットを drop します。
# $cmd tcp flags != syn / fin,syn,rst,ack ct state new drop - (5)
すべてのフラグが立っている不正なパケットを drop します。
# $cmd tcp flags fin,syn,rst,psh,ack,urg / fin,syn,rst,psh,ack,urg drop - (6)
ping応答の時間単位の上限設定
# $cmd icmp type echo-request meter t_icmp { ip saddr timeout 120s limit rate 1/minute burst 10 packets } accept - (7)
確立済みの通信は許可
# $cmd ip protocol tcp ct state related,established accept - (8)
DNS問い合わせの戻りパケットの受け入れ
# $cmd udp sport 53 accept参考ページにあった設定をそのまま変換するとこ先の通りですが、UDPの53番ポートからくる通信をすべて accept とするのは若干問題のある設定だと思います。次のようにして利用するDSNサーバーのアドレスを登録しておくのがベターだと思います。この時プライマリとセカンダリがある場合は{}で囲んで,で区切って指定します。
# $cmd ip saddr <DNSサーバーのアドレス> udp sport 53 accept - (9)
SSH接続の試行階層上限の設定
# $cmd ip ct state new tcp dport 22 tcp flags syn / fin,syn,rst,ack meter t_sshd { ip saddr timeout 120s limit rate 1/minute burst 10 packets } accept - (10)
その他の設定です。サンプルとして、Webサーバー用のポート(80,443)を設定します。
# $cmd tcp dport 80 accept # $cmd tcp dport 443 accept
変換ツール
iptables-restore-translate -f <iptables-saveで出力されるデータ> とすることで、iptablesの設定をnftablesの設定に一括変換することができます。
これをファイルに保存して、nft -f <出力ファイル> として nftable に反映させます。
IPv6ではコマンドが ipv6tables-restore-translate となります。
# iptables-save -f iptables-output # iptables-restore-transrate -f iptables-output>conv-nftabes # nft -f conv-nftables
先にも書いたように、nftables は iptables を互換させて稼働していた実績もあり、多くの場合で有効な値を出力すると思います。
また、一括で変更するのが不安な場合は iptables-tranlate のあとに iptablesのコマンドを入力する事で、nft コマンドに変換した値を表示してくれます。
# iptables-translate -A INPUT -p tcp --tcp-flags ALL NONE -j DROP nft 'add rule ip filter INPUT tcp flags 0x0 / fin,syn,rst,psh,ack,urg drop'
ただ、これらの変換では、テーブル名が 「 filter 」、チェーン名が iptablesにおけるチェーン名(INPUT,OUTPUT,FORWARD)となりますので、必要に応じて変換する必要があります。
設定の永続化
nftables も iptables がそうであったようにに電源を遮断するとすべての設定が消えてしまいます。iptablesでは iptables-persistent を用いましたが、nftables では専用のサービスが用意されています。
systemctl enable nftables とすることで、サービスが有効になり、起動時に /etc/nftables.conf 内のルールセットが適用されますので、サービスを有効にしてルールセットを作成して、/etc/nftablees.conf に設定しておきます。
# systemctl enable nftables # nft list ruleset >nft-ruleset.txt # cp nft-ruleset.txt /etc/nftables.conf # systemctl reboot