【実務・中級編】 VPN接続におけるDNSリークの発生原因と対策 – サイバーセキュリティとプライバシー保護実践ガイド

完璧なVPNトンネルに潜む「裏口」:DNSリークのメカニズムと、エンジニアが実務で必ず押さえるべき対策

ネットワークエンジニアの皆さん、日々のインフラ運用やAPIの死活監視、ご苦労様です。
ゼロトラストの時代と言われて久しいですが、リモートワークや外出先からのクラウド環境へのアクセスにおいて、個人向けVPNや商業用VPNは今やインフラの一部として不可欠な存在です。暗号化されたトンネルを潜り抜け、安全に社内リソースやAPIエンドポイントへアクセスできている――そう信じ切っていませんか?

ちょっと待ってください。その「カプセル化された安心感」、実はDNSの問い合わせという一番重要な裏口から、あなたの足跡がダダ漏れになっているかもしれません。

今回は、Web APIの設計やインフラ運用に深く関わるエンジニアの皆さんに向け、VPN接続時における「DNSリーク(DNS Leak)」の正体を徹底的に解剖します。パケットがOSのネットワークスタックをどう駆け抜け、なぜ暗号化トンネルの外へ漏れてしまうのか。そのメカニズムから、実務で使えるデバッグ手法、そしてコードレベルでの対策まで、現場の泥臭い知見を交えて解説していきます。

—

1. なぜ「VPN接続中」なのにDNSが漏れるのか?

まず、私たちが普段何気なく使っているDNS(Domain Name System)のライフサイクルと、VPNが形成する仮想インターフェースの力学関係を整理しましょう。

Web APIを叩くとき、あるいはブラウザにURLを入力するとき、アプリケーションは最初に api.example.com のようなホスト名をIPアドレスに変換するため、OSのネームリゾルバーに問い合わせ(getaddrinfo など)を投げます。

通常、VPNクライアントソフト(OpenVPNやWireGuard、各社専用アプリなど)が起動すると、OSのルーティングテーブルが書き換えられ、デフォルトゲートウェイがVPNサーバー側に向かいます。同時に、OSのDNSサーバーの設定(WindowsならIPプロパティ、Linuxなら /etc/resolv.conf や systemd-resolved)が、VPNプロバイダが指定するプライベートなDNSサーバー(例: 10.8.0.1 や Cloudflareの 1.1.1.1 など)に書き換わるはずです。

漏えいが起きる根本原因

しかし、以下のようなモダンOS特有の「親切すぎる機能」や「ネットワークスタックの仕様」が、意図しない裏口を開けてしまいます。

1. マルチホーム環境におけるクエリの競合
近代的なOS(Windows 10以降やmacOS)は、複数のネットワークインターフェース(Wi-Fi、有線LAN、VPNの仮想TUN/TAPデバイス)を同時に持っています。OSは「名前解決をより速く、確実に行うため」に、接続されているすべてのインターフェースに対して同時にDNSクエリ(Parallel DNS Queries)を投げる習性があります。これにVPN側の応答遅延(レイテンシ)が負けると、ISP(プロバイダ)やルーターが指定するローカルのDNSサーバーが先に答えを返し、そちらを採用してしまうのです。
2. スプリットトンネリング(Split Tunneling)の副作用
社内ドメインだけをVPN経由にし、パブリックな通信はローカルから抜ける設定にしている場合、DNSのフォワーディング設定が曖昧だと、パブリックなFQDNの問い合わせまでローカルのISPへ流れてしまいます。
3. OSのフォールバック機能(LLMNR / NetBIOS / mDNS)
名前解決に失敗した際、OSがローカルセグメントに向けてブロードキャストやマルチキャストで名前を叫んでしまう現象です。

これらが複合的に絡み合うことで、「通信のペイロード(中身)はVPNで暗号化されているのに、どこにアクセスしようとしているかという宛先ドメイン名(メタデータ)は、ISPやフリーWi-Fiの管理者から丸見え」という最悪の状況が生まれます。プライバシー保護の観点からも、セキュリティ監査の観点からも、これは致命傷になり得ます。

—

2. 通信フロー(シーケンス)で見るDNSリークの瞬間

文字だけではピンとこない方向けに、DNSリークが発生する瞬間のパケットの動きをシーケンスとして視覚化してみましょう。

[クライアントPC (アプリ)]
       │
       ├─ 1. APIリクエスト: "api.internal-service.com のIPをくれ"
       │
   (OS ネームリゾルバー)
       │
       ├─ 2a. 【VPN経由】仮想インターフェースへクエリ送信 ──> [VPN DNSサーバー (例: 10.0.0.1)]
       │      (本来はこっちを通ってほしい)
       │
       └─ 2b. 【ローカル経由】物理NICへクエリ送信 ────────> [ISP/ルーターのDNS (例: 192.168.1.1)]
              (★これがリーク!ISPにアクセス先がバレる)

実務の現場では、この裏で何が起きているかをパケットキャプチャ(tcpdump や Wireshark)で暴くことになります。

—

3. 実務におけるデバッグ手法と検証コマンド

「うちの環境は大丈夫か?」を確認するため、インフラエンジニアや開発者が手元で即座に実行できるデバッグ手順を解説します。

① 現在使われているDNSサーバーの確認(Linux / macOS)

まずは、OSが現在どのDNSサーバーを参照しているかを正確に掴みます。systemd環境のLinuxであれば以下のコマンドが有効です。

# systemd-resolvedの状態を確認し、どのインターフェースでどのDNSが使われているか暴く
resolvectl status

# 出力例(抜粋)
# Global
#   Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
# 
# Link 3 (wlan0) -> ローカルWi-Fi
#   Current DNS Server: 192.168.1.1
#       DNS Domain: .home
# 
# Link 7 (tun0)  -> VPNトンネル
#   Current DNS Server: 10.8.0.1
#       DNS Domain: ~., ~corp.internal

もし Link 3 (wlan0) の方に目的のドメイン解決が吸い寄せられている場合、それはDNSリークの予兆です。

② CLIからの名前解決テストと挙動確認

次に、意図したDNSサーバーに対して正しく名前解決が行われているかを dig コマンドで検証します。

# あえてVPNのDNSサーバー(例: 10.8.0.1)を直接指定して名前解決をテスト
dig @10.8.0.1 target-api.example.com +short

# 次に、サーバーを指定せず(OSのリゾルバー任せで)名前解決をテスト
dig target-api.example.com +short

この2つの結果のIPアドレスが異なる場合、あるいは後者のクエリがローカルのISP製DNSにヒットしている場合、あなたのマシンは確実にDNSリークを起こしています。

—

4. コード&設定ファイルによる具体的な解決手法

では、このDNSリークを根絶するためにはどうすればよいでしょうか。インフラレベルの設定から、アプリケーション(Python / Node.js等)での明示的な対策まで見ていきましょう。

A. Linux環境における systemd-resolved の強制設定

UbuntuなどのモダンLinuxディストリビューションでは、systemd-resolved がDNSトラフィックを管理しています。VPN接続時にDNSが漏れないよう、特定のドメインやインターフェースに対して厳格なルーティングを強制します。

/etc/systemd/resolved.conf に以下の設定を記述し、フォールバックDNSの動作を制御します。

[Resolve]
# プライベートなVPNのDNSサーバーをプライマリとして明示
DNS=10.8.0.1
# パブリックなフォールバックDNSが勝手に優先されるのを防ぐ
FallbackDNS=1.1.1.1 8.8.8.8
# パブリックなDNSクエリに対してDNSSECを有効化し、ハイジャックを防止
DNSSEC=yes
# キャッシュを有効化し、無駄な外部トラフィックを抑制
Cache=yes

設定を反映するには、サービスを再起動します。

sudo systemctl restart systemd-resolved

B. Pythonスクリプト・APIクライアント側でのDNSサーバー固定(dnspythonの活用)

インフラ側の設定に依存せず、アプリケーションやCLIツール(Python製スクリプトなど)のレベルで、DNSリークを絶対に防ぎたい場合があります。特にセキュリティ監査ツールや社内向けAPIクライアントを実装する際は、名前解決の宛先をコード内でハードコーディング(または設定ファイル化)するのが確実です。

Pythonの dnspython ライブラリを使用し、強制的にVPN側のDNSサーバーを使って名前解決を行い、そのIPアドレスに対して直接HTTPリクエストを送る堅牢なコード例を示します。

import dns.resolver
import requests

def secure_api_request(hostname: str, endpoint: string, vpn_dns_ip: str):
    """
    OSのデフォルトリゾルバーを使わず、指定したVPN用DNSサーバーを使って名前解決を行い、
    DNSリークを完全に回避してAPIリクエストを送信する関数
    """
    # 1. dnspythonのResolverオブジェクトを新規作成
    resolver = dns.resolver.Resolver()
    
    # 2. クエリを投げる宛先DNSサーバーをVPN内のものに強制指定
    resolver.nameservers = [vpn_dns_ip]
    
    try:
        # 3. 指定したDNSサーバーへ直接名前解決を要求
        answer = resolver.resolve(hostname, 'A')
        resolved_ip = answer[0].to_text()
        print(f"[+] DNS Success: {hostname} -> {resolved_ip} (via DNS: {vpn_dns_ip})")
        
    except Exception as e:
        print(f"[-] DNS Resolution Failed: {e}")
        return None

    # 4. ホスト名(SNIやHostヘッダーのため)を維持しつつ、解決済みのIPへ直接リクエスト
    # ※ SNIの検証が必要なため、requestsを使う場合はurllib3のPoolManager等で
    #    Hostヘッダーと接続先IPをマッピングするアプローチが必要になる点に注意。
    url = f"https://{resolved_ip}{endpoint}"
    
    headers = {
        'Host': hostname # 仮想ホスト対応サーバーのために本来のホスト名を維持
    }
    
    # 実際のリクエスト送信(証明書検証は適切に構成すること)
    response = requests.get(url, headers=headers, verify=True)
    return response

# 実行例
if __name__ == "__main__":
    TARGET_HOST = "api.internal-service.com"
    API_PATH = "/v1/healthcheck"
    TRUSTED_VPN_DNS = "10.8.0.1" # VPNプロバイダまたは社内DNSのIP
    
    # res = secure_api_request(TARGET_HOST, API_PATH, TRUSTED_VPN_DNS)

—

5. まとめ:プロフェッショナルとしての心構え

VPN接続は、それだけでネットワークの安全がすべて保証される「魔法の杖」ではありません。ネットワークスタックの挙動やOSの仕様を理解していないと、目に見えないところでデータが漏れ出し、セキュリティポリシーの大きな穴となります。

特に実務でAPIやクラウドインフラを設計・運用する私たちエンジニアは、「パケットが今どこを通り、どこへ名前解決を求めているのか」を常に意識するマインドセット(オブザーバビリティの精神)が求められます。

「繋いでいるから安心」ではなく、「本当に漏れていないか、ログとパケットで証明できるか」。
このひと手間を惜しまない姿勢こそが、真にセキュアなシステムを築く唯一の近道です。今日の業務の休憩時間でも、ぜひお手元の端末で resolvectl や dig を叩いて、ご自身の環境の安全性を確かめてみてください。

コメント

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