外側から始めた。ユーザーがするように、スマートフォンのブラウザでモバイルデータからこの名前を打ち、接続がタイムアウトまで固まったままになるのを見ていた。名前は解決できたので DNS は問題ない。自宅ルーターのポート転送ページも行き止まりだった。WAN アドレスが 100.64.31.42 と表示されていて、これはプロバイダがインターネット側から私のところへ経路を張れるアドレスではない。この数字がすべてを説明する。回線はキャリアグレード NAT の背後にあり、トンネルのこちら側のグローバルアドレスはプロバイダのものだ。以下は、固まったタブから応答するサービスまでの、私が実際にたどった道筋である。

クライアントから始めて内側へ

どんなルールを変える前に、どこで連鎖が止まるかを確かめる。ノート PC で dig +short files.example.net を実行すると VPS のアドレスが返り、手元の DNS が古いという可能性は消えた。次に curl -v --connect-timeout 5 files.example.net:5022 は拒否ではなくタイムアウトになった。この違いは重要だ。タイムアウトは途中でパケットが捨てられていることを、拒否は何かが答えて「違う」と言ったことを意味する。

次に内側へ一歩進んで VPS 自体を見る。ss -ltnp は 5022 で待ち受けているものを何も示さなかったが、DNAT ルールができる前なら当然だ。同じマシンで wg show(OpenVPN なら ip -br a show tun0)を使い、自宅側のピアが稼働していてハンドシェイクが新しいことを確認した。ピアが落ちていれば NAT ルールは何の役にも立たず、自宅ルーターの VPN ページの緑ランプも意味を持たない。

自宅回線が本当に CGNAT の背後にあると確認する

ルーターが WAN インターフェースで報告するアドレスと、外部サービスが見るアドレスを比べる。100.64.0.0/10、つまり 100.64.x.x から 100.127.x.x までの範囲は RFC 6598 の共有アドレス帯で、インターネットからは到達できない。ルーターが普通のプライベートアドレスだと主張するのに、外部からの照会が無関係な結果を返す場合も結論は同じだ。CGNAT 回線では、次のリースまでしか持たないグローバルアドレスが割り当てられることがあり、ポート転送が一時間だけ動いて静かに死ぬことがある。

プロバイダによっては本当のグローバルアドレスを渡しながら、家庭向け契約で受信 80 と 443 を塞いでいる。サービスがウェブだけで WAN アドレスが本当にグローバルなら、中継を組む前にサポートへ聞いてほしい。そうでなければ、以下の設計はどのポートにも等しく当てはまる。ルーター側の実験はやめる価値がある。プロバイダ網の中の変換装置が、接続があなたの WAN インターフェースに届く前に終わらせてしまうからだ。

名前を VPS のアドレスに向ける

DNS は通信を中継しない。だからサービスの A レコードには VPS のグローバルアドレスを入れる。192.168.1.20 を指す A レコードは、すでに VPN の中にいるクライアントにしか役に立たない。インターネット側から見れば、誰も経路を張れないアドレスに解決される。VPS 自身の管理アクセスには別の名前を用意して、あるレコードがどのホストを指すのかを混同しないようにしている。

VPS と自宅マシンの両方が HTTP を提供するなら、それぞれに名前を与え、どちらが答えるかは VPS に決めさせる。一つの名前にまとめて、プロキシが正しいバックエンドを選ぶことに賭けるのは、午後を丸ごと間違ったホストのデバッグに費やす近道だ。

一つのポートだけ DNAT し、SSH は 22 に残す

直接的な方法は VPS での宛先 NAT だ。動作中のカーネルで転送を有効にし、ルールには自宅サーバーのトンネルアドレスを書き、LAN アドレスは書かない。

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -p tcp -d 198.51.100.10 --dport 5022 -j DNAT --to-destination 10.8.0.2

VPS 自身への SSH は 22 のままにし、別のポート、たとえば 5022 を自宅マシンへ転送する。22 を停止中のマシンへ転送すると、それを直せる唯一の経路を失う。同じ理屈はプロキシにも当てはまる。VPS の管理ポートは中継ルールの外に置く。

ルールが動いたら永続化する。Debian や Ubuntu なら apt install iptables-persistent のあと netfilter-persistent save で /etc/iptables/rules.v4 に書き込まれる。firewalld のホストでは、ゾーンに恒久的なポート転送を置くのが同等で、反映には firewall-cmd --reload が要る。メモリの中だけに生きるルールは、プロバイダの最初のメンテナンス再起動で消える。たいていは良くない時間帯だ。

戻り経路を直すか、必要としない形にする

DNAT は宛先だけを書き換える。自宅サーバーは元のクライアントアドレスからの接続として見え、そのデフォルトルートが自宅ルーターを向いていると、応答は家の WAN から出て行き、プロバイダの変換装置に落とされる。この非対称性が、DNAT ルールが完璧に見えたあとに私が踏んだ二つ目の障害だ。

二つの直し方が効く。自宅マシンにポリシールートを与え、トンネルから来た通信をトンネルへ返す。

ip rule add from 10.8.0.2 table 100
ip route add default dev wg0 table 100

あるいは VPS に送信元を書き換えさせ、応答が到達できるアドレスを持たせ、出ていくときに conntrack で戻す。

iptables -t nat -A POSTROUTING -d 10.8.0.2 -p tcp --dport 5022 -j SNAT --to-source 10.8.0.1

SNAT の版は本当のクライアントアドレスを自宅サーバーから隠すので、アプリケーションのログには相手として 10.8.0.1 が残る。元のアドレスをアプリケーション層で渡すか、クライアント単位の記録を失うことを受け入れるか。ただし、障害の最中に気づくのではなく、意識して決めておくこと。

外側から経路全体を確かめる

VPS でも自宅でもないネットワークから検証する。モバイルデータのスマートフォンや、どこかの安いシェルでよい。nc -vz files.example.net 5022 は接続成功を報告し、openssl s_client -connect files.example.net:443 -servername files.example.net はグローバル名の証明書を表示するはずだ。そのうえでプロバイダのセキュリティグループやクラウドファイアウォールを確認する。パケットを一度も見ない DNAT ルールは、たいていホストの一段上で止められている。

最後に、トンネルが落ちたときに何が起きるかを決める。DNAT ルールは接続を受け続け、それを静かに死んだトンネルへ流し込む。壊れたサービスと見分けがつかない。ピアが落ちている間はそのポートを拒否するヘルスチェックを VPS に置けば、数行のコードで一時間の当て推量が節約できる。その後はルールをいじるのをやめ、一日眺めてから信頼すること。

関連記事