オンプレのサーバとClassic Infrastructureのサーバを、VPC Gen1のVPN経由で通信させたい

IBM CloudのVPC Gen1はIPSec VPNサービス（VPNaaS）を提供しており、オンプレのサーバはこのVPNを経由してVPCのサブネットや、DB、監視等の各種クラウドサービスとプライベートネットワーク経由で通信できます。
これとは別に、VPC Gen1にはClassic Accessというサービスがあり、これを有効にして作成したVPCはClassic Infrastructure（旧Softlayerを継承したIaaS）と通信することができます。
VPNaaSとClassic Accessの両方を使用すると、VPC Gen1を挟んでオンプレとClassic Infrastructureがつながった構成になるのですが、Classic Infrastructureの制約により、オンプレのサーバとClassic Infrastructureのサーバが直接通信することはできません。
VPCのサーバはオンプレともClassic Infrastructureとも通信できるので、VPCのサーバにゲートウェイを構成すれば、オンプレのサーバとClassic Infrastructreのサーバの通信を中継できそうです。
ゲートウェイの方法はいくつか考えられますが、ここではNATを取り上げました。実際にVPCのサーバにNATを構成し、VPNおよびClassic Accessを超えてサーバ間の疎通を確認できたので、その構成をご紹介します。

内容は次のとおりです。

1. 検証システム構成
2. NAT変換のポリシー
3. ルーティング・ポリシー
4. NAT構築準備
5. ルーティング・ポリシーの登録
6. nftablesによるNATの定義
7. 疎通確認
8. 定義の永続化

NATの構成に使用した仮想サーバ（Vertual Server Instance (VSI)）のOSはubuntu 18.04.2 LTS、カーネルは4.15.0-54-genericです。

----

### 1. 検証システム構成
構成図を以下に示します。オンプレの代わりにVPC Gen1をもう一つ用意し、VPC間をIPSec VPNで接続しました。左端のVPC内のVSI（仮想サーバ）と、右端のClassic Infrastructure内のVSIの疎通（双方向）を目指します。
中央のVPCには同一ゾーン内に２つのサブネットを作成し、一方（172.18.0.0/24）をVPN経由で左端のVPCと通信するための経路とし、インターフェースを一つ割り当てておきます（eth0）。もう一方（172.18.1.0/24）はClassic Infrastructureと通信するための経路とし、インターフェースは２つ割り当てておきます（eth3, eth4）。
実際にはどちらのサブネットからも左端のVPCやClassic Infrastructureと通信することは可能ですが、わかりやすくするためにVPN担当サブネットとClassic担当サブネットに役割を分けておきました。
![コメント 2020-07-02 133621.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32083/9c794573-005d-6cb0-0771-004d38244ddf.png)
余談ですが、Classic Accessを利用するためのカスタマイズとして、Classic Infrastructure側のVSIにスタティックルーティングを追加して、VPC宛のパケットがプライベート・ネットワークに流れるようにしています（デフォルトがパブリックを向いているため）。
`例：ip route add 172.18.0.0/16 via 10.87.0.1 dev eth0`


### 2. NAT変換のポリシー

左端のVPC内のVSIと右端のClassic Infrastructure内のVSIの疎通のシナリオを定義し、そのために必要なNATのアドレス変換のポリシーを具体的に決めます。
まずシナリオを決めます。

+ <b>シナリオ1:</b> 左端のVSI tk-ub18-dal1またはtk-ub18-dal2から、右端のVSI icosdemo1上でPort 5000を開いて稼働するWebアプリを呼び出す
+ <b>シナリオ2:</b> 右端のVSI master02から、左端のVSI tk-ub18-dal1またはtk-ub18-dal2にsshログインする

次にシナリオを実現するためのNAT変換のポリシーを決めます。

<b>シナリオ1:</b>
tk-ub18-dal1（10.240.0.19）およびtk-ub18-dal2（10.240.0.13）はicosdemo1（10.87.22.50）を直接呼び出せないので、tk-ub18-nat3を呼び出して、tk-ub18-nat3からicosdemo1を呼び出すことにします。この時tk-ub18-nat3で宛先IPアドレスの変換（DNAT）を行います。また、tk-ub18-nat3からリクエストを受け取ったicosdemo1が応答をtk-ub18-nat3に返せるように、tk-ub18-nat3はパケット送出前にソースIPアドレスを自分のインターフェースのIPアドレスに置き換えます（SNAT）。

<b>シナリオ2:</b>
master02（10.87.22.12）はtk-ub18-dal1（10.240.0.19）を直接呼び出せないので、tk-ub18-nat3を呼び出して、tk-ub18-nat3からtk-ub18-dal1を呼び出すことにします（tk-ub18-dal2に対しても同様）。この時tk-ub18-nat3で宛先IPアドレスの変換（DNAT）を行います。また、tk-ub18-nat3からリクエストを受け取ったtk-ub18-dal1が応答をtk-ub18-nat3に返せるように、tk-ub18-nat3はパケット送出前にソースIPアドレスを自分のインターフェースのIPアドレスに置き換えます（SNAT）。
master02からsshログインしたいVSIは２つあるので、この２つのVSIと1対1に対応する形で、受け口となるtk-ub18-nat3のインターフェースも２つ用意します（eth3とeth4）。

これらのシナリオを以下に図示しました。
![コメント 2020-07-03 145700.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32083/08c8dac8-e315-ea07-186a-125b5e6fcd66.png)


### 3. ルーティング・ポリシー

上の図のtk-ub18-nat3の中央にあるRouting Table&Routing Policyでは、tk-ub18-nat3に入ってきたパケットがどのインターフェースから出ていくべきかを、ルーティングテーブルやルーティング・ポリシーに従って判定しています。シナリオ毎に判定のロジックを決めておきます。

<b>シナリオ1:</b>
宛先がicosdemo1（10.87.22.50）のパケットが届いたら、eth3から外部ネットワークに送出する。
icosdemo1（10.87.22.50）からパケットが届いたら、eth0から外部ネットワークに送出する。

<b>シナリオ2:</b>
master02（10.87.22.12）からパケットが届いたら、eth0から外部ネットワークに送出する。
tk-ub18-dal1（10.240.0.19）からパケットが届いたら、eth3から外部ネットワークに送出する。
tk-ub18-dal2（10.240.0.13）からパケットが届いたら、eth4から外部ネットワークに送出する。



### 4. NAT構築準備

今回NATを構築するためにnftablesというツールを使います。ubuntu 18.04LTSには標準でインストールされていなかったのでインストールしておきます。作業者はすべてrootユーザです。

```bash
apt update
apt install -y nftables
systemctl enable nftables.service
```

LinuxをNATとして使用するためにはパケット転送を許可しておく必要があります（Defaultでは転送不可）。以下のコマンドを実行します。

```bash
echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/95-IPv4-forwarding.conf
sysctl -p /etc/sysctl.d/95-IPv4-forwarding.conf
```

### 5. ルーティング・ポリシーの登録

3章で定義したポリシーに従って、ルーティングテーブルとルーティング・ポリシーを追加していきます。
ただし標準のルーティングテーブル（netstat -rnまたはroute -nで表示するもの）はカスタマイズせず、シナリオ毎に専用のテーブルを追加して使うことにします。追加したテーブルにはインターフェースの特定以外の役割は持たせません。それ以上のことはルーティング・ポリシーとNATに担当させます。

以下のコマンドでテーブルを定義していきますが、インターフェース毎にデフォルトゲートウェイを定義して、テーブル番号を割り振っているだけです。目的は、ルーティング・ポリシーがテーブル番号を使ってインターフェースを選択できるようにすることです。
テーブル番号は1～252の範囲から選びます。ここでは下一桁をインターフェースの番号に合わせておきました。

```bash
ip route add table 100 0.0.0.0/0 via 172.18.0.1 dev eth0
ip route add table 103 0.0.0.0/0 via 172.18.1.1 dev eth3
ip route add table 104 0.0.0.0/0 via 172.18.1.1 dev eth4
```

次にルーティング・ポリシーを定義します。以下の最初の2行がシナリオ1用、後ろの3行がシナリオ2用のポリシーです。コマンドの中のprioは優先順位を定義しており、数字の小さい方が優先します。したがって例えば最後の行のポリシーは10.240.0.13から届いたパケットはtable 104に従ってeth4から送出されますが、宛先が10.87.22.50の時だけは、1行目のポリシーの方が優先するのでtable 103に従ってeth3から送出されます。

```bash
ip rule add to   10.87.22.50/32 table 103 prio 90
ip rule add from 10.87.22.50/32 table 100 prio 91
ip rule add from 10.87.22.12/32 table 100 prio 92
ip rule add from 10.240.0.19/32 table 103 prio 93
ip rule add from 10.240.0.13/32 table 104 prio 94
```

これらのルーティングテーブルとルーティング・ポリシーによって決定される経路は、ip route getで確認できます。前の4行がシナリオ1の往路と復路、後ろの4行がシナリオ2の往路と復路の経路を確認するコマンドです。

```bash
ip route get 10.87.22.50 from 10.240.0.19 iif eth0
ip route get 10.87.22.50 from 10.240.0.13 iif eth0
ip route get 10.240.0.19 from 10.87.22.50 iif eth3
ip route get 10.240.0.13 from 10.87.22.50 iif eth3

ip route get 10.240.0.19 from 10.87.22.12 iif eth3
ip route get 10.240.0.13 from 10.87.22.12 iif eth4
ip route get 10.87.22.12 from 10.240.0.19 iif eth0
ip route get 10.87.22.12 from 10.240.0.13 iif eth0
```

コマンド実行結果も載せておきます。

```bash
root@tk-ub18-nat3:~# ip route get 10.87.22.50 from 10.240.0.19 iif eth0
10.87.22.50 from 10.240.0.19 via 172.18.1.1 dev eth3 table 103
    cache iif eth0
root@tk-ub18-nat3:~# ip route get 10.87.22.50 from 10.240.0.13 iif eth0
10.87.22.50 from 10.240.0.13 via 172.18.1.1 dev eth3 table 103
    cache iif eth0
root@tk-ub18-nat3:~# ip route get 10.240.0.19 from 10.87.22.50 iif eth3
10.240.0.19 from 10.87.22.50 via 172.18.0.1 dev eth0 table 100
    cache iif eth3
root@tk-ub18-nat3:~# ip route get 10.240.0.13 from 10.87.22.50 iif eth3
10.240.0.13 from 10.87.22.50 via 172.18.0.1 dev eth0 table 100
    cache iif eth3

root@tk-ub18-nat3:~# ip route get 10.240.0.19 from 10.87.22.12 iif eth3
10.240.0.19 from 10.87.22.12 via 172.18.0.1 dev eth0 table 100
    cache iif eth3
root@tk-ub18-nat3:~# ip route get 10.240.0.13 from 10.87.22.12 iif eth4
10.240.0.13 from 10.87.22.12 via 172.18.0.1 dev eth0 table 100
    cache iif eth4
root@tk-ub18-nat3:~# ip route get 10.87.22.12 from 10.240.0.19 iif eth0
10.87.22.12 from 10.240.0.19 via 172.18.1.1 dev eth3 table 103
    cache iif eth0
root@tk-ub18-nat3:~# ip route get 10.87.22.12 from 10.240.0.13 iif eth0
10.87.22.12 from 10.240.0.13 via 172.18.1.1 dev eth4 table 104
    cache iif eth0
```

いずれのパターンも往路と復路で同じ経路が選択されています。特にシナリオ2の復路ではeth3とeth4が使い分けられることも確認できました。


### 6. nftablesによるNATの定義

nftablesはiptablesの後継で、iptablesと同様にLinux内を通過するパケットの破棄やヘッダ操作が可能であり、ファイアウォールやNATを構成することができます。今回はDNATおよびSNATの構成に利用します。

まずNAT定義用のテーブルを作成し、PreroutingとPostroutingのチェーンを追加します。テーブル名をnat、チェイン名をpreroutingとpostroutingにしていますが、名前はなんでも大丈夫です。{}の中の定義の方が意味を持っており、この部分はそのまま記述して下さい。タイプがnatでpreroutingフックに紐付いたチェーンと、postroutingフックに紐付いたチェーンをペアで作成しています。

```bash
nft add table nat
nft -- add chain nat prerouting { type nat hook prerouting priority -100 \; }
nft add chain nat postrouting { type nat hook postrouting priority 100 \; }
```

次に各チェーンにルールを登録していきます。

まずDNATのルールを登録します。以下の各コマンドが2章で示した図のPreroutingの青い箱に対応しています。

```bash
nft add rule nat prerouting iifname eth0 tcp dport 5000  dnat to 10.87.22.50:5000
nft add rule nat prerouting iifname eth3 tcp dport 22  dnat to 10.240.0.19:22
nft add rule nat prerouting iifname eth4 tcp dport 22  dnat to 10.240.0.13:22
```

続いてSNATのルールを登録します。各命令が2章で示した図のPostroutingのオレンジの箱に対応しています。

```bash
nft add rule nat postrouting oifname "eth0" snat to 172.18.0.29
nft add rule nat postrouting oifname "eth3" snat to 172.18.1.58
```

登録した結果を確認します。

```bash
nft --handle --numeric list table nat

root@tk-ub18-nat3:~# nft --handle --numeric list table nat
table ip nat {
        chain prerouting {
                type nat hook prerouting priority -100; policy accept;
                iifname "eth0" tcp dport 5000 dnat to 10.87.22.50:5000 # handle 5
                iifname "eth3" tcp dport ssh dnat to 10.240.0.19:ssh # handle 6
                iifname "eth4" tcp dport ssh dnat to 10.240.0.13:ssh # handle 7
        }

        chain postrouting {
                type nat hook postrouting priority 100; policy accept;
                oifname "eth0" snat to 172.18.0.29 # handle 8
                oifname "eth3" snat to 172.18.1.58 # handle 9
        }
}
```

nftablesによるDNATおよびSNATの定義は以上です。


### 7. 疎通確認

NATの構成が完了したので、シナリオが成立していることを実際の動作で確認します。シナリオを再掲します。

+ <b>シナリオ1:</b> 左端のVSI tk-ub18-dal1またはtk-ub18-dal2から、右端のVSI icosdemo1上でPort 5000を開いて稼働するWebアプリを呼び出す
+ <b>シナリオ2:</b> 右端のVSI master02から、左端のVSI tk-ub18-dal1またはtk-ub18-dal2にsshログインする

シナリオ1はport 5000で待ち受けているWebアプリです。アプリの説明は省略しますが、curlコマンドで172.18.0.29:5000を呼び出して、アプリから応答が返ってくることを確認できました。

```bash:シナリオ1
root@tk-ub18-dal1:~# curl 172.18.0.29:5000/api/echo
{"api":"echo","nodename":"null","pod_ip":"null","podname":"null","query_string":[""],"timestamp":"2020-06-29T13:21:02.071753"}
root@tk-ub18-dal1:~#

root@tk-ub18-dal2:~# curl 172.18.0.33:5000/api/echo
{"api":"echo","nodename":"null","pod_ip":"null","podname":"null","query_string":[""],"timestamp":"2020-06-29T06:31:09.500977"}
root@tk-ub18-dal2:~#
```

シナリオ2はsshログインです。172.18.1.58および172.18.1.46を呼び出して、それぞれtk-ub18-dal1およびtk-ub18-dal2にログインできることを確認できました。

```bash:シナリオ2（その1）
root@master02:~# ssh -i .ssh/id_rsa root@172.18.1.58
Enter passphrase for key '.ssh/id_rsa':
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-54-generic x86_64)
（中略）
Last login: Mon Jun 29 06:32:14 2020 from 172.18.0.33
root@tk-ub18-dal1:~#
```

```bash:シナリオ2（その2）
root@master02:~# ssh -i .ssh/id_rsa root@172.18.1.46
Enter passphrase for key '.ssh/id_rsa':
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-54-generic x86_64)
（中略）
Last login: Mon Jun 29 06:33:08 2020 from 172.18.0.33
root@tk-ub18-dal2:~#
```



### 8. 定義の永続化

ここまで実施した手順のうち、5章、6章の操作は、このままLinuxをリブートするとリセットされてしまいます。
リブートしてもNAT構成を維持できるように、定義を永続化する方法を最後に確認しておきます。

まずnftablesの定義を永続化します。nftablesをインストールした際にnftablesというサービスをenableにしましたが、このサービスが起動時に/etc/nftables.confを読み込むので、このファイルに定義を出力しておきます。以下のコマンドを実行します。nftablesはこれだけで大丈夫です。

```bash
nft list ruleset > /etc/nftables.conf
```

次にルーティング・ポリシーを永続化します。ubuntu 18.04の場合はnetplanの定義を作成することにより永続化することができます。ip route, ip ruleの実行結果をnetplanの形式でファイルに書き出すことは残念ながらできないようです。

5章のip route, ip ruleの設定と同等のnetplanの例を以下に紹介します。
/etc/netplanには初期状態で/etc/netplan/50-cloud-init.yamlがあるので（他のファイル名の場合もあるようですが）、これをコピーして編集します。名前でソートして後ろにくるファイルの定義が前のファイルの重複する定義を置き換えるので、50-cloud-init.yamlより後ろに来るように命名します（51-で始まる名前なら後ろに来ます）。例えば/etc/netplan/51-override.yamlにコピーして編集します。
詳細は説明しませんが、ポイントはroutes:とrouting-policy:の部分です。it routeコマンド／it ruleコマンドの内容や構成図と見比べていただいたら理解できると思います。
定義はnetplan applyコマンドで反映できます。もちろんrebootでも反映されます。

```yaml:51-override.yaml
# the datasource.  Changes to it will not persist across an instance.
# To disable cloud-init's network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 172.18.0.29/24
            match:
                macaddress: 06:72:7d:39:b7:50
            nameservers:
                addresses:
                - 161.26.0.10
                - 161.26.0.11
                search: []
            routes:
            -   to: 0.0.0.0/0
                via: 172.18.0.1
                table: 100
            routing-policy:
              - from: 10.87.22.50/32
                table: 100
                priority: 91
              - from: 10.87.22.12/32
                table: 100
                priority: 92
              - to: 10.240.0.0/24
                table: 100
                priority: 100
            set-name: eth0
        eth3:
            addresses:
            - 172.18.1.58/24
            match:
                macaddress: 06:bd:4d:65:82:77
            nameservers:
                addresses:
                - 161.26.0.10
                - 161.26.0.11
                search: []
            routes:
            -   to: 0.0.0.0/0
                via: 172.18.1.1
                table: 103
            routing-policy:
              - to: 10.87.22.50/32
                table: 103
                priority: 90
              - from: 10.240.0.19/32
                table: 103
                priority: 93
              - to: 10.87.22.0/26
                table: 103
                priority: 103
            set-name: eth3
        eth4:
            addresses:
            - 172.18.1.46/24
            match:
                macaddress: 06:16:19:af:27:c2
            nameservers:
                addresses:
                - 161.26.0.10
                - 161.26.0.11
                search: []
            routes:
            -   to: 0.0.0.0/0
                via: 172.18.1.1
                table: 104
            routing-policy:
              - from: 10.240.0.13/32
                table: 104
                priority: 94
            set-name: eth4
```


以上です。