← ブログに戻る

WireGuard・OpenVPN・Tailscaleの違い:この3つを横並びで比較すると選択を間違える

VPNの比較記事はたいてい3列の表から始まる。WireGuard、OpenVPN、Tailscale。速度、設定の手間、対応プラットフォーム。表を眺めた読者は「じゃあどれにしよう」と考える。

この問いの立て方が間違っている。3つのうち2つは同じレイヤーの競合だが、残る1つは別の階層にいるからだ。TailscaleはWireGuardの代わりに選ぶものではなく、WireGuardを選んだ上でその面倒な部分を外注するものである。この関係が見えていないと、「WireGuardとTailscaleどっちが速い?」という答えの出ない問いに時間を使うことになる。

OpenVPN:TLSを流用した設計

OpenVPNは2001年に登場した。設計の中心は「すでにあるTLSを使い回す」という判断である。

接続は2本のチャネルに分かれる。制御チャネルはTLSそのもので、証明書ベースのPKIで相手を認証し、データチャネル用の鍵を交換する。データチャネルはOpenVPN独自のフォーマットで、暗号化したIPパケットをUDP(既定で1194番)またはTCPに載せる。

TCPを選べるのがOpenVPNの実務的な強みだった。443番のTCPに流せば、外から見ればHTTPS通信と区別がつきにくい。厳しいファイアウォールの内側から抜けたいときに、これが効く。ただしTCPの上にTCPを載せるとTCP meltdownと呼ばれる再送の二重化が起きるため、通れるなら常にUDPを選ぶべきという原則は変わらない。

設計の代償は2つある。

1つ目は動作場所。OpenVPNはユーザー空間のプロセスとして動き、tunデバイス経由でカーネルとパケットをやり取りする。パケット1つごとにカーネル空間とユーザー空間の境界を往復するため、スループットが頭打ちになりやすい。OpenVPN 2.6以降のDCO(Data Channel Offload)はこの往復を削るための機能で、データチャネルをカーネルモジュールに落とす。裏を返せば、それが必要になるほど元の構造にコストがあったということでもある。

2つ目は設定の自由度。OpenVPNは暗号スイートを選べる。TLSと同じくネゴシエーションで決まる。柔軟性と引き換えに、設定を間違えると弱い組み合わせで繋がってしまう余地が常に残る。実際、OpenVPNの設定ファイルは数十行になり、そこにeasy-rsaで組んだCAとクライアント証明書の管理が加わる。

WireGuard:選択肢を削った設計

WireGuardの設計思想はOpenVPNのほぼ反対側にある。ネゴシエーションを消し、暗号アルゴリズムを固定した。

  • 鍵交換: Curve25519 (ECDH)
  • 暗号化: ChaCha20-Poly1305 (AEAD)
  • ハッシュ: BLAKE2s
  • ハンドシェイク: Noise Protocol Framework の Noise_IK パターン

選べない。そう作ってある。暗号スイートを交渉しないなら、交渉の過程で弱い方式に落とすダウングレード攻撃も原理的に成立しない。アルゴリズムに問題が見つかったらプロトコルのバージョンごと上げる、という割り切りである。

実装も小さい。公式が設計目標に掲げているのが「ごく少ない行数で実装できること」で、Linux 5.6からはカーネル本体にマージされている。OpenVPNが本体に加えてOpenSSLという巨大な依存を抱えているのと比べると、監査すべきコードの量が違う。暗号方式を選べなくしたぶん、選択を処理するコードがまるごと存在しない。

パケットの扱い方も独特で、公式にはCryptokey Routingと呼ばれる。設定はこうなる。

[Interface]
PrivateKey = <自分の秘密鍵>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <相手の公開鍵>
AllowedIPs = 10.0.0.2/32
Endpoint = 203.0.113.5:51820

AllowedIPsが二重の意味を持っているのが要点である。送信時は「このIP宛のパケットはこのpeerに送る」というルーティングテーブルとして働き、受信時は「このpeerから来てよいのはこのIPを送信元とするパケットだけ」というアクセス制御として働く。公開鍵とIPレンジの対応表が、そのままルーティングと認可を兼ねている。

接続も速い。ハンドシェイクは1-RTTで完了し、鍵は約2分ごとに更新される。さらにpeerのエンドポイントは受信パケットの送信元アドレスから自動で学習されるため、クライアント側のIPが変わっても接続が切れない。カフェのWi-Fiからテザリングに切り替えても、そのまま繋がったままになる。OpenVPNでこれをやると、多くの場合TLSのやり直しが挟まる。

もう一つ、地味に効く性質がある。WireGuardは正しい鍵で署名されていないパケットに一切応答しない。ポートスキャンをかけても何も返ってこないので、外から見るとそのポートは閉じているように見える。

WireGuardが解いていない問題

ここまで読むと、WireGuardを選べば話が終わりそうに見える。実際、2台を繋ぐだけなら終わる。

問題はノードが増えたときに起きる。

WireGuardには鍵を配る仕組みがない。IPアドレスを割り当てる仕組みもない。誰がどのノードに到達してよいかを中央で決める仕組みもない。全部が設定ファイルに手で書かれる前提である。

3台をフルメッシュで繋ぐとする。各ノードに[Peer]が2つ、合計6エントリ。まだ耐えられる。10台になると各ノードに9つ、合計90エントリになる。しかも新しい1台を足すたびに、既存9台すべての設定ファイルを書き換えて再読み込みさせる必要がある。設定量はノード数の2乗で増える。

これを避けるには、1台をハブにしてスター型にする。設定量は線形になるが、今度はハブが単一障害点になり、全トラフィックがそこを通るのでハブの回線が上限になる。

さらにNAT越えという別の問題がある。両側がNATの内側にいる場合、どちらからも接続を開始できない。WireGuardにはPersistentKeepaliveがあるが、これはNATのマッピングを維持するためのものであって、NAT越えそのものを実現する機能ではない。結局どちらか一方はグローバルIPを持っている必要がある。

つまりWireGuardは「安全で速いトンネルを張るプロトコル」としては完成しているが、「ネットワークを運用する仕組み」ではない。この2つは別の問題である。

Tailscale:足りない層を足す

Tailscaleが埋めているのは、その足りない層である。

データプレーン、つまり実際にパケットを暗号化して運ぶ部分はWireGuardのプロトコルそのものを使う(実装はGo製のwireguard-go)。その上にコントロールプレーンを載せた。役割はこうなる。

鍵配布: 各ノードは秘密鍵をローカルで生成し、公開鍵だけをコーディネーションサーバーに登録する。サーバーは全ノードの公開鍵一覧を配る役目で、秘密鍵は一度も外に出ない。中央サーバーがあっても通信の中身を復号できない構造になっている。

IP割り当て: 100.64.0.0/10(CGNAT用に予約されたレンジ)から各ノードに自動で割り振る。手で決めなくてよくなり、既存のプライベートIPとも衝突しない。

認証: OIDCで既存のIdPにフェデレートする。GoogleアカウントやGitHubアカウントでログインすれば、そのノードがネットワークに参加する。証明書を発行して配る作業が消える。

NAT越え: STUNで自分の外側から見えるアドレスを学習し、UDPホールパンチングで直接接続を試みる。両側が対称型NATなどで直結できない場合はDERP(Designated Encrypted Relay for Packets)というリレーに落ちる。このリレーサーバーはパケットを中継するだけで、中身を復号できない。秘密鍵がノードから出ないため、暗号化はWireGuardの層でend-to-endに掛かったままになる。リレーは暗号文の郵便配達をしているに過ぎない。

ACL: 誰が誰に到達してよいかをポリシーファイルで一元管理する。WireGuardのAllowedIPsを手で調整して回る作業が、1箇所の記述に集約される。

結果として、10台をフルメッシュで繋ぐ設定は各ノードでこうなる。

tailscale up

90エントリが消えたわけではない。それを書く仕事がコントロールプレーンに移っただけである。ノードの裏では今もWireGuardのpeer設定が動的に組み立てられている。

3つの関係を整理する

ここまでを図にするとこうなる。

[ 運用レイヤー ]   Tailscale / Headscale / NetBird
                   鍵配布・IP割当・NAT越え・ACL
                          ↓ 生成する
[ プロトコル ]     WireGuard          OpenVPN
                   Noise_IK           TLS
                   固定暗号           ネゴシエーション
                   カーネル空間        ユーザー空間(DCOで改善)

WireGuardとOpenVPNは同じ層の選択肢である。ここは比較して選んでよい。

一方でTailscaleは上の層にいる。だから「WireGuardとTailscaleどっちが速いか」という問いは、「エンジンとカーナビどっちが速いか」を聞いているようなものになる。速度を決めているのは下の層と、そこに至る経路の質である。直接接続が張れているかDERPリレーに落ちているかで、同じTailscaleでも数字はまるで変わる。

ただしTailscaleがデータプレーンに使うのはGo実装のwireguard-goなので、「Tailscale経由なら素のWireGuardと同じ数字が出る」とも限らない。ユーザー空間実装はパケットごとにコピーのコストを払う。もっともTailscaleはTUNドライバのTSO/GROとsendmmsg()/recvmmsg()によるシステムコールのまとめ処理を入れており、自社の計測ではwireguard-goが2.42 Gbps(改善前)から5.36 Gbpsまで伸び、同条件のカーネル版WireGuard(2.66 Gbps)を上回ったと報告している。効いていたのはパケット1つあたりの固定オーバーヘッドで、ユーザー空間で動いていること自体が主因だったわけではない、という結論になっている。

判断はこの順で切ると迷わない。

  1. プロトコルを選ぶ: 新規ならWireGuard。TCP 443に偽装して厳しいファイアウォールを抜ける必要がある、あるいは既存のPKIやRADIUS認証に統合したい場合はOpenVPN
  2. 運用レイヤーが要るか決める: ノードが2〜3台で固定、片側にグローバルIPがあるなら素のWireGuardで足りる。ノードが増える、動的IPが混ざる、NAT同士を繋ぎたいなら運用レイヤーを載せる
  3. 運用レイヤーを自前で持つか決める: Tailscaleのコントロールプレーンはプロプライエタリである。ここを自分で握りたいならHeadscale(コントロールプレーンのOSS再実装)という選択肢がある

抽象化が漏れる場所

私は自宅でGPUマシン(RTX 4070を積んだWindows機)をTailscaleで繋ぎ、メインマシンからSSHで叩いて画像生成や音声生成を回している。Tailscaleを入れた日は感動的で、tailscale upと打っただけでNAT越えもDNSも解決した。MagicDNSのおかげでIPを覚える必要すらない。

ところが、この構成には毎回同じ壊れ方をする箇所がある。GPUマシン側のtailscaledはWindowsではなくWSL2の中でsystemd管理で動いている。そしてWSL2はWindowsを再起動しても自動では起動しない。誰かがWSLのシェルを開くまで、その中のsystemdは眠ったままである。

つまりWindowsを再起動した瞬間、そのノードはTailscaleのネットワークから消える。コントロールプレーンから見れば単にオフラインなので、エラーは何も出ない。こちらからはssh: connect to host ... No route to hostが返るだけで、原因がVPNなのかSSHなのかWSLなのか、切り分けに毎回数分かかる。

Tailscaleが立てている前提は「ノードのOSが起きていればエージェントも起きている」というものだ。素直な前提で、ほとんどの環境では正しい。仮想環境がもう一段ネストしていると、その前提だけが静かに崩れる。

抽象化は下の層の面倒を引き受けてくれるが、下の層を消してはくれない。90エントリのpeer設定は書かなくてよくなったが、そこにWireGuardのトンネルがあるという事実は残り続ける。何かが繋がらなくなったとき、結局は下の層まで降りていく必要がある。

そういうわけで、「どれを使うか」を決める前に「どの層の問題を解こうとしているのか」を決めるほうが早い。3列の比較表が答えてくれないのは、そこである。

参考