WireGuardによる高性能Linux VPN構築:ポイントツーポイント、スプリットトンネリング、マルチノードセキュリティ

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

誰も望んでいなかったレイテンシのスパイクとSSHセッションの切断

前四半期、私たちの分散システム環境は大きな壁に突き当たりました。US-EastノードとEU-Centralノード間のバックアップ転送速度は著しく低下し、1 Gbps回線にもかかわらず約140 Mbpsで頭打ちになることが頻繁に発生していました。大規模なPostgreSQLスキーマ移行の最中にはSSHセッションがフリーズし、ステージング環境のクエリは定期的にタイムアウト。OpenVPNトンネルがCPUリソースを過剰に消費し、実効帯域幅の45%近くを喪失していたのです。

さらに悪いことに、全開発者のアウトバウンドトラフィックが、192.168.50.12にある単一のプライベートRedisインスタンスに到達するためだけに、中央の単一ゲートウェイを経由してルーティングされていました。単純なnpm installやZoom通話が、本番環境のデータ同期と帯域を直接奪い合う状態に陥っていたのです。ピーク時には、そのゲートウェイサーバーの2つのCPUコア使用率が常に90%に張り付いていました。

根本原因:プロトコルの肥大化とモノリシックなルーティング

ネットワークパフォーマンスを阻害していた構造的な欠陥は、主に次の2点でした。

1. ユーザ空間のコンテキストスイッチとTLSオーバーヘッド

従来のSSL/TLS VPNはユーザ空間で動作します。すべてのパケットは、仮想TUN/TAPインターフェースでキャプチャされる時と、暗号化されてネットワークソケットに戻される時の計2回、カーネル・ユーザ空間の境界を跨ぎます。この頻繁なコンテキストスイッチによってCPUのL1/L2キャッシュ局所性が破壊され、ロードアベレージが跳ね上がります。さらに、クラウドアクロス経路で1〜2%のパケットロスが発生すると、OpenVPNのステートフルなハンドシェイク機構がストールし、配下の実行中TCPセッションまで巻き込んで遅延を引き起こしていました。

2. 「すべてをルーティングする」デフォルト設定

クライアントプロファイルが無分別に0.0.0.0/0をトンネル経由でルーティングさせていました。デフォルトゲートウェイがリダイレクトされた結果、日常的な外部パッケージのダウンロードや動画ストリーミング、パブリックAPIの呼び出しまでもがステージングゲートウェイを通過していました。ルーティングテーブルが分離されていなかったため、内部管理トラフィックがパブリック通信を圧迫していたのです。

徹底比較ベンチマーク:OpenVPN vs IPsec vs WireGuard

Ubuntu 22.04 LTS(4コア、8GB RAM)の同一スペックノードを使い、1 Gbpsリンク上でiperf3を実行して3つの選択肢をテストしました。

  • OpenVPN (v2.5, UDP, AES-256-GCM):1つのCPUコアが100%に張り付いた状態で、スループットは165 Mbpsにとどまりました。内部認証局(CA)の管理や証明書のローテーション作業も、運用上の大きな負担となっていました。
  • IPsec / strongSwan (IKEv2, AES-128-GCM):カーネル空間で動作するため高速で、CPU負荷24%で780 Mbpsを達成しました。しかし、swanctl.confの設定構文は複雑です。また、2週間のエージングテスト中、ステートフルなAWS NATゲートウェイを跨いだリキー処理によってハンドシェイクが2回切断されました。
  • WireGuard (カーネルモジュール):全体のCPU使用率わずか4%で、935 Mbpsの回線上限に迫るパフォーマンスを叩き出しました。Linux 5.6以降に標準統合されており、鍵交換にCurve25519、認証付き暗号化にChaCha20-Poly1305、ハッシュ化にBLAKE2sといった最新の暗号プリミティブを採用しています。コードベースは可読性の高いC言語で4,000行未満。静粛なUDPベースのピア関係を維持するため、実際にデータを転送する必要が生じるまでハンドシェイクのオーバーヘッドはゼロに抑えられます。

本番環境の設計図:カーネルWireGuardとスプリットトンネリング

この構成を2台の本番ノード(ノードA:ゲートウェイ 203.0.113.10、ノードB:リモートワーカー 198.51.100.4)で段階的に導入しました。すべてのインターネットトラフィックをトンネルに通すのではなく、通信をプライベートサブネットのみに厳密に制限しています。

ステップ1:ユーティリティのインストールと暗号鍵の生成

両方のマシンにカーネル制御ツールをインストールします。

# Ubuntu / Debian
sudo apt update && sudo apt install -y wireguard wireguard-tools

# RHEL / Rocky Linux 9 / AlmaLinux 9
sudo dnf install -y epel-release
sudo dnf install -y wireguard-tools

WireGuardは、NoiseプロトコルやSSHに着想を得た公開鍵暗号を採用しています。両ノードで厳格なファイルパーミッションを設定した上で秘密鍵を生成し、対応する公開鍵を導出します。

umask 077
wg genkey | tee privatekey | wg pubkey > publickey

ステップ2:ゲートウェイサーバー(ノードA)の設定

ノードA上に/etc/wireguard/wg0.confを作成します。本構成において、ノードAは内部トンネルIP 10.100.0.1/24を使用し、UDPポート51820でリッスンします。

[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = <NODE_A_PRIVATE_KEY>

# 内部プライベートサブネット (192.168.50.0/24) 向けのパケット転送およびNATを処理
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = <NODE_B_PUBLIC_KEY>
AllowedIPs = 10.100.0.2/32

ノードAがインターフェース間でパケットをルーティングできるよう、カーネルのパケット転送設定を永続化します。

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

ステップ3:スプリットトンネリングを適用したクライアント(ノードB)の設定

ゲートウェイの帯域圧迫を解消するため、ノードBで高精度なルーティングを設定します。AllowedIPsの範囲を内部サブネット(10.100.0.0/24および192.168.50.0/24)のみに限定することで、wg-quickが適切なカーネルルートを自動的に挿入します。その他のアウトバウンドトラフィックはすべて、ノードBのローカルデフォルトルートに維持されます。

ノードB上に/etc/wireguard/wg0.confを作成します。

[Interface]
Address = 10.100.0.2/24
PrivateKey = <NODE_B_PRIVATE_KEY>

[Peer]
PublicKey = <NODE_A_PUBLIC_KEY>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.100.0.0/24, 192.168.50.0/24
PersistentKeepalive = 25

PersistentKeepalive = 25パラメータは、25秒ごとに空の認証済みパケットを送信します。これによりクラウドファイアウォールのステートフルNATテーブルが維持され、プロバイダーの厳しいNAT配下にあってもノードB宛てのインバウンド接続が確実に届くようになります。

ステップ4:リンクの起動とハンドシェイクの確認

wg-quickを使用してインターフェースを起動し、起動時に自動起動するようsystemdを設定します。

sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0

いずれかのノードでリンクの状態を確認します。

interface: wg0
  public key: hK9sB8r3G2...x8=
  listening port: 51820

peer: rF3wK91mP0...m1=
  endpoint: 198.51.100.4:51820
  latest handshake: 18 seconds ago
  transfer: 4.82 MiB received, 11.29 MiB sent

ノードBからのルートをテストします。内部ネットワーク宛てのパケットのみがwg0を経由していることが確認できます。

ip route get 192.168.50.12
# 出力: 192.168.50.12 dev wg0 src 10.100.0.2

ip route get 1.1.1.1
# 出力: 1.1.1.1 via 198.51.100.1 dev eth0 src 198.51.100.4

ステップ5:事前共有鍵(PSK)とスコープ限定ファイアウォールによる多層防御

異なるクラウドプロバイダーを跨いでメッシュ接続やポイントツーポイント接続を運用する場合は、将来的なCurve25519ハンドシェイクに対する量子暗号解読への備えとして、共通鍵暗号のレイヤーを追加します。事前共有鍵(PSK)を生成します。

wg genpsk > preshared.key

両方の設定ファイルの[Peer]セクション配下にPresharedKey = <CONTENTS_OF_FILE>を記述します。WireGuardはこの256ビットの秘密鍵をハンドシェイクのハッシュ計算に組み込みますが、測定可能なレイテンシの増加は一切生じません。

次に、UDPリスナーへのアクセスを制限します。接続先ピアが静的パブリックIPを持っている場合は、ポート51820をインターネット全体に開放してはいけません。相手先のリモート送信元IPのみにアクセスを限定します。

# UFW (Ubuntu / Debian)
sudo ufw allow proto udp from 198.51.100.4 to any port 51820 comment "WireGuardピア ノードB"

# firewalld (RHEL / Rocky / AlmaLinux)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.4" port port="51820" protocol="udp" accept'
sudo firewall-cmd --reload

本番環境で得られた具体的な成果

この移行により、ネットワークのボトルネックは瞬時に解消されました。リージョン間ノードのポイントツーポイントレイテンシは48msから19msへと大幅に短縮され、Pingのジッターも±14msから0.8ms未満へと激減しました。1 Gbpsをフルに使ったデータベースバックアップのレプリケーション中でも、ゲートウェイのCPU負荷は6%未満で安定。スプリットトンネリングによって中央ゲートウェイの帯域が完全に解放され、開発者はパブリックインターネット接続を圧迫することなく、プライベートデータベースとネイティブな速度で通信できるようになりました。

Share: