CUBE SUGAR CONTAINER

技術系のこと書きます。

strongSwan の IPsec VPN を Network Namespace で試す (Route-based / VTI デバイス)

strongSwan は IPsec VPN を構成するのに用いられるソフトウェア。 今回は、その strongSwan を Network Namespace で作ったネットワーク上で動かしてみる。 動作モードとしては VTI (Virtual Tunnel Interface) デバイスを使った Route-based を利用する。

なお、現行バージョンの strongSwan は設定ファイルや PID ファイルの置き場所がビルド時にバイナリへハードコードされる。 そのため、そのままでは単一のホスト上で複数のインスタンスを動作させることができない。 この課題については Mount Namespace を併用することで克服した。

使った環境は次のとおり

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-69-generic x86_64
$ ipsec version
Linux strongSwan U5.9.5/K5.15.0-69-generic
University of Applied Sciences Rapperswil, Switzerland
See 'ipsec --copyright' for copyright information.

もくじ

下準備

まずは strongSwan をインストールする。

$ sudo apt-get install strongswan

インストールしたタイミングで systemd の ipsec サービスが動作し始める。 今回は systemd のサービスは不要なので止めておく。

$ sudo systemctl stop ipsec
$ sudo systemctl disable ipsec

ネットワークを作る

まずは Network Namespace を使ってネットワークを作っていく。 この工程では IPsec VPN とかは関係ない。 ただ、単一のセグメント (203.0.113.0/24) のネットワークを構成するだけ。

はじめに、IPsec VPN のトンネルを張るルータに相当するネームスペースを用意する。

$ sudo ip netns add router1
$ sudo ip netns add router2

両者を接続するための Virtual Ethernet デバイスのインターフェイスを作成する。

$ sudo ip link add rt1-veth0 type veth peer name rt2-veth0

両端のインターフェイスをネームスペースに所属させる。

$ sudo ip link set rt1-veth0 netns router1
$ sudo ip link set rt2-veth0 netns router2

インターフェイスをリンクアップさせる。

$ sudo ip netns exec router1 ip link set rt1-veth0 up
$ sudo ip netns exec router2 ip link set rt2-veth0 up

インターフェイスに IPv4 アドレスを付与する。

$ sudo ip netns exec router1 ip address add 203.0.113.1/24 dev rt1-veth0
$ sudo ip netns exec router2 ip address add 203.0.113.2/24 dev rt2-veth0

ルータとして動作するように、それぞれのネームスペースについてカーネルのパラメータを変更する。 ただ、今回の構成であればおそらく設定しなくても問題ない。

$ sudo ip netns exec router1 sysctl net.ipv4.ip_forward=1
$ sudo ip netns exec router2 sysctl net.ipv4.ip_forward=1

これでネットワークはできた。 一旦 L3 で疎通があることを ping で確認しておく。

$ sudo ip netns exec router1 ping -c 3 203.0.113.2 -I 203.0.113.1
PING 203.0.113.2 (203.0.113.2) from 203.0.113.1 : 56(84) bytes of data.
64 bytes from 203.0.113.2: icmp_seq=1 ttl=64 time=0.086 ms
64 bytes from 203.0.113.2: icmp_seq=2 ttl=64 time=0.071 ms
64 bytes from 203.0.113.2: icmp_seq=3 ttl=64 time=0.070 ms

--- 203.0.113.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
rtt min/avg/max/mdev = 0.070/0.075/0.086/0.007 ms

IPsec VPN を構成する

続いて IPsec VPN に関する設定をしていく。

まず、デフォルトで /etc 以下に用意されている strongSwan の設定ファイルを一旦 /var/tmp/strongswan-conf にコピーしておく。 これは、前述したとおり strongSwan のバイナリに設定ファイルの場所がハードコードされているため。 複数のインスタンスを動かすには一旦別の場所に退避させておく必要がある。

$ mkdir -p /var/tmp/strongswan-conf
$ cp /etc/strongswan.conf /var/tmp/strongswan-conf/
$ cp -r /etc/strongswan.d/ /var/tmp/strongswan-conf/

コピーした設定ファイルの中で charon.conf を修正する。 設定ファイルの中の install_routes というキーを no に設定する。 この修正は strongSwan を Route-based で動作させる際に必要になる。

$ sed -i.back -e "s/# install_routes = yes/install_routes = no/" /var/tmp/strongswan-conf/strongswan.d/charon.conf

設定項目がちゃんと書き換わっていることを確認する。

$ grep install_routes /var/tmp/strongswan-conf/strongswan.d/charon.conf
    install_routes = no

router1 を設定する

ここからは Network Namespace で作成した各ルータをシェルで操作していく。 以後は、どのルータを操作しているか分かりやすいように、左端に名前を補足としてつけておく。

まずは router1 から。 新しくシェルを起動したら unshare(1) で Mount Namespace を有効にする。 こうすることで、マウントの操作をしてもシステム側に影響を与えなくする。

(router1) $ sudo unshare --mount bash

さらに nsenter(1) を使って、先ほど作成した Network Namespace を有効にする。

(router1) # nsenter --net=/var/run/netns/router1

次のように rt1-veth0 インターフェイスが見えるようになる。

(router1) # ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: rt1-veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 02:74:2c:c1:b3:26 brd ff:ff:ff:ff:ff:ff link-netns router2
    inet 203.0.113.1/24 scope global rt1-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::74:2cff:fec1:b326/64 scope link 
       valid_lft forever preferred_lft forever

続いて /etc/var/run に tmpfs で作ったファイルシステムをマウントする。 これら 2 つのディレクトリは、前述したとおり Ubuntu の strongSwan バイナリにハードコードされた設定ファイルと PID ファイルの置き場所になっている。 Mount Namespace を分けてあるので、ここでマウントの操作をしてもシステム側に影響が及ぶことはない。

(router1) # mount -t tmpfs tmpfs /etc/
(router1) # mount -t tmpfs tmpfs /var/run

マウントしたディレクトリが空っぽに見えることを確認する。

(router1) # ls /etc
(router1) # ls /var/run

空っぽになった /etc 以下に設定ファイルを配置していく。 /etc/ipsec.conf に IKE や IPsec の動作パラメータを記述していく。 ここでは 203.0.113.1203.0.113.2 の間で IPsec VPN を張る。 トンネル両端のサブネットには 192.0.2.0/24198.51.100.0/24 を使う。

(router1) # cat << 'EOF' > /etc/ipsec.conf
config setup
    charondebug="all"
conn %default
    authby=psk
    type=tunnel
    auto=start
conn myvpn
    keyexchange=ikev2
    left=203.0.113.1
    leftsubnet=192.0.2.0/24
    right=203.0.113.2
    rightsubnet=198.51.100.0/24
    ike=aes256-sha256-modp1024!
    esp=aes256!
    mark=42
EOF

事前共有鍵を /etc/ipsec.secrets に書き込む。 もちろん、本来であればもっとちゃんとした事前共有鍵を使った方が良い。

(router1) # cat << 'EOF' > /etc/ipsec.secrets 
203.0.113.1 203.0.113.2 : PSK "DeadBeef"
EOF

先ほど /etc 以下から退避させておいたコンフィグ群を書き戻す。

(router1) # cp /var/tmp/strongswan-conf/strongswan.conf /etc/
(router1) # cp -r /var/tmp/strongswan-conf/strongswan.d/ /etc/

この時点で /etc 以下は次のような構成になっている。

(router1) # ls /etc
ipsec.conf  ipsec.secrets  strongswan.conf  strongswan.d

あとは ipsec start コマンドを実行すると関連するデーモンが動き始める。

(router1) # ipsec start --debug
Starting strongSwan 5.9.5 IPsec [starter]...
Loading config setup
Loading conn 'myvpn'

このとき PID ファイルが /var/run 以下に作られる。

なお、プロセスをフォアグラウンドで動作させたいときは、次のように --nofork オプションを付けておくと良い。

(router1) # ipsec start --nofork --debug

router2 を設定する

router2 についても、同様に設定していく。 やることはほとんど変わらない。

Mount Namespace と Network Namespace を有効にしたシェルを新たに用意する。

(router2) $ sudo unshare --mount bash
(router2) # nsenter --net=/var/run/netns/router2
(router2) # ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: rt2-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 62:8a:44:2c:56:0a brd ff:ff:ff:ff:ff:ff link-netns router1
    inet 203.0.113.2/24 scope global rt2-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::608a:44ff:fe2c:560a/64 scope link 
       valid_lft forever preferred_lft forever

設定ファイルを書き込む。

(router2) # mount -t tmpfs tmpfs /etc/
(router2) # mount -t tmpfs tmpfs /var/run
(router2) # cat << 'EOF' > /etc/ipsec.conf
config setup
    charondebug="all"
conn %default
    authby=psk
    type=tunnel
    auto=start
conn myvpn
    keyexchange=ikev2
    ike=aes256-sha256-modp1024!
    esp=aes256!
    left=203.0.113.2
    leftsubnet=198.51.100.0/24
    right=203.0.113.1
    rightsubnet=192.0.2.0/24
    mark=42
EOF
(router2) # cat << 'EOF' > /etc/ipsec.secrets 
203.0.113.2 203.0.113.1 : PSK "DeadBeef"
EOF
(router2) # cp /var/tmp/strongswan-conf/strongswan.conf /etc/
(router2) # cp -r /var/tmp/strongswan-conf/strongswan.d/ /etc/
(router2) # ls /etc
ipsec.conf  ipsec.secrets  strongswan.conf  strongswan.d

ipsec start でデーモンを起動する。

(router2) # ipsec start --debug
Starting strongSwan 5.9.5 IPsec [starter]...
Loading config setup
Loading conn 'myvpn'

このとき Mount Namespace が分かれていないと PID ファイルが既にあると言われて起動に失敗する。

SA (Security Association) の状態を確認する

ここまで作業が終わったら IKE / IPsec SA の状態を確認する。 ipsec status コマンドを実行して、次のように SA が確立していれば問題ない。

(router1) # ipsec status
Security Associations (1 up, 0 connecting):
       myvpn[1]: ESTABLISHED 4 seconds ago, 203.0.113.1[203.0.113.1]...203.0.113.2[203.0.113.2]
       myvpn{1}:  INSTALLED, TUNNEL, reqid 1, ESP SPIs: c31aaf62_i c5314c6e_o
       myvpn{1}:   192.0.2.0/24 === 198.51.100.0/24

router2 についても同様に SA が確立していることを確認しておく。

(router2) # ipsec status
Security Associations (1 up, 0 connecting):
       myvpn[2]: ESTABLISHED 8 seconds ago, 203.0.113.2[203.0.113.2]...203.0.113.1[203.0.113.1]
       myvpn{2}:  INSTALLED, TUNNEL, reqid 1, ESP SPIs: c5314c6e_i c31aaf62_o
       myvpn{2}:   198.51.100.0/24 === 192.0.2.0/24

これで IPsec VPN のトンネルはできた。

VTI デバイスを追加して経路を設定する

続いては VTI デバイスを追加して経路を設定する。

一般に、ここからの作業はシェルスクリプトにした上でデーモンからキックしてもらうことで自動化することが多い 1。 しかし、今回は分かりやすさのために手作業でこなしていく。

まずは router1 の方に VTI デバイスのインターフェイスを追加する。 ここで local と remote には、先ほど構築した IPsec VPN のトンネルのエンドポイントになっているアドレスを指定する。 そして key には /etc/ipsec.conf に指定した mark と同じ値を指定する。 こうすることで処理の対象にする IPsec SA を識別するようだ。

(router1) # ip tunnel add ipsec0 mode vti local 203.0.113.1 remote 203.0.113.2 key 42

インターフェイスをリンクアップさせる。

(router1) # ip link set ipsec0 up

動作確認用としてインターフェイスに IPv4 アドレスを設定する。 このとき、アドレスは /etc/ipsec.confleftsubnet に指定したサブネットに属するものにする。

(router1) # ip addr add 192.0.2.1/24 dev ipsec0

/etc/ipsec.confrightsubnet に到達するには、追加したインターフェイスを経由するように経路を追加する。

(router1) # ip route add 198.51.100.0/24 dev ipsec0

同じ内容を router2 側にも設定する。

(router2) # ip tunnel add ipsec0 mode vti local 203.0.113.2 remote 203.0.113.1 key 42
(router2) # ip link set ipsec0 up
(router2) # ip addr add 198.51.100.1/24 dev ipsec0
(router2) # ip route add 192.0.2.0/24 dev ipsec0

動作を確認する

これで全ての設定が完了した。 試しに VTI デバイスのインターフェイスの間で ping を打ってみよう。

$ sudo ip netns exec router1 ping 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 198.51.100.1: icmp_seq=1 ttl=64 time=0.196 ms
64 bytes from 198.51.100.1: icmp_seq=2 ttl=64 time=0.170 ms
64 bytes from 198.51.100.1: icmp_seq=3 ttl=64 time=0.190 ms
...

どうやらちゃんと疎通があるようだ。

インターフェイスをパケットキャプチャすると、ちゃんとパケットがやり取りされている。 当たり前だけど、ここの内容は平文になっている。

$ sudo ip netns exec router1 tcpdump -tnl -i ipsec0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ipsec0, link-type RAW (Raw IP), snapshot length 262144 bytes
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 75, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 75, length 64
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 76, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 76, length 64
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 77, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 77, length 64
...

一方で Virtual Ethernet デバイスのインターフェイスの方をキャプチャすると IPsec の ESP パケットの形で流れている。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x18), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x18), length 120
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x19), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x19), length 120
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x1a), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x1a), length 120
...

IKE のネゴシエーションの様子も確認しておこう。 次のように ESP 以外に IKE 関連のパケットもキャプチャできるようにして tcpdump を起動する。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0 esp or udp port 500 or udp port 4500 or tcp port 4500
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

そして、どちらでも良いので router[12] で ipsec stop してから ipsec start する。 ようするに IKE のネゴシエーションからやり直してもらう。

(router2) # ipsec stop
(router2) # ipsec start

すると、次のように IKE 関連のパケットがキャプチャできる。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0 esp or udp port 500 or udp port 4500 or tcp port 4500
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 203.0.113.2.500 > 203.0.113.1.500: isakmp: parent_sa ikev2_init[I]
IP 203.0.113.1.500 > 203.0.113.2.500: isakmp: parent_sa ikev2_init[R]
IP 203.0.113.2.4500 > 203.0.113.1.4500: NONESP-encap: isakmp: child_sa  ikev2_auth[I]
IP 203.0.113.1.4500 > 203.0.113.2.4500: NONESP-encap: isakmp: child_sa  ikev2_auth[R]

パケットについている [I][R] は Initiator と Responder のどちらなのかを表しているようだ。

まとめ

今回は strongSwan を Network Namespace と Mount Namespace を駆使することで単一ホスト上で動かしてみた。