前回に引き続き、新しいマシンの環境構築をしている。
以前のマシンではファイアウォールとして、以前は iptables を使用していたが、今回 nftables に移行してみる。元々 iptables 自体、かなり昔に書いた設定を使い回していて、設定方法はすっかり忘れてしまっていた。
あらかた設定できたので、その際のメモ。
ネットワーク環境
マシンが置かれているネットワークの環境は以下のような感じ。
- ルータの内側にいるサーバマシン
- なのでほとんどの通信はルータの時点でシャットアウトしてもらえる
- とは言え、可能な限りインターネットに晒されている前提の設定を目指す
- しかし IPv6 は今回は設定しない
- 将来的には Wi-Fi の AP にもしたい
- が、それはまたの機会に
- こちらを開くとルータ内部の前提が崩れるので IPv6 も含めてしっかりやらないといけなさそう
設定方針
nftables についての資料
あちこちのブログ記事等も参照したが、最終的には公式 Wiki が一番だった。
https://wiki.nftables.org/wiki-nftables/index.php/Main_Page
説明足らずな部分もあったけど、見付けられた資料の中では一番詳しかった。
コマンドライン
nft
コマンドを使う。
このコマンドで 1 つずつルールを足していくこともできるが、今回はルールファイルを用意してそのファイルを読み込ませる方法のみを使った。
設定中に使っていたのは以下のコマンドのみ。
コマンド | 説明 |
---|---|
nft -f {ファイル名} |
設定ファイルを読み込んで適用する。 |
nft list ruleset |
現在適用されている設定を表示する。 |
作った設定ファイルは最終的には /etc/nftables.conf
に配置し、systemctl start nftables.service
でロードできる。systemctl enable nftables.service
で起動時に設定を適用できる。
設定の書き方
特にここからは、個人的に理解したことをざっくりベースで書いていく。理解が間違っている可能性も大いにある。
nftables の ruleset は複数の table を持っており、table は複数の chain を持っており、chain は複数の rule を持っている。table と chain は名前を持っている。
設定の初期化
設定は nft -f
で読み込まれる度に追記される。それは困るので、ファイルの先頭に全ての設定を削除するコマンドを入れておく。
flush ruleset
設定ファイルを読み直す度に最初にルールが全て削除されるので、常にファイルに書かれた状態にできる。
なお、nftables はルールをアトミックに変更してくれるので、「flush した一瞬だけルールがない状態でパケットが処理される」ということはない。この辺りも iptables に対する強みの1つ。
table
table はプロトコルファミリーごとに作成する。
# IPv4 用
table ip filter {
}
filter
という名前の IPv4 プロトコル向けの table を作った。ファミリーは他に、ip6
(IPv6 向け) inet
(IPv4/IPv6 両方向け) 等々。詳細はココ。
この table のブロック { ... }
の中に chain を書いていく。
chain
chain は大きく分けて base chain と non-base chain がある。
base chain
base chain は処理の入口になる chain で、先頭に type <type> hook <hook> priority <value>;
のようなものがある。これはこの chain が処理するパケットの条件で、これにマッチするパケットがその chain で処理される。
table ip filter { chain input { type filter hook input priority filter; policy drop; # ... rules ... } }
input
と言う名前の chain を作った。なお table もそうだが、名前はあくまで識別のための名前であり、名前自体に意味はない。好きな名前で大丈夫。
type に指定できるのは filter
route
nat
、filter に指定できるのは prerouting
input
forward
output
postrouting
ingress
みたいな感じ。詳細は公式 Wiki の該当ページを参照。
入ってきたパケットは条件に合う chain を priority の小さい順に処理されていく。priority は数字で、一部の数字には別名がある。priority filter
は priority 0
と同じ。負数もある。同値の場合は処理順は不定。
non-base chain
non-base chain は base chain 以外のもの。先頭に条件がなければ non-base chain になる。jump や goto などのルールで他の chain から飛んでくる。飛んでこない限り chain は参照されない。
rule
chain の中には rule を順番に書いていく。上から順に処理される。
rule は matches
による条件の絞り込みと statements
によるパケットに対するアクションを好きな順序で並べていく。
# NEWだがSYNが立っていないパケットを破棄 tcp flags & (fin | syn | rst | ack) != syn ct state new counter log prefix "NEW not SYN:" drop ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ======= ========================= ==== + matches (TCP のフラグをチェック) | | | | matches (conntrack の状態が new) | | | statements (通過したパケット数をカウント) | | statements (log に残す) | statements (パケットを drop する)
個々の matches と statements の境界が文法上は特になく、わかってないとどこで切れているのかがわからないのでわかりづらい。
matches と statements はいくつでも書けるし、順番が混ざってもよい。左から順に処理される。例えば、
# 仮の文法 <stmt1> <match1> <stmt2> <match2> <stmt3>
みたいに書けば、まず stmt1
は常に実行され、次に match1
にマッチしたパケットには stmt2
が実行され、さらに match2
がマッチしたパケット(つまり match1
と match2
両方にマッチしたパケット)は stmt3
が実行される。
drop
や accept
などの一部の statements は Verdict statements と呼ばれていて、パケットの扱いが決まる。
この場合はものによって、chain での処理を中断して chain を抜けたりする。別の chain に飛ぶ、jump
(終わったら戻ってくる)や goto
(終わっても戻ってこない) も Verdict statements の 1 つ。
chain の最後まで行った場合、chain のポリシーによって扱いが決まる。base chain の先頭に書いてた policy drop;
みたいなやつ。これは accept
か drop
のどちらかで、書いてない場合はデフォルトで accept
になる。
個人的な設定
最後に現時点での個人的な設定を貼っておく。コメントが英語だったり日本語だったり、割と雑。どなたか、もしなにかまずそうな部分を見付けたら、こっそり教えてください…。
#!/usr/bin/nft -f flush ruleset define localnet = 192.168.1.0/24 table ip filter { set dos_counter { type ipv4_addr . inet_service flags dynamic } set block_targets { type ipv4_addr . inet_service flags timeout } chain input { type filter hook input priority filter; policy drop; # セッション確立後のパケットは許可 ct state established,related counter accept # early drop of invalid connections ct state invalid counter drop # allow from loopback iifname "lo" counter accept # SYN/ACKでNEWなパケットに対してRSTを送る tcp flags & (syn | ack) == syn | ack ct state new counter log prefix "SYN/ACK and NEW:" reject with tcp reset # NEWだがSYNが立っていないパケットを破棄 tcp flags & (fin | syn | rst | ack) != syn ct state new counter log prefix "NEW not SYN:" drop # allow from local network ip saddr $localnet ct state new tcp dport . ip protocol { 53 . tcp, 53 . udp, # DNS 123 . udp, # NTP 137 . udp, 138 . udp, 139 . tcp, # Windows 445 . tcp, # Active Dierctory 22 . tcp, # ssh 17500 . tcp, 17500 . udp, # Dropbox (LAN sync) } counter accept tcp dport 22 tcp flags & (fin | syn | rst | ack) == syn ct state new goto dos_detection ct state new tcp dport . ip protocol { 80 . tcp, 443 . tcp, # HTTP/HTTPS } counter accept # Ident(113)を拒否(DROPするとレスポンスが遅くなるのでReject) tcp dport 113 counter reject with tcp reset # ping icmp type { echo-reply, echo-request } counter accept } chain output { type filter hook output priority filter; policy accept; oifname "lo" counter accept } chain dos_detection { counter add @dos_counter { ip saddr . tcp dport limit rate over 10/minute burst 5 packets } counter jump add_block ip saddr . tcp dport @block_targets counter drop counter accept } chain add_block { # Logging only first time ip saddr . tcp dport != @block_targets log prefix "DOS Attacked:" update @block_targets { ip saddr . tcp dport timeout 30m } } }
ssh はここでは 22 と書いているが、冒頭でも書いた通り実際には別のポートに変えている。
同じく冒頭で書いた DOS アタックのブロックは dos_detection
chain と add_block
辺り。ただの rate limit ではなく、検出したら block_targets
に放り込んで 30 分ずっと拒否する。
この検出の部分に limit rate
を使ってみたのだけど、なんとなくそれっぽく動いているのだけど、実際にどういう処理になっているのかがいまいちよくわかっていない…。
余談: バグ
ところで、ポート番号とかを { ... }
で列挙している部分は Set というもので、これを使うことで1つずつルールを書くよりもテーブルジャンプができることで処理が効率化できるみたい。
が、この文法を複数行で書く場合は注意が必要で、文法上のバグがある。
tcp dport { 80, # ここにはコメントが書ける # ここにはコメントが書けない! 443, } counter accept
コメントと書いたが、実際に書けないのは空行である(コメントだけの行も空行扱い)。 すでにバグトラッカーには登録されているみたいだけど、地味に不便なので直ってほしい…。 (opt_newline が任意の個数の改行を処理してくれればよさそうではあるけど、あまり深追いしてない)