UFWでルーターを作って動かしているのだが、Ubuntuは22.04の時点ではnftablesに移行していたとのこと。
iptablesでも操作はできるが、nftableに変換されているそうなので、それならネイティブになろうという話。
ルーターを組んでみる
ホームラボのルーターをnftablesに移行する。
今回もGeminiさんとCopilotさんに相談しながら進めていく。
ホームラボはこのルーターを1台動かせば、DHCPでIPアドレスが割り付けられ、DNSも提供され、Giteaで開発用のリポジトリサービスなんかも提供するようにしてある。
サービスはDockerで動かしているので、これとの共存が必須の条件。
構成
ネットワーク構成はこのようになっていて、今回は router(Lab) をnftablesで設定する。
<Internet> - [router] - <家庭用ネットワーク/WAN> - [router(Lab)] - <ホームラボネットワーク/LAN>
| 項目 | 値 | 備考 |
|---|---|---|
| OS | Ubuntu 24.04 server | アップグレードを繰り返した環境 |
| ens33 | ホームラボネットワークと接続 192.168.110.10/24, fdaa:aaaa:aaaa:aaaa::10/64 | LAN扱い |
| ens37 | 家庭用ネットワークと接続 <IPアドレスはマスク> | 今回はWAN扱い、インターネットに出られる |
UFWを無効にする
UFWでファイアウォールとルーターの設定をしているが、nftablesに切り替えるので、止めてしまう。
$ sudo ufw disable
$ sudo systemctl disable ufw.service
nftablesを有効にする
/etc/nftables.confは何も触っていない状態だと、何でもacceptなので、とりあえずnftablesを有効にして起動みる。
$ sudo systemctl enable nftables
Created symlink '/etc/systemd/system/sysinit.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.
$ sudo systemctl start nftables
ルールセットを確認してみる。
$ sudo nft list ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy accept;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
※まっさら環境で表示させたのでルールはこれだけ。運用中の環境だと、他のルールが表示されているかもしれない。
ルールが適用されていて、すべてacceptになっていることが確認できた。
ルールセットの定義
色々と整理した結果がこちら。
1行で書くこともできるのに、コメントを入れたくてガンガン改行したので、内容を捉えにくいが…
- NICは別の環境で似たようなものを動かす場合に備えて、WANとLANをdefineした。
- テーブルは2つ、inetファミリー(IPv4とIPv6の両対応)とした。
- filter:LANに提供するサービスのポートを開ける+転送許可
- nat:LANとWANの転送でアドレスを書き換える
といった具合で、感覚的にはUFWコマンドをバシバシ叩き、定義ファイルを幾つもいじって設定するより、このファイル1つで設定する方が難易度は低いように思った。
/etc/nftables.conf
#!/usr/sbin/nft -f
#flush ruleset
flush ruleset inet
define NIC_LAN = "ens33"
define NIC_WAN = "ens37"
table inet filter {
# 内部向けに開放するポート(TCP)
set lan_allow_tcp {
type inet_service
flags interval
elements = {
22, # SSH
25, # SMTP Mail Server
53, # DNS
80, # HTTP
443, # HTTPS
587, # SMTP Submission
873, # rsync
111, # NFS rpcbind
2049, # NFS Server
389, # LDAP
636, # LDAPS
88, # Kerberos Authentication
464, # Kerberos kpasswd
135, # Microsoft RPC Endpoint Mapper
49152-49200, # Windows RPC Dynamic Ports
139, # NetBIOS Session Service
445, # Microsoft-DS(SMB over TCP)
3268, # Active Directory Global Catalog
3269, # Active Directory Global Catalog SSL
3000, # Gitea
}
}
# 内部向けに開放するポート(UDP)
set lan_allow_udp {
type inet_service
elements = {
53, # DNS
123, # NTP
389, # LDAP
546, # DHCPv6(dhcpv6-client)
547, # DHCPv6(dhcpv6-server)
88, # Kerberos
464, # Kerberos kpasswd
111, # NFS rpcbind
2049, # NFS Server
137, # NetBIOS Name Service(WINS)
138, # NetBIOS Datagram Service
}
}
# 外部向けに開放するポート(TCP)
set wan_allow_tcp {
type inet_service
flags interval
elements = {
22, # SSH
25, # SMTP Mail Server
53, # DNS
80, # HTTP
443, # HTTPS
587, # SMTP Submission
}
}
# 外部向けに開放するポート(UDP)
set wan_allow_udp {
type inet_service
flags interval
elements = {
53, # DNS
546, # DHCPv6(dhcpv6-client)
}
}
# 内部から外部への転送を許可するポート(TCP)
set fwd_allow_tcp {
type inet_service
flags interval
elements = {
22, # SSH
53, # DNS
80, # HTTP
443, # HTTPS
}
}
# 内部から外部への転送を許可するポート(UDP)
set fwd_allow_udp {
type inet_service
flags interval
elements = {
53, # DNS
123, # NTP
443, # HTTP/3(QUIC)
}
}
# # 内部から外部への転送を拒否するポート(TCP)
# set fwd_block_tcp {
# type inet_service
# flags interval
# elements = {
# 135, # Microsoft RPC Endpoint Mapper
# 139, # NetBIOS Session Service
# 445, # Microsoft-DS(SMB over TCP)
# }
# }
# # 内部から外部への転送を拒否するポート(UDP)
# set fwd_block_udp {
# type inet_service
# flags interval
# elements = {
# 137, # NetBIOS Name Service(WINS)
# 138, # NetBIOS Datagram Service
# }
# }
# このルーターが受け取るパケットを判定して処理するチェーン
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
# 不正パケットの破棄
ct state invalid jump logging_invalid
# 確立済みの通信を許可
ct state established,related accept
# ICMPの許可
ip protocol icmp accept
meta nfproto ipv6 icmpv6 type {
destination-unreachable, packet-too-big, time-exceeded, parameter-problem,
echo-reply, echo-request,
nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert,
148, 149
} accept
ip6 saddr fe80::/10 icmpv6 type {
130, 131, 132, 143, 151, 152, 153
} accept
# 接続を許可
iif $NIC_LAN tcp dport @lan_allow_tcp ct state new accept
iif $NIC_LAN udp dport @lan_allow_udp ct state new accept
iif $NIC_WAN tcp dport @wan_allow_tcp ct state new accept
iif $NIC_WAN udp dport @wan_allow_udp ct state new accept
# 外部で降っている「雨」は無視して、内部やDockerコンテナからの異常のみログ出力
iif != $NIC_WAN jump logging_input_block
}
# 転送を処理するチェーン
chain forward {
type filter hook forward priority filter; policy drop;
# 確立済みの通信を許可
ct state established,related accept
# 内部から外部への接続を許可
iif $NIC_LAN oif $NIC_WAN tcp dport @fwd_allow_tcp ct state new accept
iif $NIC_LAN oif $NIC_WAN udp dport @fwd_allow_udp ct state new accept
jump logging_forward_block
}
# chain forward {
# type filter hook forward priority filter; policy drop;
#
# # 内部から外部への転送を拒否
# iif $NIC_LAN oif $NIC_WAN tcp dport @fwd_block_tcp jump logging_forward_block
# iif $NIC_LAN oif $NIC_WAN udp dport @fwd_block_udp jump logging_forward_block
#
# # 確立済みの通信を許可
# ct state established,related accept
#
# # 内部から外部への接続を許可
# iif $NIC_LAN oif $NIC_WAN ct state new accept
# }
# 許可されない接続をログ出力するチェーン
chain logging_input_block {
limit rate 3/minute burst 10 packets log prefix "[NFT INP-BLK] "
drop
}
# 許可されない転送をログ出力するチェーン
chain logging_forward_block {
limit rate 3/minute burst 10 packets log prefix "[NFT FWD-BLK] "
drop
}
# 不正なパケットをログ出力するチェーン
chain logging_invalid {
limit rate 3/minute burst 10 packets log prefix "[NFT INVALID] "
drop
}
}
table inet nat {
# 外部ネットワークに出て行くパケットの発信元IPアドレスを書き換えるチェーン
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oif $NIC_WAN ip saddr 192.168.110.0/24 masquerade
oif $NIC_WAN ip6 saddr fdaa:aaaa:aaaa:aaaa::/64 masquerade
}
}
ちなみに、IPアドレスを固定しているなら、masquerade よりも snat to <固定されたIPアドレス> と書いた方が速いとのこと。
ファイルを書き換えたら、設定ファイルをチェックして反映。
$ sudo nft -c -f /etc/nftables.conf
$ sudo systemctl reload nftables
転送できるようにする
ここまでのところで、2枚のNICの間の転送はできるようになっているが、ルーターとしては動作しない。
転送を許可する。
/etc/sysctl.d/90-override.conf ※新規作成
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
反映。
$ sudo sysctl --system
これで、サービスを提供し、ルーターとして動作するようになった。
Dockerとの共存
nftablesで操作するにあたって、その挙動が安定するまでは、Dockerのコンテナーは止めておいた方が無難だろうと思う。
しかし、今回はラボのサーバーだし、いざとなりゃどうにでもなるということで、気にせずに操作。
結果ルールが消えたりしたが、
$ systemctl restart docker
でルールが復活するので、どうにかなった。
Dockerが生成するルール
Dockerが生成するルールのアドレスファミリーと名前は以下。
- ip nat
- ip filter
- ip6 nat
- ip6 filter
- ip raw
幸い、今回作成したルールセットはinetアドレスファミリーを利用している。
nftablesでの操作をinetアドレスファミリーに限定すれば、Dockerのルールで気にするのは優先度くらいのもの。
nftables.service停止時の動作を変更
nftables.serviceが停止するとき、ルールをすべてフラッシュする動作が書かれている。
これだと、Dockerが生成したルールも含めすべてのルールが消えてしまう。
サービスを停止したときに、今回作成したルールセットだけを削除するファイルを作成。
/etc/nftables_flush.conf ※新規作成
#!/usr/sbin/nft -f
#flush ruleset
flush ruleset inet
サービス停止時の動作を変更するため、赤文字部分を追記して保存する。
$ sudo systemctl edit nftables.service
### Editing /etc/systemd/system/nftables.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file
[Service]
ExecStop=
ExecStop=/usr/sbin/nft -f /etc/nftables_flush.conf
### Edits below this comment will be discarded
### /usr/lib/systemd/system/nftables.service
# [Unit]
# Description=nftables
# Documentation=man:nft(8) http://wiki.nftables.org
# Wants=network-pre.target
# Before=network-pre.target shutdown.target
# Conflicts=shutdown.target
# DefaultDependencies=no
#
# [Service]
# Type=oneshot
# RemainAfterExit=yes
# StandardInput=null
# ProtectSystem=full
# ProtectHome=true
# ExecStart=/usr/sbin/nft -f /etc/nftables.conf
# ExecReload=/usr/sbin/nft -f /etc/nftables.conf
# ExecStop=/usr/sbin/nft flush ruleset
#
# [Install]
# WantedBy=sysinit.target
元の設定を見ると、ExecStopはルールをすべて消してしまうようになっている。
なので、元々の設定をクリアし(=迄の行)、作成したinetファミリーだけを削除するコマンドを追加している。
テスト
先程作ったルールはもう動いてしまっている。
サービスを停止させ、作成したルールだけが消えて、Dockerのルールが残っていることを確かめた。
$ sudo systemctl stop nftables
$ sudo nft list ruleset
問題なければ再びルールを有効化させる。
$ sudo systemctl start nftables
もし、ルールが消えちゃったら、何か設定が間違っているのでやり直し。
Dockerのルールはこれで再生成される。
$ sudo systemctl restart docker
ということで、これでDockerと共存できそうだ。
ログ
UFWはログを大量に吐くので、なんとなく止めてくれてるなーという感じがして安心だけれど、
- サービスをテストしているときに、ポートを開け忘れてブロックしちゃってることを知る
- ポートスキャンされてることを知る
程度しか使い道がない。
それどころか、本来受け入れるべきパケットをDropしてしまっている、といった大切な情報はログの海に紛れてしまい、見落としがち。
世界は「価値あるログ」のみを記録する方向だそう。
Geminiさんに相談しながら整理した。
空いているポートへの不正なパケット
開いているポートに不正なパケットを送ることで、以下のような攻撃が考えられるとのこと。
- ステルススキャン:セキュリティの甘い装置を騙してパケットを通過させる
- DoS攻撃:中途半端なパケットを大量に送りつけて無駄な処理をさせ、機能不全にさせる
- 強制切断、セッションハイジャック
ct state invalid jump logging_invalid
これでログ出力をしている。
ただし、このログは意図せず出力されるケースもある模様。
たとえば、Wi-Fiが不安定で切断→復旧した端末から、継続のパケットが送られてきたときに、ルーター側からみたら不正に見える等。
つまり、1発で攻撃と判断するのではなく、急激に不正なパケットが増えたときに攻撃と判断する、といった運用になる。
外部からの要求
インシデントが発生したとき、どのデーターがとられたのかを確定する必要がある。
ログはより源流に近いところでとるのが正しいということで、基本的には各サービスでログをとる方針。
ただし、ログがとれない古い機器やアプリケーション、家庭用の複合機なんかがあったとして、それが守るべき情報を持っているなら、このルーターでアクセスログをとる。
ログから特定できることは限られているが、情報が全くないよりはマシ、といったところ。
今回はそういう装置がなかったので実装していないが、検討過程で作り込んだルールはこちら。
※実際にはnftablesに読み込ませていないので、もしかしたら文法的に修正が必要だったり、動かなかったりするかもしれない。
table inet filter {
...
}
chain forward {
type filter hook forward priority filter; policy drop;
# 内部から外部への転送を拒否
iif $NIC_LAN oif $NIC_WAN tcp dport @fwd_block_tcp drop;
iif $NIC_LAN oif $NIC_WAN udp dport @fwd_block_udp drop;
# 外部から特定の装置への接続が終了することを記録
iif $NIC_WAN ip daddr 192.168.110.nnn ct state established tcp flags & (fin|rst) != 0 jump logging_accept
# 確立済みの通信を許可
ct state established,related accept
# 内部から外部への接続を許可
iif $NIC_LAN oif $NIC_WAN ct state new jump logging_accept
# 外部から特定の装置への接続を記録
iif $NIC_WAN ip daddr 192.168.110.nnn ct state new jump logging_accept
}
chain logging_accept {
meter { ip saddr limit rate 3/minute burst 5 packets } log prefix "[NFT ACCEPT] "
accept
}
...
切断をtcpに絞ってログ出力しているのは、セッションを持つのが事実上tcpだけだから、とGeminiさん。
IPアドレス単位でカウントするが、ログ出力のレートは低め。
切断ログを取れないケースがあるかもしれないけれど、誤差20秒だから許容、といった設定。
内部ネットワークの不整合・異常なアウトバウンド
これは、以下のような問題を記録する、ということのようだ。
- 設定が古いままで存在しないIPアドレスにアクセスし続けている
- 許可されていないポートから出ていこうとしている
- 端末が侵害されてデーターを外に大量に送ろうとしている
しかし、現実問題として、端末が侵害されているかどうかを調べるとしたら、
- ネットワーク上に流量計みたいなものを挟んで、傾向を分析して異常を検知
- 振る舞いを検知するソフトを端末に仕込んで検知
ウチで試せることといえば、せいぜい「許可されていないポートから出ていこうとしている」のを止めて記録するくらい。
さて…
一般に家庭用ルーターは外には自由に出て行ける。
一口にいうと、自由に出て行けないと不良サービス扱いされるから、という理由だそう。
ウチでも家庭内ネットワークで制限をかけると、ブーブー言われる可能性あり。
今回はラボなので、外に出られるポートを「許可制」に変更することにした。
...
# 内部から外部への転送を許可するポート(TCP)
set fwd_allow_tcp {
type inet_service
flags interval
elements = {
22, # SSH
53, # DNS
80, # HTTP
443, # HTTPS
}
}
...
chain forward {
type filter hook forward priority filter; policy drop;
# 確立済みの通信を許可
ct state established,related accept
# 内部から外部への接続を許可
iif $NIC_LAN oif $NIC_WAN tcp dport @fwd_allow_tcp ct state new accept
iif $NIC_LAN oif $NIC_WAN udp dport @fwd_allow_udp ct state new accept
jump logging_forward_block
}
...
企業ネットの如く、出口をかなり絞っている(ホワイトリスト管理)。
必要に応じてポートを開けていくちょっと面倒な運用にはなるが、安全ではある。
基本的に通過させ、これだけは駄目というものを止める、という方式もコメントで残してある(ブラックリスト管理)。
設置する場所によって使い分けようかと思っている。
ルーターの動作確認とログ調整
ターミナルを開いて、ログを表示させる。
nftablesはカーネルログを出すので、以下のコマンドでフォローできる。
$ journalctl -kf --grep="\[NFT "
後は、ホームラボの端末から色々とネットワークの操作をして試す。
ここまできていれば、ルーターが内包しているサービスも動いているし、インターネットにも出られる。
ログが出たら内容を確かめて、必要ならポートを開放するし、ログを止めて無言でドロップしたい場合は、
# 許可されない転送をログ出力するチェーン
chain logging_forward_block {
iif $NIC_WAN th dport { 135, 137-139, 445 } drop
limit rate 3/minute burst 10 packets log prefix "[NFT FWD-BLK] "
drop
}
等としてしまえばOK。
nftablesとは
LinuxカーネルにはNetfilterというパケット処理のためのフレームワークを持っている。
カーネル内の処理エンジンはx_tablesで、これを操作するのがiptablesだった。
Ubuntu 20.10からカーネル内のエンジンはnf_tablesに変わり、操作するのはnftになった。
メリットは、
- 1つのルールを書けばIPv4もIPv6も処理ができるようになる
- 新しいプロトコルに対応するためにカーネルを書き換えてコンパイルする手間がなくなった
ということのようだ。
そして現在、iptablesで処理をすると、nftの操作に変換されているとのこと。
UFWが生成するルールを観察する
SSHが使えるようにして、UFWを有効にする。
$ sudo ufw allow ssh
$ sudo ufw enable
そして、テーブルを見てみる。
$ sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain ufw-before-logging-input {
}
chain ufw-before-logging-output {
}
chain ufw-before-logging-forward {
}
chain ufw-before-input {
iifname "lo" counter packets 0 bytes 0 accept
ct state related,established counter packets 92 bytes 5264 accept
ct state invalid counter packets 0 bytes 0 jump ufw-logging-deny
ct state invalid counter packets 0 bytes 0 drop
ip protocol icmp icmp type destination-unreachable counter packets 0 bytes 0 accept
ip protocol icmp icmp type time-exceeded counter packets 0 bytes 0 accept
ip protocol icmp icmp type parameter-problem counter packets 0 bytes 0 accept
ip protocol icmp icmp type echo-request counter packets 0 bytes 0 accept
udp sport 67 udp dport 68 counter packets 0 bytes 0 accept
counter packets 1 bytes 78 jump ufw-not-local
ip daddr 224.0.0.251 udp dport 5353 counter packets 0 bytes 0 accept
ip daddr 239.255.255.250 udp dport 1900 counter packets 0 bytes 0 accept
counter packets 1 bytes 78 jump ufw-user-input
}
<省略>
chain INPUT {
type filter hook input priority filter; policy drop;
counter packets 910 bytes 60847 jump ufw-before-logging-input
counter packets 910 bytes 60847 jump ufw-before-input
counter packets 475 bytes 37423 jump ufw-after-input
counter packets 341 bytes 20242 jump ufw-after-logging-input
counter packets 341 bytes 20242 jump ufw-reject-input
counter packets 341 bytes 20242 jump ufw-track-input
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
counter packets 465 bytes 59620 jump ufw-before-logging-output
counter packets 465 bytes 59620 jump ufw-before-output
counter packets 176 bytes 19684 jump ufw-after-output
counter packets 176 bytes 19684 jump ufw-after-logging-output
counter packets 176 bytes 19684 jump ufw-reject-output
counter packets 176 bytes 19684 jump ufw-track-output
}
chain FORWARD {
type filter hook forward priority filter; policy drop;
counter packets 0 bytes 0 jump ufw-before-logging-forward
counter packets 0 bytes 0 jump ufw-before-forward
counter packets 0 bytes 0 jump ufw-after-forward
counter packets 0 bytes 0 jump ufw-after-logging-forward
counter packets 0 bytes 0 jump ufw-reject-forward
counter packets 0 bytes 0 jump ufw-track-forward
}
chain ufw-logging-deny {
ct state invalid limit rate 3/minute burst 10 packets counter packets 0 bytes 0 return
limit rate 3/minute burst 10 packets counter packets 0 bytes 0 log prefix "[UFW BLOCK] "
}
<省略>
chain ufw-user-input {
tcp dport 22 counter packets 0 bytes 0 accept
}
<省略>
}
# Warning: table ip6 filter is managed by iptables-nft, do not touch!
table ip6 filter {
chain ufw6-before-logging-input {
}
<省略>
どうやら、
- IPv4とIPv6のルールは別々に作られる。
- INPUTチェーンからufw-before-inputチェーンが呼び出され、そこからufw-user-inputチェーンが呼び出されて、そこで22/tcpは許可される。
- 拒否したときは、ufw-before-inputからufw-logging-denyが呼び出されて、そこでログ出力される。
といった感じの定義だった。
UFWを止めて観察する
UFWを無効にすると、どうなるか。
$ sudo ufw disable
$ sudo nft list ruleset
table ip filter {
<省略>
chain INPUT {
type filter hook input priority filter; policy accept;
counter packets 1689 bytes 733140 jump ufw-before-logging-input
counter packets 1689 bytes 733140 jump ufw-before-input
counter packets 769 bytes 65347 jump ufw-after-input
counter packets 495 bytes 29176 jump ufw-after-logging-input
counter packets 495 bytes 29176 jump ufw-reject-input
counter packets 495 bytes 29176 jump ufw-track-input
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
counter packets 982 bytes 114588 jump ufw-before-logging-output
counter packets 982 bytes 114588 jump ufw-before-output
counter packets 299 bytes 29404 jump ufw-after-output
counter packets 299 bytes 29404 jump ufw-after-logging-output
counter packets 299 bytes 29404 jump ufw-reject-output
counter packets 299 bytes 29404 jump ufw-track-output
}
chain FORWARD {
type filter hook forward priority filter; policy accept;
counter packets 0 bytes 0 jump ufw-before-logging-forward
counter packets 0 bytes 0 jump ufw-before-forward
counter packets 0 bytes 0 jump ufw-after-forward
counter packets 0 bytes 0 jump ufw-after-logging-forward
counter packets 0 bytes 0 jump ufw-reject-forward
counter packets 0 bytes 0 jump ufw-track-forward
}
}
<省略>
IPv4もIPv6も同じようにすべのポリシーがacceptになり、SSHを許可するルールも消えていた。
起動直後はリストが空っぽだけれど、一度UFWを有効にすると、残骸が残るということでもあった。
残骸を消すために、UFWが無効になっていることを確認した上で、ルールをフラッシュした。
$ sudo ufw status
Status: inactive
$ sudo nft flush ruleset
UFWが生成したルールから学ぶ
iptablesには5つのテーブル(filter, nat, mangle, raw, security)があって実行順序が定められており、それぞれに実行可能なチェーンが定義されていた。
一方、今回生成されたテーブルはip, ip6で、実行順序がフックで定義されていた。
table ip filter {
chain INPUT {
type filter hook input priority filter; policy drop;
※filterという名前のIPv4テーブル、INPUTという名前のチェーンをフィルターとして利用、inputをフックして、優先度filter(=0)で実行。
マニュアルを見ながら、Copilotさんに聞いて解説してもらった。
テーブル
テーブルの宣言、アドレスファミリー、テーブル名の順で書く。
table ip filter
テーブルはアドレスファミリーと名前で識別される。
アドレスファミリーは6つ。
| ADDRESS FAMILY | 用途 |
|---|---|
| ip | IPv4パケット専用、省略された場合はこれが使われる |
| ip6 | IPv6パケット専用 |
| inet | IPv4とIPv6のハイブリッドなテーブル |
| arp | IPv4のARPパケット専用 |
| bridge | ブリッジデバイスのパケット制御 |
| netdev | ネットワークカードの物理的な入口・出口のパケット制御 |
ホームラボでルーターを作るなら、ハイブリッドなinetを使うのが良さそうだ。
チェーン
チェーンの宣言、名前の順で書く。
chain INPUT {
type filter hook input priority filter; policy drop;
チェーンには2種類がある。
- Base Chainは、type指定でフックを選び、処理の優先度を指定する。
- Regular Chainは、jumpやgotoによって呼び出されるチェーン。
この例だと、filterのinputにフックしているので、Base Chainということになる。
タイプは4種類。
| type | 利用可能なAF | 利用可能なフック(後述) | 役割 |
|---|---|---|---|
| filter | すべて | すべて | パケットの通過を許可したり、破棄したりする判定。 パケットデーターの書き換えや、SYNプロキシ防御などの高度な処理も可能。 |
| nat | ip, ip6, inet | prerouting, input, output, postrouting | IPアドレスやポート番号を書き換える。 SNAT, DNAT, MASQUERADE, REDIRECTなど。 |
| route | ip, ip6, inet | output | パケットの送信先IPアドレスや、出口のNICを変更する。 |
| raw | ip, ip6, inet, netdev | prerouting, output | コネクショントラッキングを無効化するために利用する。 |
今回作った設定で行くと、
- type filterで2つのフックを利用。
- hook input で、ルーター自体が提供しているサービスに関するフィルターを設定。
- hook forward で、ルーティングする条件を設定。
- type natで1つのフックを利用。
- hook prerouting で、LANからWANに出て行くパケットの送信元IPアドレスを書き換え。
フック
フックは7つで、アドレスファミリーごとに使えるものが違う。
| フック | IPV4/IPV6/INET ADDRESS FAMILIES | ARP AF | BRIDGE AF | NETDEV AF | 説明 |
|---|---|---|---|---|---|
| prerouting | ○ | ○ | システムに入ってきたパケットを最初に処理 | ||
| input | ○ | ○ | ○ | ローカルシステムに配信されるパケットを処理 | |
| forward | ○ | ○ | 別ホストに転送するパケットを処理 | ||
| output | ○ | ○ | ○ | ローカルプロセスから送信されるパケットを処理 | |
| postrouting | ○ | ○ | システムから出て行くパケットを最後に処理 | ||
| ingress | ○ | ○ | ○ | preroutingフックよりも前に呼び出される NETDEV:ネットワークタップの後、かつ、tc ingressの直後、かつ、レイヤー3のプロトコルハンドラの前に呼び出される | |
| egress | ○ | NETDEV:レイヤー3のプロトコルハンドラの後、かつ、tc egressの前に呼び出される |
どのシーンでどれを選択するべきなのか、利用ポリシーを上手く考えながら使っていくのだけれど、なかなか難しかった。
そういうときには、GeminiさんやCopilotさんに聞くのが良いと思う。
優先度
優先度はpriorityで指定。
- マイナス、0、プラスの数字が使えて、小さい値が優先される。
- 名前+数字で優先度を書くことができる。
- テーブルはnftablesに置ける概念に過ぎず、フックに取り付けられたチェーンは優先度順に評価される。
ルールが組み込まれると、最終的には優先度順で処理されるとのことなので、そのことをよく意識して設定する。
priorityには、以下の定義済みの名前がある。
| 名前 | 値 | 利用可能なAF | 利用可能なフック | 用途 |
|---|---|---|---|---|
| raw | -300 | ip, ip6, inet | all | パケットを調べる前に行う処理。 |
| mangle | -150 | ip, ip6, inet | all | パケットのヘッダを書き換える処理。 |
| dstnat | -100 | ip, ip6, inet | prerouting | DNAT/REDIRECT。ルーティングが行われる前の書き換え処理。type natで利用可能。 |
| filter | 0 | ip, ip6, inet, arp, netdev | all | 標準的なフィルタリング処理。 |
| security | 50 | ip, ip6, inet | all | Linux Security Modules用。SELinuxで使う。AppArmorでは関係なし。 |
| srcnat | 100 | ip, ip6, inet | postrouting | SNAT/MASQUERADE。パケットが外に出て行く直前の書き換え処理。type natで利用可能。 |
bridgeの場合は値が違っている。
| 名前 | 値 | 利用可能なフック | 用途 |
|---|---|---|---|
| dstnat | -300 | prerouting | ブリッジとして、ルーティングが行われる前の書き換え処理。 |
| fileter | -200 | all | ブリッジとして、標準的なフィルタリング処理。 |
| out | 100 | output | ブリッジとして、出力されるパケットに対する書き換えや制御。 |
| srcnat | 300 | postrouting | ブリッジとして、パケットが外に出て行く直前の書き換え処理。 |
その他に見かけたもの
- SETS:同じ種類のデーターをまとめた配列。IPアドレスをまとめたり、ポートをまとめたりするなど。
- MAPS:キーと値の配列。ポートと宛先IP・ポートをまとめるなど。
- FLOWTABLES:確立した接続のパケット転送を高速化させる。
- STATEFUL OBJECTS:テーブルに紐付けられ、状態を保管するもの。
大まかな枠組みはこのようなものと理解。
サンプルで学習
UFWでルーターを作ったときも、その前にiptablesでルーターを作ったときも、一番の心配は「本当に安全な設定にできた?」だった。
書き方は合ってるの?本当にこれでいいの?という心配。
ということで、サンプルがこちらに保管されていたので読んでみる。
/usr/share/doc/nftables/examples
ルーターを作ろうとする私へのGeminiさんのオススメは、以下の2つを見ることだった。
- workstation.nft
- nat.nft
workstation.nft
シンプルなものだったので、1行ずつ見て行く。
/usr/share/doc/nftables/examples/workstation.nft
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0;
# accept any localhost traffic
iif lo accept
# accept traffic originated from us
ct state established,related accept
# activate the following line to accept common local services
#tcp dport { 22, 80, 443 } ct state new accept
# ICMPv6 packets which must not be dropped, see https://tools.ietf.org/html/rfc4890#section-4.4.1
meta nfproto ipv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, 148, 149 } accept
ip6 saddr fe80::/10 icmpv6 type { 130, 131, 132, 143, 151, 152, 153 } accept
# count and drop any other traffic
counter drop
}
}
flush ruleset
最初にすべてのルールを消し去る定番の作法。
iptablesの場合、デフォルトポリシーによって一瞬の隙ができる(DROP:すべての通信を遮断、ACCEPT:丸腰)が、nftablesの場合はルールの全削除と新ルールの適用が同時なので問題なし。
table inet filter {
テーブルfilterを作成。
inetは、IPv4とIPv6の両方を扱うことを意味するファミリー。
chain input {
チェーンinputを作成。
type filter hook input priority filter; ← サンプルでは数字の0だった
このチェーンをBase Chainにする。
タイプはfilter、フック先はinput、優先度はフィルター(0)。
iif lo accept
入力インターフェース lo(loopback) はすべて許可。
ct state established,related accept
ct(コネクショントラッキング)により状態がestablished(接続確立+要求に対する応答パケット), related(メインの会話に付随して必要なパケット)の場合に許可。
状態には他にnew(最初のパケット), invalid(どの接続とも結びつかない不正なパケット), untracked(追跡から除外した特殊なパケット)がある。
#tcp dport { 22, 80, 443 } ct state new accept
コメントなので有効ではないけれど…
プロトコルTCPで、宛先ポートが22, 80, 443、かつ、最初のパケットであれば許可。
meta nfproto ipv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, echo-request, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, 148, 149 } accept
レイヤー3のプロトコルがIPv6で(meta nfproto ipv6)、ICMPv6のタイプが必要なもの(icmpv6 type {必要なタイプ})の場合に許可。
タイプについては後述。
ip6 saddr fe80::/10 icmpv6 type { 130, 131, 132, 143, 151, 152, 153 } accept
送信元がfe80::/10で(ip6 saddr fe80::/10)、ICMPv6のタイプが必要なもの(icmpv6 type {必要なタイプ})の場合に許可。
タイプについては後述。
counter drop
以上の条件に合致しないパケットをカウントして拒否。
icmpv6のタイプとして指定されていたものは以下。
名前が定義されていないときは、数字を直接指定するみたい。
| タイプ名 | タイプ番号 | 内容 |
|---|---|---|
| destination-unreachable | 1 | パケットが届けられなかった |
| packet-too-big | 2 | パケットが大きすぎるので分割して送り直して |
| time-exceeded | 3 | パケットのルーティング回数が多すぎて寿命が尽きた |
| parameter-problem | 4 | パケットのヘッターが壊れている |
| echo-reply | 128 | ping応答 |
| echo-request | 129 | ping |
| nd-router-solicit | 133 | ルーター要請 |
| nd-router-advert | 134 | ルーター広告 |
| nd-neighbor-solicit | 135 | 近隣要請(MACアドレスを教えて) |
| nd-neighbor-advert | 136 | 近隣応答(私のMACアドレスはこれ) |
| 148 | 148 | CPS(Certification Path Solicitation/証明書パス要請) |
| 149 | 149 | CPA(Certification Path Advertisement/証明書パス広告) |
| mld-listener-query | 130 | MLD 問い合わせ(マルチキャストを聴きたい機器の調査) |
| mld-listener-report | 131 | MLD 報告(マルチキャストを聴きたいという参加届) |
| mld-listener-done | 132 | MLD 終了(マルチキャストの視聴停止・退会届) |
| mldv2-listener-report | 143 | MLDv2 報告(高機能版のマルチキャスト参加・退会届) |
| 151 | 151 | MRD 要請(マルチキャスト対応ルーターの探索) |
| 152 | 152 | MRD 広告(マルチキャスト対応ルーターの存在案内) |
| 153 | 153 | MRD 終了(マルチキャスト対応ルーターの停止通知) |
nat.nft
こちらもとてもシンプルだった。
#!/usr/sbin/nft -f
table ip nat {
chain prerouting {
type nat hook prerouting priority 0;
#Thanks to nftables maps, if you have a previous iptables NAT (destination NAT) ruleset like this:
# % iptables -t nat -A PREROUTING -p tcp --dport 1000 -j DNAT --to-destination 1.1.1.1:1234
# % iptables -t nat -A PREROUTING -p udp --dport 2000 -j DNAT --to-destination 2.2.2.2:2345
# % iptables -t nat -A PREROUTING -p tcp --dport 3000 -j DNAT --to-destination 3.3.3.3:3456
# It can be easily translated to nftables in a single line:
dnat tcp dport map { 1000 : 1.1.1.1, 2000 : 2.2.2.2, 3000 : 3.3.3.3} \
: tcp dport map { 1000 : 1234, 2000 : 2345, 3000 : 3456 }
}
chain postrouting {
type nat hook postrouting priority 0;
#Likewise, in iptables NAT (source NAT):
# % iptables -t nat -A POSTROUTING -s 192.168.1.1 -j SNAT --to-source 1.1.1.1
# % iptables -t nat -A POSTROUTING -s 192.168.2.2 -j SNAT --to-source 2.2.2.2
# % iptables -t nat -A POSTROUTING -s 192.168.3.3 -j SNAT --to-source 3.3.3.3
# Translated to a nftables one-liner:
snat ip saddr map { 192.168.1.1 : 1.1.1.1, 192.168.2.2 : 2.2.2.2, 192.168.3.3 : 3.3.3.3 }
}
}
preroutingで受け取ったパケットの宛先を、別のIP・ポートに書き換える。
postroutingで送るパケットの発信元を、ローカルIPアドレスからグローバルっぽいIPアドレスに書き換える。
この2つを組み合わせて、ラボのルーターが作れそうな気になってきた。
最初の設定がその結果。
さいごに
今回もGeminiさんとCopilotさんに色々と聞きながら話を進めていったのだけれど、もしかしてちょっと性能落ちてる?
とにかく考えが浅いような気がしてしまう。
会話しながら進めると、色々なところで考慮が漏れてしまう。
上手く使えている人に話を聞いたら、別のチャットでプロンプトを作ってもらって、それを流すのがいいよとのこと。
なるほど!
つらつらと考えていることを書いてプロンプトを作ってもらい、追加した方が良い項目を提案してもらって追加。
いい感じのプロンプトができたら、それで依頼する。
回答があやふやなときがたまにあって、そんなときは大抵間違っている。
公式の情報に照らして回答を検証してもらうと、訂正してくれる。
でも、あんまり考えないで読んでいると、うっかり騙されちゃうんだよなー。← これホントに注意。
そして、会話を覚えていられる範囲が狭い。
仕方がないから、やっていることを時々まとめてもらい、話がブレないように時々それを投入。
課題が解決できたら、それをまとめに追加して投入。
一口にいえば、
地図を書いて現在地を適宜確認、ダラダラ話しながらやってもらうことを決めて、回答が気になったらチェックしてもらう。
といったところ。
自分ひとりでWebを探し、調べて試して進めていた日々を思い出しながら、コーヒーブレイク。
ものすごーく知識が広くて作業も早い、だけど、おっちょこちょい。
そんな人と一緒に仕事しているみたいで、笑ったりイライラしながら自分も勉強している感じ。いいかも。
なんだか楽しい週末だった。


コメントはこちらから お気軽にどうぞ ~ 投稿に関するご意見・感想・他