自宅サーバーでVPNを構築したら、ネットワークの落とし穴に4回ハマった話
きっかけ
「外出先から自宅のサーバーに安全にアクセスしたい」
きっかけはシンプルでした。自宅にUbuntuのデスクトップPCがあり、その中に仮想マシン(KVM)を立てていろいろ実験している環境があります。これを外からスマホやノートPCで触れるようにしたくて、WireGuardというVPNソフトを使ってVPNサーバーを構築することにしました。
WireGuard自体のセットアップはとてもシンプルで、公式ドキュメント通りにやれば30分もかかりません。ところが、実際の環境で動かすまでに4つの落とし穴にハマりました。
この記事は「VPNの設定方法」というよりも、インフラ構築で思いもよらないところでつまずき、原因を切り分けて一つずつ解決していった記録です。
まずは環境の説明
ざっくり言うと、こんな構成です。
インターネット
↓
ルーター(192.168.0.1)
↓
ホストPC(Ubuntu 22.04 / 192.168.0.10)
└── 仮想マシン「ubuntu-server」(192.168.0.11)
└── WireGuardサーバーホストPCの中にKVMで仮想マシンを立てて、そこにWireGuardを入れています。仮想マシンをLAN上の独立した1台として扱うために、ブリッジ(br0) というネットワークの仕組みを使っています。
ブリッジとは、ホストPCの物理的なネットワークカード(NIC)と仮想マシンの仮想NICを「橋渡し」して、仮想マシンにもLAN上の固有IPアドレスを持たせるものです。
このブリッジが、後々すべてのトラブルの舞台になるとは……。
WireGuardのセットアップ自体は簡単だった
WireGuardの導入は本当にシンプルでした。
仮想マシンに `apt install wireguard` でインストール
サーバーとクライアント(スマホ・PC)の鍵ペアを生成
設定ファイルを書く
ルーターでポート開放(51820/UDP)
ここまではドキュメント通り。ものの20分で完了です。
さて、スマホのWireGuardアプリで接続ボタンを押してみます。
……繋がらない。
落とし穴1:パケットがホストPCで止まっていた
VPN接続しようとしても、ハンドシェイク(最初の認証のやり取り)が一向に成立しません。
こういうときはまず 「どこまでパケットが届いているか」 を調べます。`tcpdump` というツールでネットワークを流れるパケットを覗いてみました。
ホストPCの物理NIC → パケットが来ている ✅
ブリッジ(br0) → パケットが来ていない ❌つまり、パケットはインターネット経由でホストPCまでは届いているのに、そこから仮想マシンに転送されていない。
原因を調べると、libvirtd(KVMの管理デーモン)が自動的にiptablesのファイアウォールルールを追加していて、「新規の接続はすべて拒否」というルールがWireGuardのパケットより先に適用されていました。
libvirtdは仮想マシンを安全に隔離するためにこのルールを入れているのですが、今回のようにブリッジ経由でVMに直接通信させたい場合にはこれが邪魔をします。
解決策: iptablesのFORWARDチェーンで、libvirtのルールより前に「51820/UDPは通す」というルールを挿入しました。
これでようやくVPN接続が成立。スマホから自宅ネットワークにアクセスできるようになりました。
……と思ったのも束の間。
落とし穴2:数時間経つとSSHが繋がらなくなる
VPN構築の翌日、ホストPCにSSHで接続しようとしたら繋がりません。再起動すると直るのですが、数時間するとまた繋がらなくなる。
いろいろ調べた結果、原因はブリッジ(br0)のMACアドレスでした。
MACアドレスとは、ネットワーク機器が持つ固有のID番号のようなものです。ルーターはこのMACアドレスを使って「192.168.0.10はこの機器だ」と覚えている(ARPキャッシュ)のですが、br0のMACアドレスがOS起動のたびにランダム生成されていたため、ルーターの記憶が古くなると通信できなくなっていたのです。
解決策: br0のMACアドレスを物理NICと同じ値に固定し、さらに念のため5分ごとにルーターへ「自分はここにいるよ」と通知するスクリプト(ARPキープアライブ)をcronで回すようにしました。
落とし穴3:GNOMEデスクトップの罠
ある日、ホストPCの画面で何気なくGNOMEのネットワークアイコンをクリックしたら、またネットワークが壊れました。
原因を追うと、GNOMEのネットワーク設定画面で「接続」ボタンを押すと、「有線接続 1」という新しい接続プロファイルが自動生成されて、物理NIC(enp5s0)がブリッジから引き剥がされていました。
これはNetworkManager(Linuxのネットワーク管理サービス)が「enp5s0が空いてるから別の接続に使おう」と判断してしまうためです。サーバー用途なのにデスクトップ向けの親切機能が裏目に出るパターンですね。
解決策: enp5s0をNetworkManagerの管理対象外に設定しました。これでGNOMEの画面からは触れなくなり、誤操作を防止できます。
落とし穴4:対策の副作用で新しい問題が発生
落とし穴3の対策をしたら、今度はWireGuardが繋がらなくなりました。
問題の原因は、enp5s0をNetworkManagerの管理対象外にしたことで、仮想マシン起動時に自動作成される仮想NIC(vnet0)がブリッジに接続されなくなったこと。仮想マシンがLANに参加できないので、当然VPNのパケットも届きません。
落とし穴3を直したら落とし穴4が生まれる。まさに「モグラ叩き」状態です。
でもここで大事なのは、落とし穴3の対策(enp5s0のNM管理外化)は外せないということ。GNOMEの誤操作防止は必須なので、副作用の方を別の方法で解決する必要があります。
解決策: libvirt(KVMの管理ツール)にはVM起動時に任意のスクリプトを実行できる「hook」という仕組みがあります。これを使って、VM起動時に自動でvnet0をブリッジに追加するスクリプトを書きました。
# /etc/libvirt/hooks/qemu
#!/bin/bash
GUEST=$1
ACTION=$2
if [ "$ACTION" = "started" ]; then
sleep 2
for vnet in $(ls /sys/class/net/ | grep vnet); do
brctl addif br0 $vnet 2>/dev/null
done
fiこれでようやく、すべての対策が共存できる状態になりました。
最終的に安定稼働に必要だった4つの設定
① br0のMACアドレス固定 + ARPキープアライブ
→ 時間経過でSSHが切れる問題を防ぐ
② br0・bridge-slaveのautoconnect有効化
→ 再起動後にbr0が起動しない問題を防ぐ
③ enp5s0のNetworkManager管理外化
→ GNOMEの誤操作でbr0が壊れる問題を防ぐ
④ libvirt hookでvnet0を自動追加
→ 対策③の副作用でVMがブリッジに参加できない問題を防ぐ
WireGuard自体の設定は本当にシンプルでした。ハマったのは全部その「周辺」の話です。
振り返り:インフラ構築で学んだこと
今回の経験で強く感じたのは、インフラの問題は「一つ直すと別のところが壊れる」ことが多いということです。
落とし穴3を直したら落とし穴4が生まれたように、ネットワークの各設定は互いに依存関係を持っています。一つの対策を入れたら、それが他のどこに影響するかを考える必要があります。
そして、問題の切り分けには**「どこまでは正常で、どこから異常か」を一つずつ確認していく地道な作業**が不可欠でした。tcpdumpでパケットの到達地点を確認したり、brctl showでブリッジの状態を調べたり。派手さはないけれど、これがインフラのデバッグの基本だと実感しました。
もともと業務系のアプリケーション開発がメインで、インフラは未経験からのスタートでした。正直なところ最初は「VPNなんて設定ファイル書くだけでしょ」くらいに思っていましたが、実際にやってみるとネットワーク・仮想化・OS管理の知識が複合的に必要で、想像以上に奥が深い世界でした。
自宅サーバーでの構築は失敗しても誰にも迷惑がかからないので、学習環境としてはとても良いです。同じようにインフラに挑戦してみたい方の参考になれば嬉しいです。
