はじめに
前回の記事では、WebRTCの RTCDataChannel を使ってブラウザ間でP2Pファイル転送を実装しました。
でも、「自分のPCで動いたのに、別のネットワークから接続したら繋がらない」という問題に直面したことはありませんか?
これはNAT(Network Address Translation)の壁です。
この記事では、FileExpress を開発する中で実際に遭遇したNAT越えの問題と、STUN/TURNサーバーを使った解決方法を解説します。
なぜP2Pは「繋がらない」ことがあるのか?
NATの仕組み
自宅やオフィスのPCは、プライベートIPアドレス(192.168.x.x や 10.x.x.x)を持っています。このIPは外部から直接アクセスできません。
ルーターがNATを使って、プライベートIPとグローバルIPの変換を行っています。
[PCのIP] [ルーター] [インターネット]
192.168.1.100 → 203.0.113.1 → 相手に届く
P2Pの問題
WebRTCでP2P接続を確立するには、お互いのグローバルIPとポート番号を交換する必要があります。
しかし:
- 自分のグローバルIPを自分では知らない
- NATはランダムなポートを割り当てる
- ファイアウォールが外部からの接続をブロックする
この問題を解決するのが、STUNとTURNです。
STUNサーバーの仕組み
STUN(Session Traversal Utilities for NAT)は、自分のグローバルIPとポートを教えてくれるサーバーです。
動作フロー
1. クライアント → STUNサーバー: 「私のIPを教えて」
2. STUNサーバー → クライアント: 「あなたのIPは 203.0.113.1:54321 です」
3. クライアントはこの情報をシグナリングサーバー経由で相手に伝える
4. お互いのIPが分かったら直接P2P接続
コード例
const configuration = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const pc = new RTCPeerConnection(configuration);
GoogleのSTUNサーバーは無料で使えます。ただしSTUNだけでは繋がらない場合があるのが問題です。
STUNで繋がらないケース
企業のファイアウォールや、一部のキャリアグレードNATでは、STUNが機能しません。
接続成功率の現実
実際のWebRTC実装での接続成功率(一般的な傾向):
| 環境 | 成功率 |
|---|---|
| 同じLAN内 | ほぼ100% |
| 家庭用ルーター同士 | 約80% |
| 企業ネットワーク | 約50% |
| 対称型NAT同士 | ほぼ0% |
残りの20〜50%をカバーするためにTURNサーバーが必要になります。
TURNサーバーの仕組み
TURN(Traversal Using Relays around NAT)は、P2Pが失敗したときの中継サーバーです。
[STUN失敗時]
送信側 → TURNサーバー → 受信側
(サーバー経由になるが、繋がる)
TURNサーバーを経由すると帯域コストがかかるため、必ずP2Pを試してから、失敗時のフォールバックとして使うのがベストプラクティスです。
TURNサーバーの設定
const configuration = {
iceServers: [
// まずSTUNを試す
{ urls: 'stun:stun.l.google.com:19302' },
// STUNで失敗したらTURNにフォールバック
{
urls: 'turn:your-turn-server.example.com:3478',
username: 'user',
credential: 'password'
}
]
};
TURNサーバーの選択肢
| サービス | 月額コスト | 特徴 |
|---|---|---|
| Twilio NTS | 従量課金($0.40/GB〜) | 信頼性高い |
| Metered.ca | 無料枠あり(500MB/月) | 個人開発向け |
| coturn(自前) | VPSコストのみ | 完全制御可能 |
ICE(Interactive Connectivity Establishment)の仕組み
WebRTCはICEというフレームワークで、自動的に最良の接続経路を選びます。
ICE Candidateの収集
pc.onicecandidate = (event) => {
if (event.candidate) {
// 候補が見つかるたびに相手に送信
signalingChannel.send(JSON.stringify({
type: 'candidate',
candidate: event.candidate
}));
}
};
収集される候補の種類:
- host候補: ローカルIPアドレス(LAN内接続用)
- srflx候補: STUNで取得したグローバルIP(インターネット接続用)
- relay候補: TURNサーバー経由(フォールバック)
WebRTCは全候補を試して、最も良い接続を自動選択します。
FileExpressでの実装
FileExpressでは以下の戦略を取っています:
// 1. 無料のSTUNサーバーを複数指定(冗長化)
// 2. LAN内は直接接続(最速)
// 3. WAN越えはSTUN経由のP2P(速度維持)
// 4. 企業環境などはTURNフォールバック
const iceServers = [
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] }
// 必要に応じてTURN追加
];
接続状態の監視
pc.oniceconnectionstatechange = () => {
switch(pc.iceConnectionState) {
case 'connected':
case 'completed':
console.log('P2P接続確立!');
showConnectionType(); // relay か direct かを表示
break;
case 'failed':
console.log('接続失敗 - 別の方法を試してください');
break;
}
};
// 接続タイプを確認(P2P直接 or TURN経由)
async function showConnectionType() {
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
console.log('使用中の接続タイプ:', report.remoteCandidateType);
// 'host' = LAN直接, 'srflx' = P2P, 'relay' = TURN経由
}
});
}
まとめ
WebRTCのNAT越え問題をまとめると:
- STUNサーバー: 自分のグローバルIPを知るために使う(無料)
- TURNサーバー: STUNで繋がらない場合の中継(コストあり)
- ICE: 自動的に最良経路を選択するフレームワーク
実用的なP2Pアプリを作るなら、STUNだけでなくTURNも用意することを強くおすすめします。特に企業向けに使うなら必須です。
実際に動作するP2Pファイル転送を試したい方は、ぜひ FileExpress をお使いください。コードも引き続きシリーズで公開していく予定です。
シリーズ: