現在のウエブ開発では異なるネットワークに存在する端末同士が連携したシステムを構築することが多くあります。
例えば、弊社製品のクラウドロガーLTEがクラウドと連携する場合、通信事業者のネットワーク内の端末とAWSのネットワーク内のサーバーの異なるネットワークに存在する端末同士が通信を行います。
このようなネットワーク環境でのシステム構築の際に、実際にサーバーや端末を用意する前に、PC内に事前検証環境を構築できると便利です。
本記事ではlinux上においてnamespaceを使用して複数のネットワーク環境を構築し、動作検証の一例としてNATのいくつかのパターン(Full Cone NAT, Restricted Cone NAT)の動作を見てみたいと思います。
はじめに
本記事ではコンテナとnamespaceを組み合わせて、複数のネットワーク空間を持つ環境を構築します。
一般的にコンテナ技術としてはdockerが有名ですが、dockerはネットワーク設定を自動化してくれる反面、細かいネットワーク設定を行うことが難しい場合があります。
以下を満たす環境を構築するために、本記事ではipコマンドとsystemd-nspawnを使用したいと思います。
- ネットワーク管理ツールとして一般的なipコマンドを使用して、ネットワークインターフェースやルーティングの設定を行う
- 作成したネットワークインターフェースをコンテナのネットワークとして使用する
- systemdのサービスを動作させる(ただし、今回はsystemdサービスは使用しません)
namespaceについて
namespaceはlinux環境でリソースを独立させるための仕組みです。
CPUやメモリ等のリソースを独立させるcgroupと組み合わせて使用されることが多く、docker等のコンテナ技術の基盤にもなっているようです。
namespaceにはいくつかの種類がありますが、本記事ではネットワークnamespaceを使用してネットワーク空間を独立させます。
(systemd-nspawnは独自にnamespaceやcgroupを使用していると思いますが、本記事では取り上げません)
ネットワーク構成
本記事で作成するネットワークの構成は以下図のようになります。

ネットワークnamespaceはsystemd-nspawnで生成したコンテナに接続します。
かっこ内の上段はコンテナ名、下段はnamespace名を示しています。
実際のインターネット上の通信は多数のゲートウエイを経由しますが、本記事ではインターネットをブラックボックスと考え、1つのnamespaceで構成しています。
このネットワークを構築するためのスクリプト(netns.sh)は本記事末尾に掲載していますので、ここでは詳細については説明を割愛します。
ネットワークnamespaceの準備
netns.shは以下のように実行します。
|
1 2 3 4 |
#ネットワーク作成 $ sudo ./netns.sh create #ネットワーク削除 $ sudo ./netns.sh clean |
上記実行後にip netnsで確認するとnamespaceが作成されていることが分かります。
また、namespace内でip linkを実行すると作成したインターフェースが表示されますが、ホスト側で実行した場合は表示されないことから、ネットワークインターフェースも独立したnamespaceに存在していることが分かります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#生成されたnamespace $ ip netns ns-c1-nat (id: 4) ns-c1 (id: 3) ns-c0-nat (id: 1) ns-c0 (id: 0) ns-inet (id: 2) # ホスト側でのip link実行 # (MACアドレス等の情報は割愛しています) $ ip link show 1: lo: <loopback,up,lower_up> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 2: enp0s3: <broadcast,multicast,up,lower_up> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 # c0-nat内でip link実行 $ sudo ip -n ns-c0-nat link 1: lo: <loopback,up,lower_up> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 50: ve-c0-nat-node@if51: <broadcast,multicast,up,lower_up> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 53: ve-c0-nat-inet@if52: <broadcast,multicast,up,lower_up> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 </broadcast,multicast,up,lower_up></broadcast,multicast,up,lower_up></loopback,up,lower_up></broadcast,multicast,up,lower_up></loopback,up,lower_up> |
systemd-nspawn環境の構築
systemd-nspawnはchrootを強化したコマンドと言われており、namespaceやcgroupを使用してコンテナを起動します。
今回は学習用に自前でipコマンドでnamespaceを作成したことから、namespaceと接続しやすいsystemd-nspawnを使用しています。
また、本記事では使用していませんが、systemd-nspawnのコンテナ内ではsystemdを起動できるため、コンテナ中でsystemdサービスを使用したい場合にも便利です。
ルートファイルシステムの準備
今回はdebootstrapで作成したルートファイルシステムをコンテナとして使用します。
以下ではnoble配下にubuntu24.04(noble)のルートファイルシステムをダウンロードしています。
|
1 2 3 |
$ sudo debootstrap noble noble http://archive.ubuntu.com/ubuntu/ ... I: Base system installed successfully. |
ダウンロードしたままの状態ではユーザのパスワードが設定されていないので、一旦chrootでログインしてパスワードを設定しておきます。
コンテナ内ではrootで作業を行うため、rootのパスワードを設定します。
|
1 2 3 4 5 6 |
$ sudo chroot noble/ /bin/bash groups: cannot find name for group ID 118 root@test:/# passwd New password: Retype new password: passwd: password updated successfully |
debootstrapが完了すると、以下のようにsystemd-nspawnから起動できるようになります。
systemdのサービスも起動していることが分かります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ sudo systemd-nspawn -D noble/ -b Spawning container noble on /home/test/work/debootstrap/noble. Press ^] three times within 1s to kill container. systemd 255.4-1ubuntu8 running in system mode (+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified) Detected virtualization systemd-nspawn. Detected architecture x86-64. ... [ OK ] Finished systemd-update-utmp-runlevel.service - Record Runlevel Change in UTMP. Ubuntu 24.04 LTS test pts/0 test login: root Password: Welcome to Ubuntu 24.04 LTS (GNU/Linux 5.10.0-34-amd64 x86_64) |
btrfsを使用したルートファイルシステムの作成
起動中のコンテナと同じルートファイルシステムから新たにコンテナを起動しようとすると、以下のようにエラーとなります。
|
1 2 3 |
$ sudo systemd-nspawn -D noble/ -b Directory tree /home/test/work/debootstrap/noble is currently busy. $ |
systemd-nspawnに–ephemeralオプションを指定することでこのエラーは回避することができます。
しかし、–ephemeralオプションは起動時にルートファイルシステムを一時領域にコピーするため、起動に時間がかかります。
また複数のコンテナを同時に起動すると、RAMの使用量も増加します。
そこで、起動速度とRAM使用量を改善するために、スナップショットをサポートしているbtrfsでルートファイルシステムを作成したいと思います。
こちらの手順は以下を参考にさせていただきました。
https://idle.nprescott.com/2022/systemd-nspawn-and-btrfs.html
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 5GBの領域をイメージファイル向けに確保 $ fallocate -l 5G btrfs.img # イメージファイル内にbtrfsファイルシステムを作成 $ /sbin/mkfs.btrfs btrfs.img btrfs-progs v5.10.1 ... #イメージファイルをマウント $ mkdir root $ sudo mount -o loop btrfs.img root/ # debootstrapでダウンロードしたファイル群を移動 $ sudo cp -r noble/* root/ |
btrfsを使用することで–ephemeral使用時のコンテナ起動時間とRAM使用量がどの程度改善するかを確認してみます。
起動時間の比較
起動時間差を明確にするために、lsコマンドを実行して終了するだけのコンテナを起動しています。
3~4秒高速になっていることが分かります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# ext4上のルートファイルシステムの場合 $ time sudo systemd-nspawn -D noble/ --ephemeral ls Spawning container noble-7f3650b7b741662f on /home/test/work/debootstrap/.#machine.noble03044e0a90e9a5e5. Press ^] three times within 1s to kill container. bin bin.usr-is-merged boot dev etc home lib lib.usr-is-merged lib64 media mnt opt proc root run sbin srv sys tmp usr var Container noble-7f3650b7b741662f exited successfully. real 0m3.819s user 0m0.070s sys 0m3.148s # btrfs上のルートファイルシステムの場合 $ time sudo systemd-nspawn -D root/ --ephemeral ls Spawning container root-90f838c50c93a8b1 on /home/test/work/debootstrap/root/.#machine.b39a72068515c4fe. Press ^] three times within 1s to kill container. bin bin.usr-is-merged boot dev etc home lib lib.usr-is-merged lib64 media mnt opt proc root run sbin srv sys tmp usr var Container root-90f838c50c93a8b1 exited successfully. real 0m0.213s user 0m0.051s sys 0m0.039s |
メモリ使用量の比較
次にメモリ使用量も見てみます。コンテナ起動直後のRAM使用量を出力しています。
ext4でのfreeの減少量(738->262)よりbtrfを使用した方がfreeの減少量(736->657)が小さく、コンテナ実行中のメモリ使用量が抑えられていることが分かります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# ext4上のルートファイルシステムの場合 # sudo systemd-nspawn -D noble/ --ephemeral -b実行前 $ free -m total used free shared buff/cache available Mem: 7944 3168 738 75 4037 4392 Swap: 975 520 455 # sudo systemd-nspawn -D noble/ --ephemeral -b実行中 $ free -m total used free shared buff/cache available Mem: 7944 3177 262 75 4504 4382 Swap: 975 520 455 # btrfs上のルートファイルシステムの場合 # sudo systemd-nspawn -D root/ --ephemeral -b実行前 $ free -m total used free shared buff/cache available Mem: 7944 3163 736 75 4044 4396 Swap: 975 520 455 # sudo systemd-nspawn -D root/ --ephemeral -b実行中 $ free -m total used free shared buff/cache available Mem: 7944 3174 657 75 4112 4385 Swap: 975 520 455 |
使用するパッケージのインストール
スナップショットを使用して起動したコンテナ上の変更は、コンテナ終了後に破棄されます。
そのため、必要なパッケージは–ephemeralを指定せずに起動したコンテナ上に事前にインストールしておきます。
ここでは本記事後半で使用するconntrackやiptablesをインストールします。
|
1 2 3 4 5 6 |
$ sudo systemd-nspawn -D noble/ -b # コンテナにログインしてapt installを実行 root@test:~# apt install conntrack iptables #以下のようなエラーが出る場合はresolv.conf等のDNS設定を確認してください W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/noble/InRelease Temporary failure resolving 'archive.ubuntu.com' W: Some index files failed to download. They have been ignored, or old ones used instead. |
コンテナの起動
コンテナとネットワーク環境の準備が整ったので、ネットワークnamespaceをコンテナに接続して起動してみます。
systemd-nspawnのオプションについて簡単に説明します。
>
| オプション | 説明 |
|---|---|
| –network-namespace-path | コンテナに接続するnamespaceのパス(通常/var/run/netns/配下に存在)を指定 |
| –capabitrty | コンテナ内でネットワークの操作を行うため、CAP_NET_ADMINを指定 |
| -M | 複数のコンテナを起動するため、各コンテナにはネットワーク構成で示した名前を指定 |
起動例は以下のようになります。
|
1 2 3 4 5 6 |
$ sudo ./netns.sh create #以下の各systemd-nspawnはそれぞれに対してターミナルを起動して実行します $ sudo systemd-nspawn --ephemeral -b -D root --network-namespace-path=/var/run/netns/ns-c0 --capability=CAP_NET_ADMIN -M c0 $ sudo systemd-nspawn --ephemeral -b -D root --network-namespace-path=/var/run/netns/ns-c0-nat --capability=CAP_NET_ADMIN -M c0-nat $ sudo systemd-nspawn --ephemeral -b -D root --network-namespace-path=/var/run/netns/ns-c1 --capability=CAP_NET_ADMIN -M c1 $ sudo systemd-nspawn --ephemeral -b -D root --network-namespace-path=/var/run/netns/ns-c1-nat --capability=CAP_NET_ADMIN -M c1-nat |
起動中のコンテナはmachinectlコマンドで確認することができます。
以下の例ではc0, c0-nat, c1、c1-natが起動していることが分かります。
またmachinectlは各種コマンドが搭載されており、起動中のコンテナへのログインも可能です。
|
1 2 3 4 5 6 7 8 |
$ machinectl MACHINE CLASS SERVICE OS VERSION ADDRESSES c0 container systemd-nspawn ubuntu 24.04 10.0.1.1… c0-nat container systemd-nspawn ubuntu 24.04 10.0.1.100… c1 container systemd-nspawn ubuntu 24.04 10.0.2.1… c1-nat container systemd-nspawn ubuntu 24.04 10.0.2.100… 3 machines listed. |
NAT動作検証
本記事ではシンプルな動作確認を行うために、UDPパケットの通信を使った検証を行います。
検証対象のNATについて
今回の検証ではNATの動作を見てみたいと思います。
NATは大きく以下の4種類に分けられるようですが、下に行くほど制約が厳しくなります。
| NAT種別 | 説明 |
| Full Cone NAT | 内部と外部のポートのマッピングが固定。マッピングされたポートについて、外部からのパケットを内部ホストに転送 |
| Restricted Cone NAT | 内部と外部のポートのマッピングが固定。マッピングされたポートについて、内部から通信確立した相手(IPアドレスのみ判定)からのパケットを内部ホストに転送 |
| Port Restricted Cone NAT | 内部と外部のポートのマッピングが固定。マッピングされたポートについて、内部から通信確立した相手(外部IPアドレス+ポートを判定)からのパケットを内部ホストに転送 |
| Symmetric NAT | 内部からの通信ごとに異なる外部ポートをマッピング。マッピングされたポートについて、内部から通信確立した相手(外部IPアドレス+ポートを判定)からのパケットを内部ホストに転送 |
現在ではマッピングとフィルタリングを分けてEndpoint-Independent MappingやEndpoint-Independent Filteringのような分類が推奨されるようですが、本記事では上記4分類の用語を使用し、Full Cone NATとRestricted Cone NATの動作をconntrackコマンドで確認してみたいと思います。
conntrackについて
conntrackはカーネルが管理している接続追跡テーブルを表示・操作するためのコマンドです。
ssやnetstatはプロセスと紐づいたソケットの情報を表示しますが、conntrackはカーネル内部のNAT変換や接続や切断の状態を表示することができます。
通信がどのタイミングで接続確立と判断されたかや、NAT変換前後のアドレス・ポート等を確認することができ、通信状況の確認に役立ちます。
conntrackの出力内容について簡単に説明します。
以下はc1からc0-natへnetcatでUDPパケットを送信した際のc1-natのconntrackの出力例です。
conntrackは-Eオプションをつけることで、リアルタイムに状態変化を表示することができます。
|
1 2 3 4 5 6 |
# c1からnetcat実行 root@test:~# echo 111|nc 100.64.1.100 12345 -u # c1-natでのconntrack出力 root@test:~# conntrack -E [NEW] udp 17 30 src=10.0.2.1 dst=100.64.1.100 sport=54536 dport=54321 [UNREPLIED] src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 |
| フィールド | 説明 |
|---|---|
| [NEW] | 新しいエントリを作成 |
| udp 17 | upd(プロトコル番号17)のパケット |
| 30 | 応答がない場合に削除されるまでのタイムアウト時間 |
| src=10.0.2.1 dst=100.64.1.100 sport=54536 dport=54321 | 送信元のIPアドレスは10.0.2.1:54536, 送信先は100.64.1.100:54321 |
| [UNREPLIED] | 応答パケットが未検出 |
| src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 | 期待する応答元は100.64.1.100:54321、応答先は100.64.2.100:54321 |
Full Cone NATの検証
c0をFull Cone NATとして設定しているので、対向のc1側からnetcatでパケットを送信します。
動作結果をコードブロック内に記載します。
コードブロック内のコメントの番号がタイミングを示しています。
c1からc0-nat(100.64.1.100)のポート54321へUDPパケットを送信するとc0-natでNAT変換が行われ、c0(10.0.1.1)でUDPポート12345で受信待ちしているnetcatにパケットが届いていることが分かります。
また、c0-nat外部からのパケットが無条件でc0-nat内部のc0に届いており、c0-natがFull Cone NATの動作となっていることを確認することができました。
c0
|
1 2 3 4 |
# (1) c0でUDPポート12345を受信待ち root@test:~# nc -l 12345 -u # (2) c1からのパケットを受信 abc |
c0-nat
|
1 2 3 4 5 6 |
# (1) c0-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c1(内部ホスト)からの通信パケット(外部アドレス100.64.2.100として)を検出。c0からの応答がないので[UNREPLIED]となっている [NEW] udp 17 30 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 [UNREPLIED] src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 # (3) c0のnetcatが返信パケットを送信しなかったため、応答パケットがカーネルレベルで検出されず(UNREPLIEDのまま)、UDPのタイムアウト(30秒)で削除された [DESTROY] udp 17 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 [UNREPLIED] src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 |
c1
|
1 2 |
#(2) c1からc0-natのポート54321へUDPパケット送信 root@test:~# echo abc | nc 100.64.1.100 54321 -u |
c1-nat
|
1 2 3 4 5 6 |
# (1) c1-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c1からの通信を検出。c0からの応答がないので[UNREPLIED]となっている [NEW] udp 17 30 src=10.0.2.1 dst=100.64.1.100 sport=54536 dport=54321 [UNREPLIED] src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 # (3) c0のnetcatが返信パケットを送信しなかったため、応答パケットがカーネルレベルで検出されず(UNREPLIEDのまま)、UDPのタイムアウト(30秒)で削除された [DESTROY] udp 17 src=10.0.2.1 dst=100.64.1.100 sport=54536 dport=54321 [UNREPLIED] src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 |
Restricted Cone NATの検証: 接続確立前
Restricted Cone NATは接続が確立する前の状態では、外部からのパケットをNAT内部に転送しません。
c1をRestricted Cone NATとして設定しているので、先ほどとは反対に対向のc0側からnetcatでパケットを送信してみます。
以下のようにRestricted Cone NAT内のc1ではパケットを受信できていないことが分かります。
また、接続が削除された後はc0からc1へパケットが届かなくなっていることも分かります。
c0
|
1 2 |
#(2) c0からc1-natのポート54321へUDPパケット送信 root@test:~# echo xyz | nc 100.64.2.100 54321 -u |
c0-nat
|
1 2 3 4 5 6 |
# (1) c0-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c0-natからの通信を検出。c1からの応答がないので[UNREPLIED]となっている [NEW] udp 17 30 src=10.0.1.1 dst=100.64.2.100 sport=41807 dport=54321 [UNREPLIED] src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 # (3) 30秒経過しても応答がないため、追跡対象から削除される [DESTROY] udp 17 src=10.0.1.1 dst=100.64.2.100 sport=41807 dport=54321 [UNREPLIED] src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 |
c1
|
1 2 3 |
# (1) c1でUDPポート12345を受信待ち root@test:~# nc -l 12345 -u # (2) c0からのパケットを受信できない |
c1-nat
|
1 2 3 |
# (1) c1-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c0-natからの通信を検知していない |
Restricted Cone NATの検証: 接続確立時
次にc1が最初にc0へ送信をした場合を見てみます。
この場合、c0が返信した時点で接続が確立されたと判断され、それ以降のc0からのパケットはc1に転送されるようになります。
c0
|
1 2 3 4 5 6 7 8 9 10 |
# (1) c0でUDPポート12345を受信待ち root@test:~# nc -l 12345 -u # (2) c1からのパケットを受信 abc # (3) c1へパケットを返信 reply 1 # (4) c1へもう一度パケットを返信 reply 2 # (6) c1-natで接続が削除された後にパケットを送信 reply after destroy |
c0-nat
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# (1) c0-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c1からの通信を検出。c0からの応答がないので[UNREPLIED]となっている [NEW] udp 17 30 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 [UNREPLIED] src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 # (3) c0からの返信を検出し[UNREPLIED]が消えている。この状態でc1からc0への接続が確立したと判断している。 [UPDATE] udp 17 30 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 # (4) c0からの再度の通信により、通信安定と判断して[ASSURED]となっているため、タイムアウト時間が120秒に延長されている [UPDATE] udp 17 120 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 [ASSURED] # (5) 120秒経過しても応答がないため、追跡対象から削除される [DESTROY] udp 17 src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 [ASSURED] # (6) 接続が削除された後の送信パケットを検出 [NEW] udp 17 30 src=10.0.1.1 dst=100.64.2.100 sport=12345 dport=54321 [UNREPLIED] src=100.64.2.100 dst=100.64.1.100 sport=54321 dport=54321 |
c1
|
1 2 3 4 5 6 7 |
#(2) c1からc0-natのポート54321へUDPパケット送信 root@test:~# echo abc | nc 100.64.1.100 54321 -u #(3) c0からの返信を受信 reply 1 #(4) c0からの返信をもう一度受信 reply 2 # (6) c1-natで接続が削除されたため、c1がパケットを受信できなかった |
c1-nat
|
1 2 3 4 5 6 7 8 9 10 11 |
# (1) c1-natでconntrackで監視開始 root@test:~# conntrack -E # (2) c1からの通信を検出。c0からの応答がないので[UNREPLIED]となっている [NEW] udp 17 30 src=10.0.2.1 dst=100.64.1.100 sport=44432 dport=54321 [UNREPLIED] src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 # (3) c0からの返信を検出し[UNREPLIED]が消えている。この状態でc1からc0への接続が確立した状態 [UPDATE] udp 17 30 src=10.0.2.1 dst=100.64.1.100 sport=44432 dport=54321 src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 # (4) c0からの再度の通信により、通信安定と判断して[ASSURED]となっているため、タイムアウト時間が120秒に延長されている [UPDATE] udp 17 120 src=10.0.2.1 dst=100.64.1.100 sport=44432 dport=54321 src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 [ASSURED] # (5) 120秒経過しても応答がないため、追跡対象から削除される [DESTROY] udp 17 src=10.0.2.1 dst=100.64.1.100 sport=44432 dport=54321 src=100.64.1.100 dst=100.64.2.100 sport=54321 dport=54321 [ASSURED] # (6) 接続が削除された後のパケットはc1-natで破棄されているため、conntrack上で検出されなかった |
以上の動作により、c1-natが上述のRestricted Cone NATとして動作していることを確認できました。
また、UDPの場合は一定時間通信がない場合に切断として判断していることや、それ以降は外部からの通信を破棄していることも分かりました。
余談: wireguardにおけるPersistentKeepaliveとの関係
VPNプロトコルとして人気のあるwireguardはノード間の通信はUDPで行います。
以下のwireguardの概要を見ると、NAT配下にあるNATの接続状態を継続するためにKeepAliveパケットを送信する機能があり、送信間隔はPersistentKeepaliveというパラメータで指定できるようです。
NAT and Firewall Traversal Persistence
ここで、以下のように多くのNATにおいて25秒が適当な旨の記載があります。
|
1 |
A sensible interval that works with a wide variety of firewalls is 25 seconds. |
上記検証でUDPの接続確立後、ASSUREDとなる前の無通信状態では30秒で接続が破棄されることを確認しましたが、wireguardのPersistentKeepaliveの25秒はこれを踏まえての値と推測できます。
後片づけ
以下を実行して今回の検証で作成したnamespaceを削除します。
|
1 |
$ sudo ./netns.sh clean |
まとめ
本記事ではipコマンドを使用してネットワークnamespaceを作成し、systemd-nspawnで起動したコンテナに接続する方法を紹介しました。
また、その活用例としていくつかのNATの動作検証を行いました。
これら技術を応用することで同一ホスト内に様々なネットワーク環境を構築することができ、ネットワークの学習やシステム開発の事前検証等の効率化にも役立つと思います。
本記事を活用していただければ幸いです。
netns.sh
本記事で使用したnetns.shを以下に掲載します。
ご使用の環境のIPアドレスと衝突する場合は適宜修正してご使用ください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
#!/bin/bash set -e CMD="${1:?}" ## 端末設定 # 端末名 NODES=("c0" "c1") # 端末のIPアドレス NODE_IPS=("10.0.1.1/24" "10.0.2.1/24") ## NAT設定 # 端末側のNATのIPアドレス NAT_FOR_NODE_IPS=("10.0.1.100/24" "10.0.2.100/24") # インターネット側のNATのIPアドレス NAT_FOR_INET_IPS=("100.64.1.100/24" "100.64.2.100/24") ## インターネット設定 # インターネット側のIPアドレス INET_FOR_NAT_IPS=("100.64.1.254/24" "100.64.2.254/24") # インターネットのnamespace INET="inet" # NAT種別 NAT_TYPES=("fullcone" "restcone") # NATのiptables設定 function set_iptables_for_nat() { local idx=${1:?} local type=${NAT_TYPES[$idx]} # 端末のnamespace、veth、IP local node_ns="ns-${NODES[$idx]}" local node_veth="ve-${NODES[$idx]}" local node_ip="${NODE_IPS[$idx]}" node_ip="${node_ip%/*}" # NATのnamespaceとveth local nat_ns="${node_ns}-nat" local nat_for_node_veth="${node_veth}-nat-node" local nat_for_inet_veth="${node_veth}-nat-inet" # 端末向けのNATのIPアドレス local nat_for_node_ip=${NAT_FOR_NODE_IPS[$idx]} nat_for_node_ip="${nat_for_node_ip%/*}" # インターネット向けのNATのIPアドレス local nat_for_inet_ip=${NAT_FOR_INET_IPS[$idx]} nat_for_inet_ip="${nat_for_inet_ip%/*}" # NAT内部の端末のポート local in_nat_port=12345 # インターネット側に公開するポート local out_nat_port=54321 # 事前に対象のnamespaceのiptablesを初期化しておく ip netns exec "${nat_ns}" iptables -F ip netns exec "${nat_ns}" iptables -X ip netns exec "${nat_ns}" iptables -t nat -F ip netns exec "${nat_ns}" iptables -t nat -X ip netns exec "${nat_ns}" iptables -P INPUT DROP ip netns exec "${nat_ns}" iptables -P FORWARD DROP # カーネルパラメータで転送は許可しておく. NAT種別ごとにiptablesで制御するため ip netns exec "${nat_ns}" sysctl -w net.ipv4.ip_forward=1 echo "set ${nat_for_inet_veth} to $type" case "$type" in fullcone | restcone | portrestcone) # Full Cone, Restricted Cone, Port Restricted Cone NATに共通の設定 # # 内部の端末から外部へ通信時に、NATのインターネット側のIPアドレスと特定のポートに変換する ip netns exec "${nat_ns}" iptables -t nat -A POSTROUTING -o "${nat_for_inet_veth}" -p tcp -j SNAT --to-source "${nat_for_inet_ip}:${out_nat_port}" ip netns exec "${nat_ns}" iptables -t nat -A POSTROUTING -o "${nat_for_inet_veth}" -p udp -j SNAT --to-source "${nat_for_inet_ip}:${out_nat_port}" # 外部からのアクセスを特定のポートで受け付け、内部の端末の特定のポートに転送する ip netns exec "${nat_ns}" iptables -t nat -A PREROUTING -i "${nat_for_inet_veth}" -p tcp --dport "${out_nat_port}" -j DNAT --to-destination "${node_ip}:${in_nat_port}" ip netns exec "${nat_ns}" iptables -t nat -A PREROUTING -i "${nat_for_inet_veth}" -p udp --dport "${out_nat_port}" -j DNAT --to-destination "${node_ip}:${in_nat_port}" # 内部の端末からの転送は全て許可する ip netns exec "${nat_ns}" iptables -A FORWARD -p tcp -i "${nat_for_node_veth}" -j ACCEPT ip netns exec "${nat_ns}" iptables -A FORWARD -p udp -i "${nat_for_node_veth}" -j ACCEPT ;; *) ;; esac case "$type" in #各FORWARDは上記PREROUTING変換後のパケットに対して適用される fullcone) #上記で指定の外部ポートから変換された場合は全て転送を許可する ip netns exec "${nat_ns}" iptables -A FORWARD -p tcp -d "${node_ip}" --dport "${in_nat_port}" -j ACCEPT ip netns exec "${nat_ns}" iptables -A FORWARD -p udp -d "${node_ip}" --dport "${in_nat_port}" -j ACCEPT ;; restcone) # 内部から通信を確立した相手(外部IPのみ判定)からのパケットを転送する ip netns exec "${nat_ns}" iptables -A FORWARD -p tcp -d "${node_ip}" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ip netns exec "${nat_ns}" iptables -A FORWARD -p udp -d "${node_ip}" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ;; #postrestcone, symmetricについては今回の検証では使用しないのでコメント割愛 portrestcone) ip netns exec "${nat_ns}" iptables -A FORWARD -p tcp -d "${node_ip}" --dport "${in_nat_port}" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ip netns exec "${nat_ns}" iptables -A FORWARD -p udp -d "${node_ip}" --dport "${in_nat_port}" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT ;; symmetric) ip netns exec "${nat_ns}" iptables -t nat -A POSTROUTING -o "${nat_for_inet_veth}" -j MASQUERADE ;; esac } # インターネットnamespace作成 function create_inet() { local inet=${INET} # インターネットに見立てて中継を行うだけなので、FORWARD機能のみ有効とする ip netns add "ns-${inet}" ip netns exec "ns-${inet}" iptables -P FORWARD ACCEPT ip netns exec "ns-${inet}" sysctl -w net.ipv4.ip_forward=1 } # インターネットnamespace削除 function delete_inet() { local inet=${INET} ip netns del "ns-${inet}" } # 端末、NAT向けnamespace生成 function create_network() { local idx=${1:?} # 端末のnamespace、veth、IP local node_ns="ns-${NODES[$idx]}" local node_veth="ve-${NODES[$idx]}" local node_ip="${NODE_IPS[$idx]}" # NATのnamespace local nat_ns="${node_ns}-nat" # NATの端末側veth, IPアドレス local nat_for_node_veth="${node_veth}-nat-node" local nat_for_node_ip=${NAT_FOR_NODE_IPS[$idx]} # NATのインターネット側veth, IPアドレス local nat_for_inet_veth="${node_veth}-nat-inet" local nat_for_inet_ip=${NAT_FOR_INET_IPS[$idx]} # インターネットのnamespace、veth、IP local inet_ns="ns-${INET}" local inet_veth="${node_veth}-inet" local inet_for_nat_ip="${INET_FOR_NAT_IPS[$idx]}" ## 端末側の設定 # vethはnamespace間を接続するための仮想インターフェースペア # ホスト上で、端末とNAT端末側のvethペアを作成 ip link add dev "${node_veth}" type veth peer name "${nat_for_node_veth}" # 端末のnamespaceを作成 ip netns add "${node_ns}" # 端末のvethを端末namespaceに移動 ip link set dev "${node_veth}" netns "${node_ns}" # ip -n namespaceで、指定のnamespace内でipコマンドを実行できる # 端末namespaceでIPアドレス設定 ip -n "${node_ns}" addr add dev "${node_veth}" "${node_ip}" # 端末namespaceでvethをUPする ip -n "${node_ns}" link set dev "${node_veth}" up # 端末namespaceでデフォルトルートをNAT端末側のIPアドレスに設定 ip -n "${node_ns}" route add default via "${nat_for_node_ip%/*}" # NATのnamespaceを作成 ip netns add "${nat_ns}" ## NATの端末側の設定 # 端末側設定で作成したNAT端末側vethをNATのnamespaceに移動 ip link set dev "${nat_for_node_veth}" netns "${nat_ns}" # NATのnamespaceでIPアドレス設定 ip -n "${nat_ns}" addr add dev "${nat_for_node_veth}" "${nat_for_node_ip}" # NATのnamespaceでvethをUPする ip -n "${nat_ns}" link set dev "${nat_for_node_veth}" up ## NATのインターネット向けの設定 ## インターネット向けのnamespaceはcreate_inetで作成済みの前提 # ホスト上で、NATインターネット側とインターネットのvethペアを作成 ip link add dev "${nat_for_inet_veth}" type veth peer name "${inet_veth}" # NATインターネット側vethをNAT側namespaceに移動 ip link set dev "${nat_for_inet_veth}" netns "${nat_ns}" # インターネットのvethをインターネットnamespaceに移動 ip link set dev "${inet_veth}" netns "${inet_ns}" # NAT向けのnamespaceでNATインターネット側vethのIPアドレス設定 ip -n "${nat_ns}" addr add dev "${nat_for_inet_veth}" "${nat_for_inet_ip}" # NATのnamespaceでvethをUPする ip -n "${nat_ns}" link set dev "${nat_for_inet_veth}" up # NATのnamespaceでデフォルトルートをインターネットのIPアドレスに設定 ip -n "${nat_ns}" route add default via "${inet_for_nat_ip%/*}" # インターネットnamespaceでインターネットのvethにIPアドレス設定 ip -n "${inet_ns}" addr add dev "${inet_veth}" "${inet_for_nat_ip}" # インターネットnamespaceでvethをUPする ip -n "${inet_ns}" link set dev "${inet_veth}" up # NAT種別に応じたiptables設定 set_iptables_for_nat "${idx}" "${NAT_TYPES[$idx]}" } # 端末、NAT向けnamespace削除 function delete_network() { local idx=${1:?} local node_ns="ns-${NODES[$idx]}" local nat_ns="${node_ns}-nat" # namespaceを削除するとvethも自動的に削除される ip netns del "${node_ns}" ip netns del "${nat_ns}" } if [[ "${CMD}" = "create" ]]; then # ネットワーク生成 create_inet i=0 for _ in "${NODES[@]}"; do echo $i create_network ${i} i=$((i + 1)) done elif [[ "${CMD}" = "clean" ]]; then # ネットワーク削除 delete_inet i=0 for _ in "${NODES[@]}"; do echo $i delete_network ${i} i=$((i + 1)) done # delete_inet fi |


