KVM仮想マシンにWireGuard VPNサーバーを構築する【iptables落とし穴あり】
はじめに
自宅サーバーのUbuntu 22.04上にKVM仮想マシンを作り、そこにWireGuard VPNサーバーを構築しました。
外出先からAndroidスマホ・WindowsノートPCで自宅ネットワークに安全にアクセスするのが目的です。
構築中にiptablesのFORWARDチェーンでVPN接続が通らないという落とし穴にハマったので、その調査・解決方法も合わせて記録します。
環境
ホストPC(Ubuntu 22.04)
| 項目 | 詳細 |
|---|---|
| OS | Ubuntu 22.04.5 LTS |
| CPU | Intel Core i7-9700K(8コア) |
| RAM | 40GB |
| ネットワーク管理 | NetworkManager |
| 物理NIC |
enp5s0(192.168.0.10/24) |
VPNサーバーVM(ubuntu-server)
| 項目 | 詳細 |
|---|---|
| VM管理 | QEMU/KVM(virsh) |
| OS | Ubuntu Server(Noble) |
| NIC | enp1s0 |
| 固定IP | 192.168.0.11/24 |
クライアント
- Androidスマホ
- Windowsノート PC
最終的なネットワーク構成
インターネット
↓ 51820/UDP
ルーター(192.168.0.1)
↓ ポートフォワード → 192.168.0.11(ubuntu-server直接)
br0(192.168.0.10)← ホストOS
├── enp5s0(物理NIC・スレーブ)
└── ubuntu-server(192.168.0.11)← LAN直接参加
└── WireGuard(wg0: 10.0.0.1)
├── Android (10.0.0.2)
└── Windows (10.0.0.3)
Step 1: ブリッジ(br0)の作成
KVMのデフォルトNAT構成ではVMがLANに直接参加できません。
ブリッジ(br0)を作成して、VMをLAN上の1台として扱えるようにします。
補足:
enp5s0は筆者の環境でのNIC名です。
ip aコマンドで自分の環境のNIC名を確認してから置き換えてください。
# ブリッジ作成
sudo nmcli con add type bridge ifname br0 con-name br0
# 物理NICをブリッジに参加
sudo nmcli con add type bridge-slave ifname enp5s0 master br0
# IPアドレス設定(enp5s0のIPをbr0に移す)
sudo nmcli con modify br0 ipv4.addresses 192.168.0.10/24
sudo nmcli con modify br0 ipv4.gateway 192.168.0.1
sudo nmcli con modify br0 ipv4.dns 192.168.0.1
sudo nmcli con modify br0 ipv4.method manual
# 旧接続を削除してbr0を有効化(一瞬ネットワーク切断あり)
sudo nmcli con delete "有線接続 1" && sudo nmcli con up br0
sudo nmcli con up bridge-slave-enp5s0
注意:
sudo nmcli con delete "有線接続 1"を先に実行してから br0 を有効化することで、IP競合を防げます。順序を逆にすると不安定になります。
Step 2: VMのネットワークをブリッジモードに変更
virsh edit ubuntu-server
以下のように変更します:
<!-- 変更前 -->
<interface type='network'>
<source network='default'/>
<!-- 変更後 -->
<interface type='bridge'>
<source bridge='br0'/>
変更を反映:
virsh destroy ubuntu-server && virsh start ubuntu-server
Step 3: VMのIPアドレスを固定
VM内で /etc/netplan/50-cloud-init.yaml を編集します:
network:
version: 2
ethernets:
enp1s0:
dhcp4: false
addresses:
- 192.168.0.11/24
routes:
- to: default
via: 192.168.0.1
nameservers:
addresses:
- 192.168.0.1
- 8.8.8.8
sudo netplan apply
ルーターのDHCP配布範囲から 192.168.0.11 を除外しておくことも推奨します。
Step 4: WireGuardのインストール
VM内で実行:
sudo apt update && sudo apt install wireguard -y
確認:
wg --version
# wireguard-tools v1.0.20210914
Step 5: 鍵ペアの生成
# サーバー用鍵ペア
wg genkey | sudo tee /etc/wireguard/server_private.key | wg pubkey | sudo tee /etc/wireguard/server_public.key
sudo chmod 600 /etc/wireguard/server_private.key
# Android用鍵ペア
wg genkey | tee ~/android_private.key | wg pubkey > ~/android_public.key
# Windows用鍵ペア
wg genkey | tee ~/windows_private.key | wg pubkey > ~/windows_public.key
生成されるファイル:
| ファイル | 用途 | 管理 |
|---|---|---|
/etc/wireguard/server_private.key |
サーバー秘密鍵 | 絶対に外部へ漏らさない |
/etc/wireguard/server_public.key |
サーバー公開鍵 | クライアント設定に使用 |
~/android_private.key |
Android秘密鍵 | クライアント設定に記載 |
~/android_public.key |
Android公開鍵 | サーバー設定に記載 |
~/windows_private.key |
Windows秘密鍵 | クライアント設定に記載 |
~/windows_public.key |
Windows公開鍵 | サーバー設定に記載 |
Step 6: サーバー設定ファイルの作成
/etc/wireguard/wg0.conf を作成:
[Interface]
PrivateKey = <サーバー秘密鍵(server_private.keyの内容)>
Address = 10.0.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE
[Peer]
# Android
PublicKey = <android_public.keyの内容>
AllowedIPs = 10.0.0.2/32
[Peer]
# Windows
PublicKey = <windows_public.keyの内容>
AllowedIPs = 10.0.0.3/32
VPNトンネル内のIPアドレス構成:
| ノード | VPN IP |
|---|---|
| サーバー(ubuntu-server) | 10.0.0.1 |
| Android | 10.0.0.2 |
| Windows | 10.0.0.3 |
Step 7: WireGuard起動・自動起動設定
# 起動
sudo wg-quick up wg0
# OS起動時に自動起動
sudo systemctl enable wg-quick@wg0
# 動作確認
sudo wg show
sudo systemctl status wg-quick@wg0
Step 8: ルーターのポート開放
ルーターの管理画面でポートフォワーディングを設定します:
| 設定項目 | 値 |
|---|---|
| プロトコル | UDP |
| 外部ポート | 51820 |
| 転送先IP | 192.168.0.11(ubuntu-server) |
| 転送先ポート | 51820 |
Step 9: クライアント設定
各端末にWireGuardアプリをインストールして、以下の設定を行います。
Android
[Interface]
PrivateKey = <android_private.keyの内容>
Address = 10.0.0.2/24
DNS = 192.168.0.1
[Peer]
PublicKey = <server_public.keyの内容>
Endpoint = <DDNSドメインまたはグローバルIP>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Windows
[Interface]
PrivateKey = <windows_private.keyの内容>
Address = 10.0.0.3/24
DNS = 192.168.0.1
[Peer]
PublicKey = <server_public.keyの内容>
Endpoint = <DDNSドメインまたはグローバルIP>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
AllowedIPs = 0.0.0.0/0 はフルトンネル(全通信をVPN経由)です。
自宅ネットワークのみに限定したい場合は 192.168.0.0/24, 10.0.0.0/24 を指定してください(スプリットトンネル)。
ハマりポイント:iptablesのFORWARDチェーン競合
上記の手順が完了してクライアントからVPN接続を試みましたが、ハンドシェイクが確立されませんでした。
症状
sudo wg show を確認しても latest handshake が表示されない。
調査
# ホストまでパケットが届いているか確認
sudo tcpdump -n -i any udp port 51820
# → enp5s0には届いていた(ルーター・DDNS・ポートフォワーディングは正常)
# VMへ転送されているか確認
sudo tcpdump -n -i br0 udp port 51820
# → 何も届いていなかった
パケットはホストまで届いているのにVMに届いていない。FORWARDチェーンの問題と判断。
sudo iptables -L FORWARD --line-numbers -n
# FORWARDチェーンの状態
1: DOCKER-USER
2: DOCKER-FORWARD
3: LIBVIRT_FWX
4: LIBVIRT_FWI ← ここで新規接続(NEW)を全てREJECT ★問題箇所
5: LIBVIRT_FWO
6: ACCEPT udp → 192.168.0.11:51820 ← ここに到達できない
LIBVIRT_FWI チェーンの中身を確認:
sudo iptables -L LIBVIRT_FWI -n
ACCEPT all -- 0.0.0.0/0 192.168.122.0/24 RELATED,ESTABLISHED
REJECT all -- 0.0.0.0/0 192.168.122.0/24 reject-with icmp-port-unreachable
libvirtd が RELATED,ESTABLISHED のみを通過させ、それ以外を REJECT していました。
WireGuardのハンドシェイク(新規接続 = NEW)がここで弾かれていたのが原因です。
解決策
ACCEPTルールを LIBVIRT_FWX より前に移動します:
# 旧ルール削除
sudo iptables -D FORWARD 6
# LIBVIRT_FWXより前(3番)に挿入
sudo iptables -I FORWARD 3 -p udp -d 192.168.0.11 --dport 51820 -j ACCEPT
# 永続化
sudo apt install netfilter-persistent iptables-persistent -y
sudo netfilter-persistent save
変更後の順序:
1: DOCKER-USER
2: DOCKER-FORWARD
3: ACCEPT udp → 192.168.0.11:51820 ← LIBVIRTより前でACCEPT ✅
4: LIBVIRT_FWX
5: LIBVIRT_FWI
6: LIBVIRT_FWO
確認
sudo wg show
# latest handshake: 17 seconds ago
# transfer: 180 B received, 92 B sent
ハンドシェイク成立・VPN接続確認済み。
おまけ:CPUパススルーで暗号処理を高速化
デフォルトのKVM仮想CPU(qemu64)ではホストCPUのAES-NI・AVX2が使えず、WireGuardの暗号処理が遅くなります。
host-passthrough に変更することでホストCPUの命令セットをそのまま利用できます。
virt-xml ubuntu-server --edit --cpu host-passthrough
virsh destroy ubuntu-server && virsh start ubuntu-server
VM内で確認:
grep -o 'aes\|avx2\|avx' /proc/cpuinfo | sort -u
# → aes / avx / avx2 の3つが有効に
| 命令 | 用途 |
|---|---|
| aes | AES-NI(AES暗号のハードウェア処理) |
| avx | 256bit SIMD(数値演算の並列処理) |
| avx2 | WireGuardのChaCha20暗号高速化に効く |
iperf3による速度計測結果(スプリットトンネル)
| 項目 | 値 |
|---|---|
| 送信(クライアント → VPN) | 9.65 Mbps |
| 受信(VPNサーバー側) | 8.23 Mbps |
| パケット再送(Retr) | 0(ロスなし) |
フルトンネルでも問題なく接続・通信できました。
セキュリティ上の注意事項
- 秘密鍵(
*_private.key)は絶対に外部に漏らさない - クライアント端末を紛失した場合は、サーバーの
wg0.confから該当PeerのPublicKey行を削除し、sudo systemctl restart wg-quick@wg0で即座に無効化する -
/etc/wireguard/server_private.keyにはchmod 600が設定済みであることを確認する
ls -la /etc/wireguard/
# -rw------- 1 root root ... server_private.key ← 600であること
まとめ
- KVM仮想マシン上にWireGuardサーバーを構築し、Android・Windowsからの接続に成功
- ブリッジ(br0)を作成することでVMをLANに直接参加させ、ルーターのポートフォワーディングで外部公開
- libvirtd が管理するiptablesのFORWARDチェーンとの競合が最大のハマりポイント
- CPUパススルーでAES-NI/AVX2を有効化し、暗号処理を高速化
WireGuard自体のセットアップは非常にシンプルですが、KVMとlibvirtdの組み合わせ特有のiptables問題に注意が必要です。