それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

linuxのnftables(nft)でルータ機能を作る

linuxで単純なルータ機能を作る

マスカレードやDNAT/SNATをして、ルータ機能を作ってみる。

先のiptablesと比較してnft でどうなるのかを見たいので、試した。

ネットワーク構成図

以下のようなネットワーク構成を作る

実際に作った構成

ipコマンドには netnsの機能があるので、これを利用して内部で名前空間(namespace)で分割して簡単にネットワーク構成を作ってみた。

作成用のコマンド

## コンテナ作成
lxc launch ubuntu:22.04 t01
lxc config set t01 security.nesting true
lxc restart t01
lxc shell t01

## デバイス追加
ip link add name veth0 type veth peer name veth0-c01
ip netns add c01
ip link set veth0-c01 netns c01
ip netns exec c01 ip link set veth0-c01 name eth0
ip link set veth0 name eth1

## アドレス追加
ip netns exec c01 ip addr add 10.2.0.2/24 dev eth0
ip addr add 10.2.0.1/24 dev eth1

## リンクアップ
ip netns exec c01 ip link set eth0 up
ip link set eth1 up

## ルーティング
ip netns exec c01 ip route add default via 10.2.0.1 dev eth0

マスカレードを試す。

ルータ機能で一番使い勝手のいいマスカレードを試してみる。

実際のコマンド

## マスカレード
nft add table mynat
nft add chain mynat postrouting { \
  type nat hook postrouting priority srcnat \; \
}
nft add rule mynat postrouting oifname "eth0" masquerade

## 後片付け
nft delete table mynat 

出来上がったルール

nft list table テーブル名 でルールが確認できる。

root@t01:~# nft list table mynat
table ip mynat {
        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                oifname "eth0" masquerade
        }
}

accept は自動で書かれた。

SNATをする

ソースアドレスを自分のものに書き換えてNATする。マスカレードで間に合わないときに使う。

実際のコマンド

## LAN_IP=10.17.238.236
LAN_IP=$(ip a  s eth0 | grep -oP '(?<=inet )([\d+\.]+)(?=/)')

## SNAT
nft add table mynat
nft add chain mynat postrouting { \
  type nat hook postrouting priority srcnat \; \
}
nft add rule mynat postrouting oifname "eth0" snat to $LAN_IP

出来上がったルール

root@t01:~# nft list table mynat
table ip mynat {
        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                oifname "eth0" snat to 10.17.238.236
        }
}

DNAT

DNATで宛先を変更する。SNAT・MASQURADEと違い、ルーティング機能より前に挟み込まれるのでpreroutingへフックする。 実験なので、すべてのパケットをeth0/10.17.238.236宛に向ける。ICMP(ping)が通ればいいやレベル。

実際のコマンド

## LAN_IP=10.17.238.236
LAN_IP=$(ip a  s eth0 | grep -oP '(?<=inet )([\d+\.]+)(?=/)')

## SNAT
nft add table mynat
nft add chain mynat prerouting { \
  type nat hook prerouting priority dstnat \; \
}
nft add rule mynat prerouting iifname "eth1" dnat to $LAN_IP

## 後片付け
nft delete table mynat 

実際に出来上がったルール

nft list table mynat
table ip mynat {
        chain prerouting {
                type nat hook prerouting priority dstnat; policy accept;
                iifname "eth1" dnat to 10.17.238.236
        }
}

DNAT(宛先IPを指定、プロトコル指定)

先程のDNATはザックリし過ぎなので、具体的に宛先IPとプロトコル(ICMP)を決めてパケットを変換する。

実際のコマンド

## LAN_IP=10.17.238.236
LAN_IP=$(ip a  s eth0 | grep -oP '(?<=inet )([\d+\.]+)(?=/)')

## SNAT
nft add table mynat
nft add chain mynat prerouting { \
  type nat hook prerouting priority dstnat \; \
}
nft add rule mynat prerouting \
  iifname "eth1"   \
  ip daddr 1.1.1.1 \
  ip protocol icmp \
  dnat to $LAN_IP

## 後片付け
nft delete table mynat 

実験する

ip netns exec c01 ping 1.1.1.1 # DNATでeth0が応答する
ip netns exec c01 ping 1.0.0.1 # 戻り経路がないので応答しない

実際に出来上がったルール

root@t01:~# nft list table mynat
table ip mynat {
        chain prerouting {
                type nat hook prerouting priority dstnat; policy accept;
                iifname "eth1" ip daddr 1.1.1.1 ip protocol icmp dnat to 10.17.238.236
        }
}

DNAT( IP / port / tcp を指定)

DNATでもうちょっと具体的に、TCPポートを指定する。

## LAN_IP=10.17.238.236
TARGET_IP=$( dig +short g.co )

## DNAT 1.1.1.1"80 を$TARGET_IP:80へ
nft add table mynat
nft add chain mynat prerouting { \
  type nat hook prerouting priority dstnat \; \
}
nft add rule mynat prerouting \
  iifname "eth1" \
  tcp dport 80  \
  ip daddr 1.1.1.1 \
  dnat to $TARGET_IP

## SNAT 戻り経路
LAN_IP=$(ip a  s eth0 | grep -oP '(?<=inet )([\d+\.]+)(?=/)')
nft add chain mynat postrouting { type nat hook postrouting priority srcnat \; }
nft add rule mynat postrouting oifname "eth0" snat to $LAN_IP

## 後片付け
nft delete table mynat 

実験コマンドと、その結果

ip netns exec c01 curl  http://1.1.1.1
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

1.1.1.1 がDNATされて、g.co へ飛ばされているのがわかる。

実際に出来上がったルール

root@t01:~# nft list table mynat
table ip mynat {
        chain prerouting {
                type nat hook prerouting priority dstnat; policy accept;
                iifname "eth1" tcp dport 80 ip daddr 1.1.1.1 dnat to 172.217.25.174
        }

        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                oifname "eth0" snat to 10.17.238.236
        }
}

nftables の hook とチェインについて

nft には、「ベースチェイン」「レギュラーチェイン」がある。

名称 役割
ベースチェイン hook ポイントへフックする
レギュラーチェイン 単なる関数として動作

ベースチェインでhook ポイントへフックさせてつかい、条件にマッチしたらレギュラーチェインへjump させる。という使い方になる。

主に使うのはベースチェイン

ここままで、使ってきたチェインはすべてベースチェインであり、hook で フックポイントへ登録している。

nft add chain mynat prerouting   { type nat hook prerouting priority dstnat \; }
nft add chain mynat postrouting { type nat hook postrouting priority srcnat \; }

登録は次のように解釈できる。

type natが主に使うタイプなのだが、パケットをフィルタリングするときは、 type filter を使い、パケットをルーティングするときは type routeを使う。もちろん用途があるので、natをforwarding に登録したり、routeを inputに登録するような無意味なことはできない。filter はfilterの名前のとおりであるが、「条件にマッチさせる」ことが目的でありマッチ後に何をするかは自由なので、何でも書ける。フィルタは疎通決定というより、マッチさせるという意味で解釈するとわかりやすいかもしれない。

type フックするポイント
filter prerouting , forwarding, input , outout. postrouting
nat prerouting , input , output , postrouting
route output

優先度に付いて

優先度は、もともと決まっている。

どの(レイヤ)のどこ(フック)で何をするか(mangle、dstnat 、srcnat )である程度決まっている。フックに登録するときにデフォルト設定が適用されて、それを±1して調整するっぽい。

まとめ・感想

iptablesで作ったときと同等の処理をnftで記述してみた。

どう考えてもnft のほうが覚えることが多くて冗長になりがちで。覚え直すことある。ついつい敬遠しがちである。

ただ、nftほうが複雑化するルールを整理整頓でき、影響範囲を限定できる。ルールが思わぬ混線をすること減らせる。またテーブル名が一種の名前空間になっているため、不要なルールを除くオペレーションをやりやすい。ミスのリカバリを速くできる。nftが使えるときはnftコマンドを使ったほうがいいかもしれない。

ただ、nft はiptablesより整理されてるとはいえ、nftそれ自体がコマンド完結で使いづらいので、今後また大規模な進化が起こる予感がする。init.d のかわりに upstartが提案されたけど結局はsystemdになったように、chrootのかわりにdockerが作られたけど、podmanになったように。lxcがlxdになったように、ifconfigがip a s になったように。

nftも進化の途中っぽい感じはびんびんする。(似たようなキーワードが多くて分かりづらい)

個人的にはこの辺の基礎部分をイジってほしくは無いのですが・・・

ブリッジモードも試そうと思って、iptablesでは作りかけていたが力尽きたので諦めた。

iptables / nf_tablesの変換表

今回使ったルールで、iptablesとnft でのフィルタルール例の早見表(チートシート)にまとめておいた。

nftables 早見用

iptables nftables モジュール
--protocol tcp
-p tcp
protocol tcp xt_tcp
---source 192.168.1.0/24
-s 192.168.1.0/24
ip saddr 192.168.1.0/24
---destination 192.168.1.0/24
-d 192.168.1.0/24
ip daddr 192.168.1.0/24
-o eth0
--out-interface eth0
oifname eth0
-i eth0
--i-interface eth0
iifname eth0
--dport 443 tcp dport 443
--jump MARK --set-mark 0x2
mark 0x2 xt_MARK
--jump SNAT -to-source 192.168.1.1 snat to 192.168.1.1 xt_SNAT

モジュールは iptables(nftコマンド内蔵)で作ったルールで適用されたモジュールのメモである。

参考資料

Linuxにおける新たなパケットフィルタリングツール「nftables」入門 | さくらのナレッジ