2026年5月24日
2026年5月24日
NATトラバーサルとSTUN/TURNサーバーの設定(coturnの構築とWebRTCからの使い方)
NATの内側にある端末どうしを直接つなぐ(NATトラバーサル)には、自分の外側のアドレスを調べるSTUNと、直接つながらないときに中継するTURNの2つを用意するのが基本です。この記事では、STUN・TURN・ICEの役割を整理したうえで、オープンソースのcoturnで自前のSTUN/TURNサーバーを構築し、ファイアウォールと認証を設定して、WebRTCのアプリ(WordPressサイトに組み込む場合を含む)から使うまでの手順を説明します。
NATトラバーサルが必要になる理由
家庭やオフィスの端末の多くは、ルーターのNAT(正確にはポート番号も変換するNAPT)の内側でプライベートIPを使っています。外へ出ていく通信はルーターがグローバルIPとポート番号に変換してくれますが、外から内側の端末に向けていきなり通信を始めることはできません。ビデオ通話やゲーム、P2Pのファイル共有のように、端末どうしが直接やり取りしたい用途では、これが壁になります。
NATトラバーサルは、この壁を越えて通信を成立させる技術の総称です。WebRTCでは、次の3つの仕組みを組み合わせます。
- STUN:端末がSTUNサーバーに問い合わせ、「外から見た自分のIPアドレスとポート番号」を教えてもらいます。この情報を相手に伝えれば、相手はそこへ向けて送信できます。
- TURN:直接の通信がどうしても成立しない場合に、サーバーがすべての通信を中継します。確実性は高い反面、サーバーの帯域を消費します。
- ICE:端末が持つ候補(ローカルアドレス、STUNで得たアドレス、TURNの中継アドレス)を集めて、実際につながる組み合わせを順に試し、最もよい経路を選ぶ手順です。
STUNだけでは足りないケース
STUNで得たアドレスが使えるかどうかは、NATの振る舞いで決まります。宛先が変わっても同じ外側のポートを使い続けるNAT(RFC 4787でいう「エンドポイント非依存のマッピング」)なら、STUNで調べたアドレスに相手から届く可能性が高くなります。
一方、宛先ごとに別の外側ポートを割り当てるNAT(いわゆる対称型NAT)や、通信事業者側でさらにNATをかけるCGNAT、UDPを通さない企業ネットワークでは、STUNで得たアドレスが相手には使えません。こうした環境の利用者が一定数いる以上、実運用のサービスではTURNも用意しておくのが現実的です。
coturnでSTUN/TURNサーバーを構築する
coturnは、STUNとTURNの両方を提供するオープンソースのサーバーで、多くのLinuxディストリビューションでパッケージとして配布されています。ここではUbuntu/Debian系を例にします。グローバルIPを持つVPSやクラウドのインスタンスを用意し、turn.example.com のようなホスト名をそのIPに向けておいてください。
sudo apt update
sudo apt install coturn
古いパッケージでは、/etc/default/coturn の TURNSERVER_ENABLED=1 の行を有効にしないと起動しないものがあります。インストール後にこのファイルの有無と中身を確認してください。
設定ファイル
設定は /etc/turnserver.conf に書きます。元のファイルはコメント付きの見本なので、コピーを取ってから編集します。
sudo cp /etc/turnserver.conf /etc/turnserver.conf.bak
# /etc/turnserver.conf の例(値は自分の環境に置き換える)
listening-port=3478
tls-listening-port=5349
# クラウドなど1対1NATの環境では「グローバルIP/プライベートIP」の形で書く
external-ip=203.0.113.10/10.0.0.5
# 中継に使うUDPポートの範囲(ファイアウォールでも同じ範囲を開ける)
min-port=49152
max-port=65535
realm=turn.example.com
fingerprint
# 期限付きの一時パスワード方式(共有シークレット)を使う
use-auth-secret
static-auth-secret=your-long-random-secret
# TLS証明書(Let's Encryptなどで取得したもの)
cert=/etc/coturn/certs/fullchain.pem
pkey=/etc/coturn/certs/privkey.pem
# 中継先として内部ネットワークへ到達させない
no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
no-cli
設定のうち、つまずきやすい点を説明します。
external-ip:クラウドのインスタンスは、OSから見えるのはプライベートIPで、外とはプロバイダー側の1対1NATでつながっています。この設定が無いと、coturnはプライベートIPを候補として返してしまい、外の端末から中継アドレスに届きません。
denied-peer-ip:TURNサーバーは、認証を通った利用者の指示で任意の宛先へ通信を中継します。制限しないと、サーバーと同じ内部ネットワークや、クラウドのメタデータ用アドレス(169.254.169.254)への踏み台にされるおそれがあります。自分の環境の内部アドレス帯も追加してください。
static-auth-secret:推測されにくい長いランダム文字列にします。openssl rand -hex 32 などで生成できます。この値はサーバー側だけで使い、ブラウザには渡しません。
証明書のファイル権限:coturnは専用ユーザーで動くことが多く、Let's Encryptの秘密鍵を直接読めないことがあります。更新時にコピーして権限を合わせるなどの工夫が必要です。
ファイアウォールと起動
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 5349/tcp
sudo ufw allow 49152:65535/udp
sudo systemctl enable --now coturn
sudo systemctl status coturn
クラウドを使っている場合は、OSのファイアウォールに加えてセキュリティグループなどでも同じポートを許可します。中継用のポート範囲を開け忘れると、STUNは動くのにTURNの中継だけ失敗する、という分かりにくい状態になります。
動作を確認する
まずSTUNとして応答するかを、coturnに付属するツールで確認します。
turnutils_stunclient turn.example.com
外から見たアドレスが表示されれば、STUNは動いています。次に、WebRTCの公式サンプルにある「Trickle ICE」のページ(webrtc.github.io の samples 内)を開き、STUN/TURNのURLと、後述の方法で作った一時ユーザー名・パスワードを入れて候補を集めます。結果の種類欄で、srflx はSTUNで得たアドレス、relay はTURNの中継アドレスを表します。relay が出なければ、認証情報、中継ポートの開放、external-ip の順に疑います。
うまくいかないときは、/var/log/ 配下や journalctl -u coturn でcoturnのログを確認します。認証の失敗か、通信が届いていないのかで対処が変わります。
WebRTCアプリ(WordPress)から使う
use-auth-secret 方式では、ユーザー名に有効期限のUNIX時刻を含め、パスワードを「共有シークレットでユーザー名をHMAC-SHA1した値をBase64にしたもの」にします。サーバー側で発行すれば、シークレットをブラウザに出さずに済み、漏れても期限が来れば使えなくなります。
WordPressに組み込むなら、ログイン中のユーザーにだけ一時認証情報を返すREST APIを、mu-pluginや子テーマの functions.php に用意する方法があります。シークレットは wp-config.php に define( 'MYAPP_TURN_SECRET', 'your-long-random-secret' ); のように定数で置き、コードやリポジトリには書きません。
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myapp/v1', '/turn-credentials', array(
'methods' => 'GET',
'permission_callback' => function () {
return is_user_logged_in();
},
'callback' => function () {
if ( ! defined( 'MYAPP_TURN_SECRET' ) ) {
return new WP_Error( 'turn_not_configured', 'TURN is not configured.', array( 'status' => 500 ) );
}
$ttl = 3600; // 1時間で失効
$username = ( time() + $ttl ) . ':' . get_current_user_id();
$password = base64_encode( hash_hmac( 'sha1', $username, MYAPP_TURN_SECRET, true ) );
$response = new WP_REST_Response( array(
'username' => $username,
'credential' => $password,
'ttl' => $ttl,
) );
$response->header( 'Cache-Control', 'no-store' );
return $response;
},
) );
} );
ブラウザからCookie認証でこのAPIを呼ぶには、wp_rest のnonceを X-WP-Nonce ヘッダーで送る必要があります。スクリプトを wp_enqueue_script で読み込み、wp_localize_script などでURLとnonceを渡します。
wp_localize_script( 'myapp-call', 'myappTurn', array(
'url' => esc_url_raw( rest_url( 'myapp/v1/turn-credentials' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
const res = await fetch(myappTurn.url, {
headers: { 'X-WP-Nonce': myappTurn.nonce },
credentials: 'same-origin',
});
const turn = await res.json();
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:turn.example.com:3478' },
{
urls: [
'turn:turn.example.com:3478?transport=udp',
'turns:turn.example.com:5349?transport=tcp',
],
username: turn.username,
credential: turn.credential,
},
],
});
TLS(turns:)の5349番ポートも候補に入れておくと、UDPやTCPの一般ポートを止めている企業ネットワークからでも中継できる可能性が上がります。
運用上の注意
- 公開STUNサーバーの扱い:GoogleなどのSTUNサーバーが広く使われていますが、提供条件や継続性が保証されたサービスではありません。業務用途では自前のサーバーを用意しておくと安心です(2026年10月現在)。
- TURNの帯域:中継が発生すると、映像や音声の通信量がそのままサーバーを通ります。VPSの転送量の上限や課金体系を確認し、利用者あたりの帯域に上限を設けることも検討します。
- アップデート:coturnは外部に公開するサーバーなので、OSのセキュリティ更新とあわせてパッケージを最新に保ちます。
まとめ
- NATトラバーサルは、STUNで外側のアドレスを調べ、直接つながらない場合はTURNで中継し、ICEが最適な経路を選ぶ、という組み合わせで実現します。
- 対称型NATやCGNAT、UDPを通さないネットワークではSTUNだけでは足りないため、実運用ではTURNも用意します。
- coturnでは
external-ip、中継ポート範囲の開放、denied-peer-ipによる内部ネットワークの保護が要点です。 - 認証は
use-auth-secretの期限付きパスワードにし、シークレットはサーバー側だけに置きます。WordPressからはnonceと権限チェック付きのREST APIで発行します。