Ubuntu

Ubuntu24.04 nftablesを使ってルーターを作る

Ubuntu

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>

項目備考
OSUbuntu 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用途
ipIPv4パケット専用、省略された場合はこれが使われる
ip6IPv6パケット専用
inetIPv4とIPv6のハイブリッドなテーブル
arpIPv4の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プロキシ防御などの高度な処理も可能。
natip, ip6, inetprerouting, input, output, postroutingIPアドレスやポート番号を書き換える。
SNAT, DNAT, MASQUERADE, REDIRECTなど。
routeip, ip6, inetoutputパケットの送信先IPアドレスや、出口のNICを変更する。
rawip, ip6, inet, netdevprerouting, 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システムから出て行くパケットを最後に処理
ingresspreroutingフックよりも前に呼び出される
NETDEV:ネットワークタップの後、かつ、tc ingressの直後、かつ、レイヤー3のプロトコルハンドラの前に呼び出される
egressNETDEV:レイヤー3のプロトコルハンドラの後、かつ、tc egressの前に呼び出される

どのシーンでどれを選択するべきなのか、利用ポリシーを上手く考えながら使っていくのだけれど、なかなか難しかった。
そういうときには、GeminiさんやCopilotさんに聞くのが良いと思う。

優先度

優先度はpriorityで指定。

  • マイナス、0、プラスの数字が使えて、小さい値が優先される。
  • 名前+数字で優先度を書くことができる。
  • テーブルはnftablesに置ける概念に過ぎず、フックに取り付けられたチェーンは優先度順に評価される。

ルールが組み込まれると、最終的には優先度順で処理されるとのことなので、そのことをよく意識して設定する。

priorityには、以下の定義済みの名前がある。

名前利用可能なAF利用可能なフック用途
raw-300ip, ip6, inetallパケットを調べる前に行う処理。
mangle-150ip, ip6, inetallパケットのヘッダを書き換える処理。
dstnat-100ip, ip6, inetpreroutingDNAT/REDIRECT。ルーティングが行われる前の書き換え処理。type natで利用可能。
filter0ip, ip6, inet, arp, netdevall標準的なフィルタリング処理。
security50ip, ip6, inetallLinux Security Modules用。SELinuxで使う。AppArmorでは関係なし。
srcnat100ip, ip6, inetpostroutingSNAT/MASQUERADE。パケットが外に出て行く直前の書き換え処理。type natで利用可能。

bridgeの場合は値が違っている。

名前利用可能なフック用途
dstnat-300preroutingブリッジとして、ルーティングが行われる前の書き換え処理。
fileter-200allブリッジとして、標準的なフィルタリング処理。
out100outputブリッジとして、出力されるパケットに対する書き換えや制御。
srcnat300postroutingブリッジとして、パケットが外に出て行く直前の書き換え処理。

その他に見かけたもの

  • 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-unreachable1パケットが届けられなかった
packet-too-big2パケットが大きすぎるので分割して送り直して
time-exceeded3パケットのルーティング回数が多すぎて寿命が尽きた
parameter-problem4パケットのヘッターが壊れている
echo-reply128ping応答
echo-request129ping
nd-router-solicit133ルーター要請
nd-router-advert134ルーター広告
nd-neighbor-solicit135近隣要請(MACアドレスを教えて)
nd-neighbor-advert136近隣応答(私のMACアドレスはこれ)
148148CPS(Certification Path Solicitation/証明書パス要請)
149149CPA(Certification Path Advertisement/証明書パス広告)
mld-listener-query130MLD 問い合わせ(マルチキャストを聴きたい機器の調査)
mld-listener-report131MLD 報告(マルチキャストを聴きたいという参加届)
mld-listener-done132MLD 終了(マルチキャストの視聴停止・退会届)
mldv2-listener-report143MLDv2 報告(高機能版のマルチキャスト参加・退会届)
151151MRD 要請(マルチキャスト対応ルーターの探索)
152152MRD 広告(マルチキャスト対応ルーターの存在案内)
153153MRD 終了(マルチキャスト対応ルーターの停止通知)

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を探し、調べて試して進めていた日々を思い出しながら、コーヒーブレイク。
ものすごーく知識が広くて作業も早い、だけど、おっちょこちょい。
そんな人と一緒に仕事しているみたいで、笑ったりイライラしながら自分も勉強している感じ。いいかも。

なんだか楽しい週末だった。

ここから広告
広告
広告ここまで

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