VPNクライアントのサービスに到達する:リバーストンネルかDNATか
ポート転送なしでOpenVPNクライアント上のサービスに到達する方法:リバースSSHトンネルとサーバー側DNAT、そして選択を決める三つの条件を比較します。
キャリアNATの背後にあるノートPCでも、VPNサーバーが待ち合わせ地点になれば同僚にサービスを提供できます。そこへ至る仕組みは二つあり、クライアントが外向きに接続してサーバーに待ち受けポートを保持してもらうか、サーバーがポートを公開してトンネルへ変換するかです。どちらも同じ到達性の問題を解決しますが、障害・監視・ログの扱いは異なります。どちらを選ぶべきかは三つの条件で決まります。
二つの形、一つの待ち合わせ地点
一つ目の形では、クライアントが外向きにSSHセッションを張り、選んだポートで待ち受けるようサーバーに依頼し、各接続をそのセッション経由で戻します。サーバー側では何も転送されず、パケット変換も起きないため、必要なのはSSHアカウント、許可された転送、そのポートのファイアウォール規則だけです。サービスはクライアントのループバックに束縛されたままで、通常はこれが望ましい形です。
ssh -N -R 8081:127.0.0.1:8080 -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 [email protected]
二つ目の形ではサーバーが公開ポートを所有し、各接続をクライアントのトンネルアドレスへ書き換えます。そのためにはIP転送の有効化、入口での宛先変換、出口での送信元変換が必要で、SSHセッションが存在しなくても動作し続けます。ポートはセッションではなくサーバーに属し、この性質が実際のほとんどのケースを決めます。
どちらの形も構造上より安全というわけではありません。どちらもサーバーの公開アドレスに到達できる相手に一つのサービスポートを公開し、どちらもサービス自身の認証に依存します。
第一の条件:ポートはセッションより長く生きる必要があるか
リバース転送は、それを作成したSSHセッションと同じだけの寿命しかありません。回線が切れると待ち受けは消え、同僚は遅い経路ではなく接続拒否を受け取ります。コマンドをsystemdユニットと再起動ポリシーで監視すれば解決し、ExitOnForwardFailure=yes はバインド失敗を即座に見えるようにして、転送なしで動き続けるセッションを残しません。
変換されたポートは、規則が永続化されていれば再起動を生き延びますが、クライアントが同じトンネルアドレスを保つことに依存します。再接続後にノートPCへ別のアドレスを渡す動的プールは、規則を静かに何もない場所へ向けます。まずトンネルアドレスを予約し、そのあとに規則を書き、両者を一つの変更として扱ってください。
誰も見ていない午前三時にサービスが応答しなければならないなら、起動したままにする必要があるプロセスがトンネルだけなので、サーバーが公開するポートの形のほうが可動部が少なくて済みます。
第二の条件:ファイアウォールとログは誰のものか
リバースの形では、すべてのポリシーが二か所に収まります。sshd は AllowTcpForwarding を通じてリモート転送をそもそも許可するかを決め、GatewayPorts は待ち受けをループバックに束縛するか全アドレスに束縛するかを決めます。既定値 no のままでは、転送が成功してもサーバー外の同僚はポートに到達できず、これは束縛アドレスの問題なのに壊れたトンネルのように見えます。
sudo ss -ltn '( sport = :8081 )'
sudo grep -E '^(GatewayPorts|AllowTcpForwarding)' /etc/ssh/sshd_config
変換の形では転送の有効化と二つの規則が必要で、二つ目の規則が多くの手順書で間違える箇所です。送信元変換を conntrack の DNAT 状態に限定するということは、宛先規則が作成した接続だけが書き換えられ、サーバーがトンネルへ送るすべてのパケットが書き換えられるわけではありません。
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8081 -j DNAT --to-destination 10.88.0.6:8080
sudo iptables -t nat -A POSTROUTING -o tun0 -m conntrack --ctstate DNAT -j SNAT --to-source 10.88.0.1
その書き換えは帰属も破壊します。以後クライアントはすべての呼び出し元をサーバーのトンネルアドレスとして見るため、アドレスごとのレート制限は一つのバケットに潰れ、誰が何を送ったかが隠れます。帰属が重要な場合は、サーバー側で接続を終端するか、許可の判断をサーバーに残したうえで、サービスが信頼するヘッダーで識別情報を渡してください。この二つの修正のどちらを選ぶかは、サービスがプロキシヘッダーを読むかどうかで決まります。
第三の条件:一つのポートか複数か、名前は誰のものか
一つのポートに一つのサービスという場合は、手間の面でリバース転送が勝ちます。コマンド一つ、アカウント一つ、ファイアウォール規則一つで済み、後片付けはセッションを切るだけです。同じ公開アドレスに複数のサービスを置くなら、リバース転送ごとにサーバー側のポートが必要でサービスが増えるほど集合が大きくなるため、サーバー公開の形に寄っていきます。
名前はさらに複雑にします。リバース転送はSNIで振り分けるTCPプロキシの背後に置けますが、変換されたポートは通常サービスごとに一つです。サービス自身がTLSを終端し元のホスト名を必要とする場合、プロキシ方式ならそれを保てますが、DNAT方式で保てるのはサービスがそのポートでその名前を受け入れる場合だけです。
三つの条件のうちどれが形を決めたのかを書き留めてください。そうしないと、二つ目のサービスを追加する次の人が、より少ない文脈で同じ比較をやり直すことになります。
決めてから、外側から試す
ノートPCへの経路がないネットワークから経路を試し、要求が走っている間にサーバーのカウンターを観察してください。サーバーから自分自身の公開アドレスへ試しても、パケットはローカルで生成されて入口の変換フックに決して届かないため、ここでは何も証明できず、実際の経路が壊れていてもテストは通ってしまいます。
sudo iptables -t nat -L PREROUTING -n -v
sudo tcpdump -ni any 'tcp port 8081' -c 20
カウンターが動かないままなら、パケットは規則に到達しておらず、プロバイダーかホストのファイアウォールを示します。サービスが拒否しているのにカウンターが動くなら、クライアント自身のファイアウォールか、サービスが待ち受けているアドレスを示します。パケットを運びながら何も返さないトンネルは、経路または戻り経路の問題であり、しかもこの三つの結果はいずれも、クライアントのアダプターを編集して調べるべきものではありません。何かを変更する前に、次のテストがその基準値を必要とするので、どのカウンターが動いたかを書き留めてください。
形が動くようになったら、ポート、所有者、監視方法をトンネル設定のそばに記録してください。誰かのシェル履歴にしか存在しない到達性の修正は、再起動一回で再発インシデントになります。