vpc
OpenVPN
gcp

Google Cloud PlatformのVPCに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


前提

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


  • Virtual Private Cloud(VPC)のサブネットのネットワークアドレス:

    defaultのサブネット: 10.146.0.0/20 (default asia-northeast)

    my-vpcのサブネット: 172.16.0.0/20 (自分で作った)

  • OpenVPNのサブネットのネットワークアドレスとその割り当て:

    ネットワークアドレス: 10.8.0.0/24

    サーバー用CE(CE1): 10.8.0.1

    家のPC: 10.8.0.2

  • サーバー用CE(CE1)のIPアドレス:

    eth0: 10.146.0.2 -> xx.xx.xx.xx (default VPCに接続)

    eth1: 172.16.0.2 (my-vpc VPCに接続)
    tun0: 10.8.0.1 (OpenVPNのサブネットに接続)

  • テスト用CE(CE2)のIPアドレス:

    eth0: 172.16.0.3 (default VPCに接続)

  • 家のPCのIPアドレス:

    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


VPC、サブネット、CEを作る

ここは省略。


OpenVPNサーバーを立てる


インストール

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

sudo apt install openvpn

でいいと思う。


証明書を用意する

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


CAを作る

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)なので、後々問題になるかもしれない。だから、今回はこの証明書で署名した公開鍵を作り、それをサーバ証明書に使う。二つ目のコマンドを実行した時に色々聞かれると思うけど、適当に答えていればいい。


サーバ証明書を作る

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とかを適当に変えて答える。


サーバーの設定を行う

これに関しては(も?)、ほかのサイトがたくさんあるのでそちらを見てね。

いくつか補足しておく。

一つ目は、DH鍵をopensslで作る方法。

openssl dhparam -out dhparam.pem 1024

とすればいい。

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


サービスの設定

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

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を使った。次に、クライアント設定ファイルを作るのだけど、これについてもさっきのサイトに丸投げすることにするので、略。

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


サーバーCE(CE1)のネットワーク設定


OpenVPNの動作

ネットワーク設定の前に、OpenVPNがどのようにVPNを実現しているのか軽く見ておく。とはいっても、これも他サイトに丸投げ。。

OpenVPN: how secure virtual private networks really work

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


フォワーディングの設定

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

とりあえず、フォワーディングの設定をしていく。

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


/etc/sysctl.conf

...(略)...

# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1 # コメントアウト
...(略)...

これを指示通りにコメントアウトすればいい。そして、

sudo sysctl -p

で設定の再読み込みをする。もしくは、

sudo sysctl -w net.ipv4.ip_forward=1

を実行してもいい。ちなみに、net.ipv4.ip_forwardの値は、

cat /proc/sys/net/ipv4/ip_forward

で確認できる(sysctl -v net.ipv4.ip_forwardでもできるけど)。逆に、ここに書き込んでもいい!!

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

設定前は、

ip route get 172.16.0.3 from 10.8.0.2 iif tun0

# => RTNETLINK answer: Invalid argument

だったのが、

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を使う。なんでや!?って突っ込まれそうだけど、それには、気分や!!ってボケてみる。

インストールは普通に、

sudo apt install nftables

でいい。これもまたサービスなので、

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
}
}


を書き込む。もしファイルがなくても、作ればいい。このファイルは、起動時に自動で読み込まれるファイルらしい。次に、

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のためだろう。

ip saddr 10.8.0.0/24iif "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に載っている図Flow of network packets through the Netfilterが参考になる。

話が変わるけど、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)


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

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

ping 172.16.0.3 # CE2のInternal IP

として返事が帰ってこれば成功。だが、この成功にはもう少しカラクリがある。おぉ!?以下のICMPのIPマスカレードを参照。

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

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アドレス。こうなっていなければ、

ip route add 10.8.0.0/24 via 172.16.0.1 dev eth0

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

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

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


余談


トラブルシューティング

何かトラブルが起こった時には、pingやtracerouteはもちろん、その他にもtcpdumpを使ってみるといいかも。例えば、

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で

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の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)



追記


IPマスカレードはしなくていい

CEを作る時にIP forwardingを有効にすれば、IPマスカレードをしなくても通信できる。(参考: Google Cloud)


双方向通信

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

とすればいい。