CUBE SUGAR CONTAINER

技術系のこと書きます。

Network Namespace と nftables で Source NAT を試す

今回は Network Namespace で作ったネットワーク上で nftables 1 を使った Source NAT (Network Address Translation) を試してみる。 nftables は、Linux で長らく使われてきた iptables 2 などのプログラムを置き換えることを志向したフレームワーク。 nftables と iptables は、どちらも Linux の netfilter 3 という仕組みを使って実装されたフロントエンドに相当する。

netfilter は Linux カーネルのプロトコルスタックにおいて、処理のタイミング毎に用意されたフックポイントにコールバックを登録できる仕組みのこと。 登録されるコールバックの中でパケットやフレームを処理することで、ファイアウォールや NAT といった機能を実現できる。

Source NAT を使うと、少数のグローバルアドレスを、多数のプライベートアドレスで共有できるようになる。 つまり、限られた資源である IPv4 のグローバルアドレスを節約することができる。 なお、アドレスを節約するためには NAT の一種である NAPT (Network Address Port Translation) である必要がある。

今回使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"
$ uname -srm
Linux 5.15.0-87-generic x86_64
$ nft --version
nftables v1.0.2 (Lester Gooch)

もくじ

下準備

あらかじめ必要なパッケージをインストールしておく。

$ sudo apt-get -y install nftables iproute2 tcpdump

nftables のサービスを稼働させる。

$ sudo systemctl start nftables
$ sudo systemctl enable nftables

nftables のカーネルモジュールがロードされていることを確認する。

$ lsmod | grep ^nf_tables
nf_tables             258048  0

Network Namespace でネットワークを作成する

まずは今回作成するネットワークの論理構成を以下に示す。

ネットワーク構成 (論理)

203.0.113.0/24 をグローバル、192.0.2.0/24 をプライベートなセグメントに見立てている。 Source NAT では、送信元 IP アドレスが 192.0.2.0/24 のパケットを router の持っている 203.0.113.254 に書き換える。

ここからは Network Namespace を使ってネットワークを作成していく。 まずは Network Namespace を作成する。

$ sudo ip netns add lan
$ sudo ip netns add router
$ sudo ip netns add wan

次に Network Namespace 同士をつなぐ veth インターフェイスを追加する。

$ sudo ip link add lan-veth0 type veth peer name gw-veth0
$ sudo ip link add wan-veth0 type veth peer name gw-veth1

作成したインターフェイスを Network Namespace に所属させる。

$ sudo ip link set lan-veth0 netns lan
$ sudo ip link set gw-veth0 netns router
$ sudo ip link set gw-veth1 netns router
$ sudo ip link set wan-veth0 netns wan

インターフェイスの状態を UP に設定する。

$ sudo ip netns exec lan ip link set lan-veth0 up
$ sudo ip netns exec router ip link set gw-veth0 up
$ sudo ip netns exec router ip link set gw-veth1 up
$ sudo ip netns exec wan ip link set wan-veth0 up

lan について、インターフェイスに IP アドレスを付与する。 また、デフォルトルートを設定する。

$ sudo ip netns exec lan ip address add 192.0.2.1/24 dev lan-veth0
$ sudo ip netns exec lan ip route add default via 192.0.2.254

router について、インターフェイスに IP アドレスを付与する。 また、ルータとして動作するようにカーネルパラメータの net.ipv4.ip_forward1 を設定する。

$ sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0
$ sudo ip netns exec router ip address add 203.0.113.254/24 dev gw-veth1
$ sudo ip netns exec router sysctl net.ipv4.ip_forward=1

最後に wan について、インターフェイスに IP アドレスを付与する。 また、デフォルトルートを設定する。

$ sudo ip netns exec wan ip address add 203.0.113.1/24 dev wan-veth0
$ sudo ip netns exec wan ip route add default via 203.0.113.254

nftables を設定する

ここからは nftables を使って Source NAT を設定していく。

nftables は基本的に nft(8) で設定する。 設定は nft list ruleset コマンドで確認できる。 初期状態では何も設定されていないため、結果は何も表示されない。

$ sudo ip netns exec router nft list ruleset

テーブルを追加する

まずは nft create table コマンドでテーブルを追加する。 テーブルはアドレスファミリとチェーンタイプを指定して追加する。

$ sudo ip netns exec router nft create table ip nat

上記ではアドレスファミリが ip でチェーンタイプが nat のテーブルを作っている。

テーブルを追加すると、次のように nft list ruleset の結果にテーブルが表示される。

$ sudo ip netns exec router nft list ruleset
table ip nat {
}

チェーンを追加する

続いて、処理のタイミングを表すチェーンをテーブルに追加する。 以下では先ほど作った ip nat のテーブルに POSTROUTING という名前でチェーンを追加している。 カッコ内は追加するチェーンの種類と、処理されるタイミングを示している。

$ sudo ip netns exec router nft add chain ip nat POSTROUTING { type nat hook postrouting priority srcnat\; }

チェーンを追加すると nft list ruleset の結果にチェーンが表示されるようになる。

$ sudo ip netns exec router nft list ruleset
table ip nat {
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
    }
}

ルールを追加する

最後に、具体的な処理の内容を表すルールをチェーンに追加する。 以下では先ほど作った ip nat テーブルの POSTROUTING チェーンにルールを追加している。

$ sudo ip netns exec router nft add rule ip nat POSTROUTING oifname "gw-veth1" ip saddr 192.0.2.0/24 masquerade

上記は送信元 IP アドレスが 192.0.2.0/24 で出力先のインターフェイスが gw-veth1 のときに IP マスカレードを実行するという意味になる。

ルールが追加されると nft list ruleset の実行結果は次のようになる。

$ sudo ip netns exec router nft list ruleset
table ip nat {
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "gw-veth1" ip saddr 192.0.2.0/24 masquerade
    }
}

動作を確認する

必要な設定が全て終わったので、ここからは動作を確認しよう。

lan から wan の IP アドレスに宛てて ping を打ってみよう。

$ sudo ip netns exec lan ping 203.0.113.1
PING 203.0.113.1 (203.0.113.1) 56(84) bytes of data.
64 bytes from 203.0.113.1: icmp_seq=1 ttl=63 time=0.164 ms
64 bytes from 203.0.113.1: icmp_seq=2 ttl=63 time=0.110 ms
64 bytes from 203.0.113.1: icmp_seq=3 ttl=63 time=0.107 ms
...

別のターミナルを開いて、まずは lan のインターフェイスでパケットをキャプチャする。

$ sudo ip netns exec lan tcpdump -tnl -i lan-veth0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lan-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 192.0.2.1 > 203.0.113.1: ICMP echo request, id 23567, seq 11, length 64
IP 203.0.113.1 > 192.0.2.1: ICMP echo reply, id 23567, seq 11, length 64
IP 192.0.2.1 > 203.0.113.1: ICMP echo request, id 23567, seq 12, length 64
IP 203.0.113.1 > 192.0.2.1: ICMP echo reply, id 23567, seq 12, length 64
IP 192.0.2.1 > 203.0.113.1: ICMP echo request, id 23567, seq 13, length 64
IP 203.0.113.1 > 192.0.2.1: ICMP echo reply, id 23567, seq 13, length 64

この段階では、送信元 IP アドレスは 192.0.2.1 のまま。

続いては wan のインターフェイスでパケットをキャプチャしよう。

$ sudo ip netns exec wan tcpdump -tnl -i wan-veth0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wan-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 203.0.113.254 > 203.0.113.1: ICMP echo request, id 23567, seq 22, length 64
IP 203.0.113.1 > 203.0.113.254: ICMP echo reply, id 23567, seq 22, length 64
IP 203.0.113.254 > 203.0.113.1: ICMP echo request, id 23567, seq 23, length 64
IP 203.0.113.1 > 203.0.113.254: ICMP echo reply, id 23567, seq 23, length 64
IP 203.0.113.254 > 203.0.113.1: ICMP echo request, id 23567, seq 24, length 64
IP 203.0.113.1 > 203.0.113.254: ICMP echo reply, id 23567, seq 24, length 64

今度は送信元 IP アドレスが 203.0.113.254 になっている。 つまり、ちゃんと router の持っている IP アドレスに送信元が書き換えられていることが確認できる。

まとめ

今回は Network Namespace を使って作成したネットワーク上で、nftables を使って Source NAT を試してみた。

補足

iptables のルールを nftables のルールに変換する

nftables には、iptables など旧来のプログラムから移行するためのツールが用意されている。 たとえば iptables から移行する際には iptables-translate(8) が利用できる。 使用する際は iptables コマンドを iptables-translate コマンドに書き換えて実行する。 すると、nftables 版の設定に書き換えられた結果が表示される。

$ iptables-translate -t nat \
    -A POSTROUTING \
    -s 192.0.2.0/24 \
    -o gw-veth1 \
    -j MASQUERADE

ただし、利用する上で注意点がある。 iptables ではデフォルトで用意されているテーブルやチェーンがあった。 しかし nftables にはデフォルトで用意されるテーブルやチェーンが基本的にない。 そのため、必要に応じて自分で作る必要がある。

ネットワーク図の作成について

ネットワーク図は以下の nwdiag 4 の定義で作成した。

nwdiag {

  network {
    address = '192.0.2.0/24';
    lan[address = 'lan-veth0, 192.0.2.1'];
    router[address = 'gw-veth0, 192.0.2.254'];
  }

  network {
    address = '203.0.113.0/24';
    router[address = 'gw-veth1, 203.0.113.254'];
    wan[address = 'wan-veth0, 203.0.113.1'];
  }
}

参考

manpages.ubuntu.com