Google Cloud PlatformのVPCにOpenVPNを使って家から接続する

# 動機
興味ない人は、[OpenVPNサーバーを立てる](#OpenVPNサーバーを立てる)まで飛ばしてね。

Google Cloud Platform(GCP)のCompute Engine(CE)に家からアクセスするには、CEに割り当てられたExternal IPを使うのが普通だと思う。当然、このIPはstaticにしたい。でも、staticなExternal IPは各regionにつき一つしか取得できないという事情もある。この制約のために、例えば、asia-northeast region (Tokyo)にCEをたくさん作って、それを家から管理するには、

- そのうちの一つにstatic External IPを割り当て、sshとかでアクセスし、そのCEからさらにほかのCEにアクセスする
- ephemeral External IPを使い、それぞれのCEに起動時に割り当てられるIPを毎回入力する。

のどちらかしか、ネイティブでの選択肢がないと思う(いや、あるのかもしれないがぱっと思いつかない)。これでは不便なので、OpenVPNを使って、

- 一つのCEにOpenVPNサーバーを立てて、サーバー経由で他のCEにアクセスする

という方法にしようと思った。このとき、各CEにはInternal IPでアクセスできるようにする。Internal IPならいくつでもstaticなものをもらえるから、IPをhostsファイルに登録するなりできるので便利。ここまでは良かったのだけど、ここからが泥沼だったので、ここにメモしておく。また構築したくなるかもしれないしね。

# 完成予想図
まあ、先にどんな環境を作るのかを大まかに図解。フリーハンドでごめんなさい。

![IMG_1223.jpg](https://qiita-image-store.s3.amazonaws.com/0/126131/7d3048b5-7119-1e4d-15bb-b28254c5e26e.jpeg)


# 前提
今回は、以下の設定で環境構築をする。以降の説明では、OpenVPNサーバーを立てるCEをサーバー用CE、またはCE1で表す。VPCは、defaultの他に、my-vpcという名前のものを作った。CE1の他にもCE2を作り、my-vpcに接続している。また、CE2はdefault VPCとは接続しておらず、インターネット接続もない(ある程度隔離している)。

- Virtual Private Cloud(VPC)のサブネットのネットワークアドレス:<br/>
defaultのサブネット: 10.146.0.0/20 (default asia-northeast)<br/>
my-vpcのサブネット: 172.16.0.0/20 (自分で作った)
- OpenVPNのサブネットのネットワークアドレスとその割り当て:<br/>
ネットワークアドレス: 10.8.0.0/24<br/>
サーバー用CE(CE1): 10.8.0.1<br/>
家のPC: 10.8.0.2
- サーバー用CE(CE1)のIPアドレス:<br/>
eth0: 10.146.0.2 -> xx.xx.xx.xx (default VPCに接続)<br/>
eth1: 172.16.0.2 (my-vpc VPCに接続)
tun0: 10.8.0.1 (OpenVPNのサブネットに接続)
- テスト用CE(CE2)のIPアドレス:<br/>
eth0: 172.16.0.3 (default VPCに接続)
- 家のPCのIPアドレス:<br/>
eth0: 192.168.0.4/24 -> yy.yy.yy.yy
tun2: 10.8.0.2 (OPenVPNのサブネットに接続)

サーバー用CE(CE1)ではNICを二つ作り、eth0をdefault VPCに、eth1をmy-vpcに接続している。また、CE1ではOpenVPNのサブネットはtun0に、家のPCでもtun0に接続されている。さらに、my-vpcにはCE1の他にCE2を接続している。分かりにくいので、図にしてみた。

![IMG_1224.jpg](https://qiita-image-store.s3.amazonaws.com/0/126131/a7e45b80-0a90-ccaf-0712-043a327c94b9.jpeg)

# VPC、サブネット、CEを作る
ここは省略。

# OpenVPNサーバーを立てる <span id=hosfeogja></span>
## インストール
以降、各CEのOSはDebian9(Stretch)だとして話を進める。まずは、CE1にOpenVPNサーバーをインストールする。ここは、普通に

```shell
sudo apt install openvpn
```

でいいと思う。

## 証明書を用意する
OpenVPNは暗号化通信に公開鍵暗号方式を使っている。そのため、まずはいつも通り？証明書たちを作る必要がある。easy-rsaとかを使っているサイトが多いけど、ここではopensslを使ってみる(ふぬぅー)。

### CAを作る
```shell
openssl genrsa -out CA.key
openssl req -new -key CA.key -out CA.csr
openssl x509 -req -in CA.csr -signkey CA.key -out CA.crt
```
で秘密鍵CA.keyと公開鍵CA.crtが作れる。ただし、CA.crtはオレオレ証明(self-signed)なので、後々問題になるかもしれない。だから、今回はこの証明書で署名した公開鍵を作り、それをサーバ証明書に使う。二つ目のコマンドを実行した時に色々聞かれると思うけど、適当に答えていればいい。

### サーバ証明書を作る
```shell
openssl genrsa -out server.key
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr -CA CA.crt -CAkey CA.key -CAcreateserial -out server.crt
```
これで、サーバ証明書server.crtが作れた。今回も二つ目のコマンド実行時に質問されるけど、前回と全く同じ答えをすると、あとでself-signedと言われてしまうから(いや、そうなんだけど)、例えばCommon Nameとかを適当に変えて答える。

## サーバーの設定を行う
これに関しては(も？)、ほかのサイトがたくさんあるのでそちらを見てね。

- [OpenVPNのインストールとセットアップからインターネット接続までのガイドブック](https://qiita.com/noraworld/items/2fe6be489e1d93c748b8)

いくつか補足しておく。
一つ目は、DH鍵をopensslで作る方法。

```shell
openssl dhparam -out dhparam.pem 1024
```

とすればいい。
二つ目は、server.conf内で使われている`route`とか`push`の意味。routeはあとで説明するルーティングの設定を自動でやってもらうために使う。pushはクライアントの環境での設定にも適用することを指示する。だから、`route xxx`はサーバー側でしか設定されないけど、`push route xxx`はクライアント側でも設定される。

### サービスの設定
最近はサービスの管理はsystemdがやるようになっている。systemdに対して命令するために、systemctlコマンドを使う。

```shell
sudo systemctl start openvpn # サービスの起動
sudo systemctl enable openvpn # 自動起動設定
```

さっき貼ったリンクの記事でも書いてあるけど、openvpnがきちんと起動しているのかの確認は、`systemctl status openvpn`よりも`ps aux|grep openvpn`とかを使う方がいい。また、ポートがlistenであるかは、`netstat -al | grep openvpn`とかで確認できる。

参考までに、僕が初めて起動した時は、server.confの`tls-auth ta.key 0`でエラーが出ていた。確か、/var/log/daemonとかを見て見つけた気がする。それは、この行をコメントアウトして直した？。

## クライアントの設定を行う
ここでは、クライアントとしてMacを想定する。まず、OpenVPNクライアントソフトをどっかからダウンロード。僕は、[Tunnelblick](https://tunnelblick.net)を使った。次に、クライアント設定ファイルを作るのだけど、これについてもさっきのサイトに丸投げすることにするので、略。

これで、OpenVPN関係の設定は終わり。なんだけど、ここからが沼だった。。。

# サーバーCE(CE1)のネットワーク設定
## OpenVPNの動作
ネットワーク設定の前に、OpenVPNがどのようにVPNを実現しているのか軽く見ておく。とはいっても、これも他サイトに丸投げ。。
[OpenVPN: how secure virtual private networks really work](https://cloudacademy.com/blog/openvpn-how-secure-virtual-private-networks-really-work/)

要するに、OpenVPNのサブネットにtunとかいう名前のネットワークインターフェースが繋がっていると思えばいい。暗号化とかインターネット上のルーティングとかは考えずに、ただサーバーのtunインターフェースとクライアントのtunインターフェースが直接繋がっていると思えばいい。簡単だね。

## フォワーディングの設定
さっきの話からするに、サーバー内のルーティングに必要な設定は、tunインターフェースに入ってきたパケットをeth1インターフェースから送り出すことだけでいい。ように初めは思っていたのだが、実は他にもIPマスカレードの設定が必要だった(解説(ページ内): [RPF](#RPF))。
とりあえず、フォワーディングの設定をしていく。

Linuxの多くのディストリビューションでは、デフォルトでフォワーディングが無効なので、これを有効にする。それには、

```/etc/sysctl.conf
...(略)...
# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1 # コメントアウト
...(略)...
```
これを指示通りにコメントアウトすればいい。そして、

```shell
sudo sysctl -p
```
で設定の再読み込みをする。もしくは、

```shell
sudo sysctl -w net.ipv4.ip_forward=1
```
を実行してもいい。ちなみに、net.ipv4.ip_forwardの値は、

```shell
cat /proc/sys/net/ipv4/ip_forward
```
で確認できる(sysctl -v net.ipv4.ip_forwardでもできるけど)。逆に、ここに書き込んでもいい！！

本当にこれでフォワーディングが有効になったのか、確認してみよう。
設定前は、

```shell
ip route get 172.16.0.3 from 10.8.0.2 iif tun0
# => RTNETLINK answer: Invalid argument
```
だったのが、

```shell
ip route get 172.16.0.3 from 10.8.0.2 iif tun0
# => 172.16.0.3 from 10.8.0.2 via 172.16.0.1 dev eth1
```
に変わっている。

ちなみに、iif(Input/Incomming InterFace?)を指定しないと、ホストからの通信と解釈されて、10.8.0.2のIPアドレスを持つNICがないために、やはり`RTNETLINK answer: Invalid argument`となる。

話がそれるが、フォワーディングが無効な理由を考えてみる。これは、片方のNICはインターネットに、もう片方のNICはLANに繋がっている時を考えればわかる。フォワーディングが無条件に有効だと、インターネット上からLAN内が丸見えだ。当然、今回もフォワーディングを有効にしたので、これには注意しないといけない。

これに対処するために、ファイヤーウォールを使うのだけど、まずはインストールしないといけないよね。

## ファイヤーウォールの設定
ファイヤーウォールとして有名なのは、iptables、firewalld、ufwなどだろうが、今回はnftablesを使う。なんでや！？って突っ込まれそうだけど、それには、気分や！！ってボケてみる。

インストールは普通に、

```shell
sudo apt install nftables
```
でいい。これもまたサービスなので、

```shell
sudo systemctl start nftables # サービスの起動
sudo systemctl enable nftables # 自動起動設定
```
としておく。そして、/etc/nftables.confというファイルに

```/etc/nftables.conf
table ip nat {
	chain prerouting {
		type nat hook prerouting priority 0; policy accept;
	}

	chain postrouting {
		type nat hook postrouting priority 0; policy accept;
		ip saddr 10.8.0.0/24 oif "eth0" masquerade
		ip saddr 10.8.0.0/24 oif "eth1" masquerade
	}
}
table inet filter {
	chain input {
		type filter hook input priority 0; policy accept;
	}

	chain forward {
		type filter hook forward priority 0; policy drop;
		ip saddr 10.8.0.0/24 accept
		ip daddr 10.8.0.0/24 accept
	}
}
```
を書き込む。もしファイルがなくても、作ればいい。このファイルは、起動時に自動で読み込まれるファイルらしい。次に、

```shell
sudo nft -f /etc/nftables.conf
```
を実行して、ファイヤーウォールの設定を読み込む。

一応、内容の説明を軽くしておく。フォーマットは、

```
table #family #table_name {
    chain #chain_name {
        type #type hook #hook priority #priority; policy #default_policy
        #statement
        ...
    }
    ...
}
...
```
だ。ここで、

|id|任意or指定|説明|
|:--|:-:|:--|
|family|指定|ルールを適用する対象をざっくり指定。|
|table_name|任意|Netfilterのhookとは無関係。|
|chain_name|任意|Neffilterのタイプとは無関係。|
|type|指定|Netfilterのタイプと対応。|
|hook|指定|Netfilterのhookと対応。|
|priority|任意|優先度を指定。|
|default_policy|指定|デフォルトの動作を指定。|
|statement|指定|動作を指定|
である。詳しくは、man nft(8)を参照。

先ほど説明した、フォワーディングの脆弱性に対処するため、

```
chain forward {
    type filter hook forward priority 0; policy drop;
    ip saddr 10.8.0.0/24 accept
    ip daddr 10.8.0.0/24 accept
}
```
を加えている。

さらに、natテーブルのpostroutingチェインでIPマスカレードの設定を行なっている。ここでは、10.8.0.0/24というネットワークアドレスを持つIPが送信元であるパケット、つまり、OpenVPN経由で送られてきたパケットをeth1にフォワーディングしている。eth1はmy-vpcに接続されているので、CE2と同じサブネット内なのだが、送信元が10.8.0.0/24サブネット内になっていると、同じサブネット内であるにも関わらず通信ができない。これは、おそらくGoogleのルーターの[RPF](#RPF)のためだろう。

`ip saddr 10.8.0.0/24`は`iif "tun0"`でいいような気もするが、それはうまくいかない。openvpnサービスがnftablesサービスより後に起動する場合、nftablesサービスがこの文を読み込む時にはtun0インターフェースは存在しないので、エラーとなってしまうからだ。同様の理由で、natテーブルのpostroutingチェイン内でも`ip saddr 10.8.0.0/24`を使っている。

さて、ここで`ip saddr 10.8.0.0/24`という条件がIPマスカレードと干渉しないかきになると思う。10.8.0.x:yというアドレスが、例えば172.16.0.a:bに変換されたとしよう。ここで、ファイヤーウォールの条件指定として、172.16.0.a:bを使わなくても良いのかという問題だ。これは、元のIPで条件指定するのが正解だ。

tun0->eth1の時を考える。IPマスカレードはpostroutingで行われているので、ファイヤーウォールの条件確認で使われるIPアドレスは、マスカレード前のもの(10.8.0.x:y)だからだ。さらに、eth1->tun0の時を考える。このとき、マスカレードされたIPがもとに戻るのは、おそらくpreroutingだろうから(下記引用参照)、結局この場合も、ファイヤーウォールの条件確認で使われるIPアドレスは、マスカレード前のもの(10.8.0.x:y)だ。これについては、[wikipedia](https://en.wikipedia.org/wiki/Netfilter)に載っている図[Flow of network packets through the Netfilter](https://en.wikipedia.org/wiki/File:Netfilter-packet-flow.svg)が参考になる。

話が変わるけど、nftablesのデフォルトポリシーはacceptなので、filterテーブルのinputチェインは消去しても良いが、natテーブルのpostroutingチェインは消去してはならない。これは、IPマスカレード(IP masquerade)を有効にするためだ。

> you still have to add the prerouting nat chain, since this translate traffic in the reply direction. (from: [wikipedia](https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)#Masquerading))

以上の設定で、家のPCからVPCのサブネットにOpenVPN経由で接続できるようになったはずだ。だよね？

例えば、pingで確認してみる。家のPCで、

```shell
ping 172.16.0.3 # CE2のInternal IP
```
として返事が帰ってこれば成功。だが、この成功にはもう少しカラクリがある。おぉ！？以下の[ICMPのIPマスカレード](#ICMPのIPマスカレード)を参照。

返事が返ってこない時は、ipルーティングを確認してみるとよい。CE2上で、

```shell
ip route get 10.8.0.2
```

の結果が、

```
10.8.0.2 via 172.16.0.1 dev eth0 src 172.16.0.xx
```

となっていれば良い。172.16.0.1はGoogleが用意したVPC用のルーターのIPアドレス。こうなっていなければ、

```shell
ip route add 10.8.0.0/24 via 172.16.0.1 dev eth0
```

でルーティングを追加する。

これでもダメなら、[余談 双方向通信](#双方向通信)を試して見ると良い。

===== お 　
 か 
　 わ　　り =====

# 余談
## トラブルシューティング
何かトラブルが起こった時には、pingやtracerouteはもちろん、その他にもtcpdumpを使ってみるといいかも。例えば、

```shell
sudo tcpdump -en -i eth1 -Q in icmp
```
でeth1インターフェースに入ってくるicmpプロトコル通信をキャプチャできる。

|オプション|説明|
|:--|:--|
|e|Macアドレスを表示|
|n|ホスト名ではなくIPアドレスで表示|
|-i {interface}|対象インターフェースを指定|
|-Q in/out|キャプチャする通信の方法を指定|

ほかにも、pingの代わりにarpingを使うのもいい。ICMPの代わりにARPリクエストを投げる。対象ホストがICMPをfirewall等で遮断している時にも使える。ただし、相手が同じネットワークに入っている必要がある。導通確認のほかにも、パケットチャプチャと組み合わせたりすると相手のMacアドレスがわかる。

## VPCの同一サブネットにあるCE間の通信
同じサブネット内のCE同士の通信もGoogleのルーターを経由する。実際、CE1で

```shell
ip route
```
を実行すると、

```
...(略)...
172.16.0.0/20 via 172.16.0.1 dev eth1
172.16.0.1 dev eth1 scope link
```
となる。ここで、172.16.0.0/20はmy-vpc VPCのサブネットのネットワークアドレスだった。CE1はmy-vpcのサブネットに接続しているにも関わらず、他のCEと通信するためには、172.16.0.1というIPアドレスを持つ機器(ルーター)を経由することになっている。他のCEと直接通信はできないのだ。

## RPF
RPFとはReverse Path Filter/Forwardの略。これは、送信元IPを使ったフィルタリングのこと。送信元IPは簡単に偽造できるのは知っての通り。これをIP spoofといい、攻撃の手段となり得る。例えば、Dos攻撃をする際にパケットが戻ってきては面倒なので、別のIPアドレスを送信元にすることでそれを防ぐことができる。また、なりすましにも使える。これを防ぐため、Linux(のディストリビューション？)ではデフォルトでrp_filterが有効になっている。このフィルタは、送信元IPが、パケットを受け付けたNICとは別のNICが接続しているネットワーク内のものであった時にそれを排除する。

排除された通信はmartianとして扱われる。martianとは"火星人の"という意味の形容詞で、おかしな通信だからきっと火星から来たんだろう(笑)という意味だと思われる。

Linuxと同じく、どうやらVPCで使われるルーター(172.16.0.1など)も、これが有効になっているらしい。例えば、送信元IPが10.8.0.2などである時、10.8.0.2はプライベートアドレスなので、自身が接続している172.16.0.0/20というサブネット内にあるのはおかしいという判断がなされるのだと思う。そのため、IPマスカレードを有効にして10.8.0.0/24と172.16.0.0/20をフォワーディングしないと通信がルーター(172.16.0.1)で止まってしまう(少なくともそのように見える)のだろう。


ここで気になるのは、送信元IPがグローバルIPだった時に遮断されるのかということだが、確認が面倒なので成否は不明だ(誰かやって〜）。


[Google Cloud](https://cloud.google.com/vpc/docs/using-routes)のEnabling IP forwarding for instancesによると、

> By default, GCP performs strict source and destination checking for packets so that:
1. VM instances can only send packets whose sources are set to match an internal IP address of its interface in the network.
2. Packets are only delivered to an instance if their destinations match the IP address of the instance's interface in the network.

らしい。つまり、送信元IPがプライベートだろうがグローバルだろうが関係なかった。

## ICMPのIPマスカレード
IPマスカレードはIPアドレスの他にポートも変換することで、一つのIPアドレスに多くのIPアドレスを対応させることができるといった説明をよく見かける。しかし、よく考えてみると、その方法ではTCPでもUDPでもないICMPについて、IPマスカレードはできないはず。だが、実際にはできている。これは、ICMPについては特別な配慮がなされているからだ。

> You probably have to compile a later 2.0.36 kernel to add this [icmp masquerading] support. (from: [linuxgazette.net](https://linuxgazette.net/issue42/tag/19.html))

# 追記
## IPマスカレードはしなくていい
CEを作る時にIP forwardingを有効にすれば、IPマスカレードをしなくても通信できる。(参考: [Google Cloud](https://cloud.google.com/vpc/docs/using-routes))

## 双方向通信
VPNを通して、家のPCからVPCにアクセスする方法をこの記事では書いたのだけど、実は逆もできる。すなわち、VPC内のCEから家のPCにアクセスする。そのためには、VPCに新しいROUTEを加える必要がある。今回の場合だとROUTEの設定は、

|項目|内容|
|:--|:--|
|Network|my-vpc|
|Dst IP range|10.8.0.0/24|
|Next hop|Specify an instance|
|Next hop instance|CE1|

とすればいい。