【実務・中級編】 VPN接続速度のベンチマーク測定方法とボトルネック分析 – サイバーセキュリティとプライバシー保護実践ガイド

「おい、VPNを通すとAPIのレスポンスが極端に落ちるんだが、ネットワーク側で何か詰まってないか?」

インフラ運用やWeb APIの開発現場に身を置いていると、開発チームやクライアントからこんな泣き言(あるいはクレーム)が頻繁に飛んできます。特に、リモートワークの普及や公共Wi-Fiのセキュリティ対策としてVPN(Virtual Private Network)が必須となった現代において、VPNの「速度低下」は避けて通れないテーマです。

しかし、優秀なネットワークエンジニアであれば、「暗号化しているから遅いのは仕方がない」などという曖昧な回答で濁してはいけません。
パケットは嘘をつきません。遅延には必ず、プロトコル、パケットサイズ、あるいは物理法則に裏付けられた「定量的な理由」が存在します。

今回は、VPN接続におけるボトルネックを科学的に分析し、パケットレベルで何が起きているのかを突き止め、最適なスループットを叩き出すためのベンチマーク測定方法とチューニングの真髄を伝授します。

—

1. VPN接続を緊縛する「3大ボトルネック」の正体

VPNの速度低下を引き起こす要因は、大きく分けて以下の3つに集約されます。これらがどのようにしてパケットの往復を阻害しているのか、まずはそのメカニズムを脳内にマッピングしましょう。

① 暗号化とカプセル化のオーバーヘッド(MTU/MSS問題)

VPNは、元のIPパケットを暗号化し、新たなIPヘッダーで包み込む「カプセル化」を行います。ここで引き起こされるのが、ネットワークインフラにおける最大の宿敵「IPフラグメンテーション(断片化)」です。

通常のイーサネットフレームのMTU(Maximum Transmission Unit)は 1500 バイトです。ここにVPNヘッダー(プロトコルや暗号スイートにより数十バイト)が追加されると、実質的なMTUは減少します。

[ 通常のIPパケット ]
+---------------------------+---------------------------------+
| IP Hdr (20B) | TCP Hdr (20B) |         Payload (1460B)         |  = 1500B
+---------------------------+---------------------------------+

[ VPNカプセル化後のパケット ]
+------------+-------------+------------+-------------+-------+
| New IP (20B| VPN Hdr (40B| IP Hdr (20B| TCP Hdr (20B|Payload|  = 1500B
+------------+-------------+------------+-------------+-------+
                                                      ^
                                             実質的な有効ペイロードが減少

もし、クライアントがこの減少を考慮せずに 1500 バイトのパケットを送信し、かつ経路上のルーターでフラグメンテーション(分割処理)が発生すると、以下の問題が噴出します。

  • CPU負荷の増大: ルーターやクライアント、VPNゲートウェイでのパケット分割・再構築処理により、CPUが悲鳴を上げます。
  • パケットロスの連鎖: 分割されたパケットの1片でも失われれば、TCPは全体の再送を要求するため、実質的なスループットは指数関数的に低下します。

これは、RFC 4459(MTU/MSS tuning)やRFC 1191(Path MTU Discovery)でも深く議論されている、古典的かつ現在進行形の課題です。

② 物理的距離とRTT(遅延)によるTCPの限界(BDPの壁)

「光の速さは有限である」――この物理の基本原則が、ネットワークの帯域幅を縛ります。
通信相手(VPNサーバー)との物理的距離が離れれば、当然RTT(Round Trip Time: 往復遅延時間)は増大します。ここで効いてくるのがBDP(Bandwidth-Delay Product:帯域遅延積)です。

$$BDP (bits) = \text{帯域幅} (bps) \times RTT (\text{秒})$$

TCPは、送信したデータに対する確認応答(ACK)を受け取るまで、一定量のデータ(TCPウィンドウサイズ)しか一度に送信できません。
例えば、1Gbpsの超高速回線であっても、RTTが 100ms(海外のVPNサーバーなど)あり、TCPウィンドウサイズがデフォルトの 64KB(524,288 ビット)に制限されている場合、理論上の最大スループットは以下の通り絶望的な数値になります。

$$\text{最大スループット} = \frac{524,288 \text{ bits}}{0.1 \text{ 秒}} \approx 5.24 \text{ Mbps}$$

回線がどれだけ太くても、遅延が大きく、ウィンドウサイズが適切にスケーリングされていなければ、VPNの帯域を使い切ることは不可能なのです。

③ ISPによるVPNトラフィックへの帯域制限(Throttling)

一部のISP(インターネットサービスプロバイダー)や公共Wi-Fiの管理者、あるいは国家レベルのファイアウォールは、DPI(Deep Packet Inspection)を用いて暗号化トラフィックを監視しています。
OpenVPN(デフォルト:UDP 1194)やWireGuard(デフォルト:UDP 51820)といった特徴的なポートやパケットパターンを検知すると、QoS(Quality of Service)によって意図的に帯域を絞り込む(Throttling)制御を行うことがあります。

—

2. ネットワークの健康診断:ベンチマーク測定手法

では、これらの要因をどのように切り分け、測定すべきでしょうか。
一般ユーザーが使う「ブラウザ上のスピードテスト」は、CDNやブラウザのレンダリングエンジン、JavaScriptの動作オーバーヘッドがノイズとなり、ネットワーク単体のベンチマークとしては不正確極まりありません。

我々エンジニアが信頼すべきは、プロトコルレベルで挙動を制御できる iperf3 です。

iperf3 を用いた正確なベンチマークの取り方

iperf3 は、TCPおよびUDPの生のスループットを測定するためのデファクトスタンダードツールです。VPN接続を確立した状態で、クライアントとVPNサーバー(もしくはVPN背後のセグメントにあるホスト)間でテストを実行します。

1. サーバー側の起動(VPN背後の検証用ホスト、またはVPNサーバー自体)

# iperf3をサーバーモードで起動(デフォルトポート: 5201)
iperf3 -s

2. クライアント側(手元の開発マシンなど)からの測定コマンド例

まずは、TCPで並列コネクションを張り、回線の限界値を探ります。

# TCPによる測定:並列接続数 4 (-P 4)、測定時間 10秒 (-t 10)、1秒ごとのレポート (-i 1)
iperf3 -c 192.168.10.100 -P 4 -t 10 -i 1

次に、UDPモードを用いて、パケットロス率とジッター(遅延の揺らぎ)を測定します。TCPのウィンドウ制御に邪魔されないため、回線本来のキャパシティとVPN装置の限界処理能力を測定するのに適しています。

# UDPによる測定:帯域制限を50Mに設定 (-b 50M)、パケットロスとジッターをあぶり出す
iperf3 -c 192.168.10.100 -u -b 50M -t 10 -i 1

—

3. 実践!ボトルネック分析とデバッグ手順

ここからは、実際に手元で叩けるコマンドやコードを用いて、ボトルネックを突き止めるデバッグ手順を解説します。

ステップ1:MTUの限界値をハンドシェイクなしで探る(ICMP Sweep)

VPNトンネル内でフラグメンテーションを起こさない最適なMTUサイズを特定するために、ping コマンドで「断片化禁止(DF: Don’t Fragment)ビット」を立ててパケットを送出します。

Linux / macOS の場合:

# -D でDFビットをセット、-s でペイロードサイズを指定
# 1472バイト(1472 + ICMP 8B + IP 20B = 1500B)から徐々に小さくしていく
ping -c 3 -D -s 1440 192.168.10.100

もし以下のようなエラーが返ってきたら、そのサイズはVPNトンネルの許容範囲を超えています。
ping: sendto: Message too long または Frag needed and DF set

Windows の場合:

:: -f でDFビットをセット、-l でバッファサイズを指定
ping -f -l 1440 192.168.10.100

エラーが出なくなる最大のサイズに 28バイト(IPヘッダー 20B + ICMPヘッダー 8B)を足した値が、そのVPN経路における最適MTUとなります。例えば、1412バイトでpingが通り、1413バイトで失敗した場合、最適MTUは 1412 + 28 = 1440 バイトです。

ステップ2:PythonによるBDPとTCPウィンドウサイズのシミュレーション

インフラのチューニングを行う際、理論上のスループット上限をあらかじめ把握しておくことは極めて重要です。以下のPythonスクリプトを使って、RTTとパケットロス率がTCPスループットに与える影響(Mathisの公式に基づく簡易計算)をシミュレートしてみましょう。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def calculate_bdp(bandwidth_mbps, rtt_ms):
    """
    帯域遅延積 (BDP) を計算し、必要なTCPウィンドウサイズを算出する
    """
    bandwidth_bps = bandwidth_mbps * 1_000_000
    rtt_sec = rtt_ms / 1000.0
    bdp_bits = bandwidth_bps * rtt_sec
    bdp_bytes = bdp_bits / 8
    return bdp_bytes

def estimate_mathis_throughput(rtt_ms, packet_loss_rate):
    """
    Mathisの公式を用いて、パケットロス存在下でのTCP最大スループット(理論値)を推定する
    MSS (Maximum Segment Size) は一般的な 1460 バイト(VPNを考慮すると 1380 バイト程度)と仮定
    """
    if packet_loss_rate <= 0:
        return float('inf')
    
    mss_bytes = 1380  # VPNオーバーヘッドを考慮したMSS
    rtt_sec = rtt_ms / 1000.0
    
    # Mathisの公式: Throughput <= (MSS * C) / (RTT * sqrt(p))
    # Cは定数(一般的に 1.22)
    c = 1.22
    throughput_bps = (mss_bytes * 8 * c) / (rtt_sec * (packet_loss_rate ** 0.5))
    return throughput_bps / 1_000_000  # Mbpsで返却

if __name__ == "__main__":
    # シナリオ設定
    target_bandwidth = 100.0  # 契約回線や物理ポートの上限 (Mbps)
    measured_rtt = 80.0       # VPNサーバーへのRTT (ms)
    loss_rate = 0.005         # 0.5% のパケットロスが発生していると仮定

    required_window = calculate_bdp(target_bandwidth, measured_rtt)
    theoretical_max_throughput = estimate_mathis_throughput(measured_rtt, loss_rate)

    print(f"=== VPNネットワーク特性シミュレーション ===")
    print(f"ターゲット帯域: {target_bandwidth} Mbps")
    print(f"測定された RTT : {measured_rtt} ms")
    print(f"パケットロス率 : {loss_rate * 100:.2f} %")
    print(f"------------------------------------------")
    print(f"■ 100Mbpsを使い切るために必要なTCPウィンドウサイズ:")
    print(f"  {required_window:,.0f} バイト (約 {required_window / 1024:.1f} KB)")
    print(f"■ パケットロスを考慮したTCP最大スループット(Mathisの公式):")
    print(f"  {theoretical_max_throughput:.2f} Mbps")
    print(f"==========================================")

このスクリプトを実行すると、わずか「0.5%」のパケットロスであっても、TCPのスループットがどれほど劇的に制限されるかが一目瞭然となります。VPNによるカプセル化の失敗やフラグメンテーションが、どれほど致命的なパフォーマンス低下を招くかの裏付けになります。

—

4. ボトルネックを打破する処方箋(サーバー/クライアント設定)

原因が特定できたら、次はいよいよ実務でのチューニングに入ります。インフラエンジニアが即座に適用すべき具体的な解決策を提示します。

解決策A:MSSクランプ(MSS Clamping)の適用

VPNトンネルを通過するTCPパケットの MSS を強制的に書き換え、フラグメンテーションを未然に防ぐ技術です。VPNルーターや、Linuxで構築したVPNサーバーの iptables に以下のルールを追加します。

# VPNインターフェース(例: wg0 / tun0)を通過するTCPのSYNパケットのMSSを、Path MTUに合わせて自動調整(クランプ)する
sudo iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -o wg0 -j TCPMSS --clamp-mss-to-pmtu

# または、明示的に安全なサイズ(例: 1360バイト)に固定する
sudo iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -o wg0 -j TCPMSS --set-mss 1360

*※この設定一行で、Webサイトの特定の画像だけがロードされない、あるいはAPIレスポンスが途中でハングアップするといった怪現象の多くが劇的に解決します。*

解決策B:WireGuardの採用と暗号アルゴリズムの最適化

OpenVPN(特にTCPモード)は、ユーザー空間での動作オーバーヘッドが大きく、コンテキストスイッチによる遅延が発生しやすい構造です。
可能であれば、Linuxカーネル空間で動作し、極めて軽量な暗号スイートである ChaCha20-Poly1305 を採用した WireGuard への移行を強く推奨します。

以下は、最適化された wireguard.conf の設定例です。

[Interface]
PrivateKey = <クライアントの秘密鍵>
Address = 10.0.0.2/24
# トンネルのMTUを明示的に指定。一般的な光回線+PPPoE/IPoE環境を考慮し 1420(または1280)に設定
MTU = 1420

[Peer]
PublicKey = <サーバーの公開鍵>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
# NATのセッションアウトを防ぐため、25秒おきにキープアライブを送信
PersistentKeepalive = 25

解決策C:OSのTCPバッファチューニング

BDPの壁を突破するため、Linuxクライアント/サーバー側でTCPの送受信バッファを自動スケーリングできるようにカーネルパラメーターを調整します。 /etc/sysctl.conf に以下を追記して適用します。

# TCPソケットの最大バッファサイズを拡張(大容量・高遅延回線用)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

# TCP受信/送信バッファの最小、デフォルト、最大値(バイト数)のチューニング
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# パケットロス耐性に優れたTCP輻輳制御アルゴリズム「BBR」を有効化
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

—

5. まとめ

VPNの速度低下は、魔法やオカルトではありません。

  • 暗号化によるヘッダーの肥大化が引き起こす MTU/MSSの不整合(フラグメンテーション)
  • 物理的な距離がもたらす RTTの増加とTCPウィンドウサイズの限界(BDP)
  • ネットワークの途中経路で牙を剥く ISPの帯域制御

これらが複雑に絡み合った結果に過ぎません。

次に開発メンバーから「VPNが遅くてデバッグにならない」と相談されたら、黙って iperf3 を起動し、ping -D でMTUを測定し、今回紹介したPythonスクリプトで理論値を叩き出して見せてあげてください。

「なんとなく遅い」を「これだけの理由で、ここが詰まっている」と言い切れる、科学的でスマートなインフラ運用を、ぜひ今日から実践していきましょう。

コメント

タイトルとURLをコピーしました