クライアントは Compute Engine VM 上のOpenVPNサーバーに接続し、ステータスウィンドウには接続済みと表示されるのに、VPC 内のホストへの ping はどこにも届きません。直感的には push 行をもう一行足したくなりますが、それで直ることはありません。Compute Engine は自身のルートテーブルと、VM のインターフェースが実際に保持しているアドレスに基づいてパケットの行き先を決めるため、VM が所有していないトンネルプールは、誰も配送できないプールになります。

設定ファイルではなく失敗しているパケットを追いかけていくと、足りないものが順番に見えてきます。

死んだパケットから始める

SYN はクライアントを出てトンネルに入り、VPC 内の宛先に向かうカプセル化パケットとして VM に届きます。OpenVPN はそれをデカプセル化して eth0 から転送しようとしますが、そこでファブリックは何よりも先に一つの問いを突きつけます:このインスタンスは宛先アドレスを所有しているのか。他人のサブネットから切り出したトンネルプールはこの問いに落ち、パケットはルートやファイアウォールルールが参照される前に破棄されます。

つまりクライアントから VPC への方向には二つの別個の要件があり、どちらも push ディレクティブではありません。VM にはトンネルプールを覆うエイリアスレンジが必要で、それがこれらのアドレスの正当な所有者であることの根拠になります。VPC には、保護された各プレフィックスについて VM をネクストホップとして指定するルートが必要です。どちらか一方でも欠けると、パケットは別の場所で死に、クライアントからは同じ沈黙が見えるだけです。

その順序で障害を読んでください:クライアント側から見ると二つの条件はまったく同じに見えるからです。接続済みと報告しながら何も応答しないトンネルは、暗号方式の不具合ではなくトポロジの問題の兆候です。

インスタンスはトンネルプールを所有しているか

VM に自身のサブネット内のセカンダリレンジを与え、OpenVPN がちょうどそのレンジからアドレスを配布するようにします。プライマリアドレスが 10.156.0.10/20 のインスタンスでは、/28 のエイリアスにするとプールを他のインスタンスがすでに保持しているアドレスから切り離せます:

gcloud compute instances network-interfaces update openvpn-vm \
  --zone=europe-west1-b \
  --aliases=eth0:10.156.1.0/28

次に server ディレクティブが同じブロックを記述していなければなりません。その最初のアドレスがトンネルゲートウェイになり、残りのアドレスがクライアントに配布されます。ここでは二つの数字が一つのブロックを表します:ネットワークとネットマスクで、これが OpenVPN が今も期待するプール構文です。

server 10.156.1.0 255.255.255.240

アドレスが OpenVPN の設定の中だけでなく、実際にインターフェースに載ったことを確認します。ファブリックが転送するのは NIC が報告するアドレスであり、デーモンが信じているアドレスではありません。一つのコマンドで決着します:

ip -4 addr show dev eth0

eth0 に届かないセカンダリアドレスや、他のサブネットと重なるプールは、起動時のエラーではなく、後から断続的なドロップとして現れます。デーモンは起動し、クライアントも接続でき、一部の宛先だけが不調になります。プールをルートテーブルで見分けやすいよう、エイリアスレンジは小さく保ちます。他のインスタンスがすでに使っているサブネットから切り出すのはやめてください。

戻りトラフィックにネクストホップを与える

VPC のホストからの応答はトンネルプール内のクライアントアドレス宛てで、そのアドレスは VPC が知るどのサブネットにも属しません。そのプールへのルートがないと、応答は一致するものが無い最初のホップで止まり、リクエスト自体は遠端で成功している可能性が十分あります。

gcloud compute routes create openvpn-return \
  --network=prod-vpc \
  --destination-range=192.168.127.0/24 \
  --next-hop-instance=openvpn-vm \
  --next-hop-instance-zone=europe-west1-b \
  --priority=800

クライアントの背後にあり VPC の内部にはないプレフィックスにも、同じルートが必要です。そうしたプレフィックスには、そのクライアントの ccd ファイルに iroute エントリも必要です。そのエントリはサーバー側の記帳で、どのクライアントがそのプレフィックスを所有するかをサーバーに伝えるもので、クライアント自身には何もインストールしません。

push リストは、接続してくるマシンにルートを追加するだけなので、会話のクライアント側のために取っておきます。VPC サブネットへの push 行と、プールへ戻る VPC ルートは同じ経路の二つの半分であり、どちらか片方だけでは、リクエストは運ぶが応答は運ばないトンネルになります。

ファイアウォール、転送、その間のホップ

VPC ファイアウォールはデカプセル化後のパケットを評価するため、イングレスルールはトンネルプールと、その経路で VM に届くサイトプレフィックスを許可しなければなりません。デフォルト拒否の姿勢はここで静かに失敗します:パケットはデカプセル化後に破棄され、OpenVPN のログには健全なセッションが表示されます。

終端ではなく転送を行う Linux ホストでは、net.ipv4.ip_forward が有効かどうかを確認します。所有のルールはそこでも当てはまります:保持していないサブネットのために転送するマシンは、あの VM と同じようにパケットを落とします。トンネルを疑う前に値を読んでください。

ちょうど一つのプレフィックスが失敗するとき

部分的な失敗は、経路の他の部分がすでに証明済みなので最も読みやすいケースです。まずルート一覧を見て、そのプレフィックスを覆うべきエントリの宛先範囲とネクストホップを読み、次にインスタンスの説明を見て、そのエイリアスレンジをサーバーが配布するプールと比べます。

gcloud compute routes list --filter=destination-range~10.164.0.0
 gcloud compute instances describe openvpn-vm --format=get(networkInterfaces[0].aliasIpRanges)

ルートが無ければそのプレフィックスにネクストホップが無い形で現れ、エイリアスが無ければパケットがインターフェースで落ちる形で現れます;クライアントから見ると似ていますが、この二つの出力ではまったく違います。両方ある場合、残る容疑者はそのプレフィックスを名乗るクライアントの ccd iroute です。

四つの値をトンネルのそばに書き留めます:エイリアスレンジ、サーバープール、push するプレフィックス、VPC ルートです。古いエイリアスはプールの全アドレスを壊し、古いルートは一方向を壊し、push の欠落はクライアントからプレフィックスを隠し、iroute の誤りはプレフィックスを誤ったピアへ送ります。次にサブネットを追加する人にはこの四つが一箇所に必要で、そのメモは一晩より安くつきます。

関連記事