ゲームでよくある「NATタイプ」はどう判定しているの?
はじめに
家庭用ゲーム機などのネットワーク設定で「NATタイプ」というのを見たことがある人は多いと思います。
これはオンラインマルチプレイなど通信を行うゲームをする際、ゲーム機器同士で通信可能かどうかを見極める目安として使われます。
本記事では、このNATタイプをどのように判定するのか、 RFC 5780 ベースで簡単に説明します。
この記事はDeNA Advent Calendar 2021の8日目の記事です。
なぜNATタイプの判定を行うのか
一般的なクライアント/サーバモデルの通信であれば、そもそもNATタイプが何であるか気にすることはないと思います。
では、家庭用ゲーム機などがなぜNATタイプを判定するのかというと、「P2Pが成立するかどうか」を見極めるためです。
P2Pで通信を行う際は、NAT(NAPT)が存在する場合、いわゆる「NAT越え」が必要になります。
NATがあると、インターネットへ接続する際にNATを介することになるため、P2Pとはいっても実際にはピア同士はNATのIPアドレス:Portを宛先として通信することになります。
簡単に言うと、このようなNATがある環境でP2Pを成立させることを「NAT越え」と言いますが、それが可能かどうか、NATタイプによってある程度判断することができます。
NATタイプの判定方法
NATタイプは、「NATを外から見た時の振る舞い」によって判定します。
具体的には、RFC5780では「NATのマッピング動作」と「NATのフィルタリング動作」によって判定します。(NATタイプの定義自体はRFC 4787)
NATがある環境でP2P通信をする場合、「通信の宛先がNATのIPアドレス:Portになる」というのを前述しましたが、この時、NATタイプによってはマッピングされるIPアドレス:Portが、通信の宛先によって変わることがあります。
このようなNATタイプだと、通信相手は自分のIPアドレス:Portを特定することが難しいので、P2Pが成立しづらいと予測することができます。
これは、「NATのマッピング動作」によって判定することができます。
また、NATにマッピングされるIPアドレス:Portを特定できたとしても、ファイヤーウォール等によってパケットがドロップされてしまうと通信ができないため、このような場合P2Pは成立しづらいと予測することができます。
これを、「NATのフィルタリング動作」によって判定することができます。
なお、実際にP2Pが成立できないかどうかは、片方のNATタイプだけでは判定はできず、両方のピアのNATタイプを組み合わせないとわかりません。
できるかどうかについては、NATタイプによっては判定可能です。
基本的には片方がP2P成立可能なNATタイプであれば、P2Pは成立可能になります。
たとえば一方のピアが宛先によってIP:Portが変わらず、ファイヤーウォールが全くないNATタイプであれば、もう一方のピアのNATタイプが何であっても、もう一方のピアから見れば原理的にはクライアント/サーバモデルと変わらずP2Pは成立可能と考えられるためです。
よく使われるプロトコル
NATタイプの判定には STUN (Session Traversal Utilities for NAT) というプロトコルがよく使われます。
STUNは RFC 8489 (現時点ではこれが最新のはず) で標準化されています。
このプロトコルは非常にシンプルで、自分のグローバルから見たときのIPアドレス:Portが何であるかを調べるためのプロトコルです。
仕組みとしては、自分のIPアドレス:Portを知りたいクライアントが、STUNサーバに対してSTUN Bindingリクエストを投げることによって、STUNサーバがクライアントのIP:Portをレスポンスとして返します。
STUNサーバのOSSとしては coturn などがあります。
P2Pを成立させるための第一歩として、まず各家庭のゲーム機器やスマートフォンなどが、外から見たときにどのIPアドレス:Portにマッピングされているかわる必要があり、これが一番重要です。
(実際のところ、これが両ピア間でわかってしまえばNATタイプがわからなくてもP2P通信を試みることができます。)
NATタイプは、このSTUNプロトコルを使って、自分のNATタイプの振る舞いを確認して判定します。
NATのマッピング動作の判定方法
NATのマッピング動作は、クライアントがNATにマッピングされるIPアドレス:Portが、宛先のIPアドレス:Portによって変化するかを見て判定します。
マッピング動作によるNATタイプには、以下があります。
- Endpoint-Independent Mapping NAT (EIM-NAT)
- 宛先エンドポイントに依存しないIPアドレス:Portのマッピングを行うNAT
- 異なる宛先IPアドレス:Portに対しても、クライアントにマッピングされるIP:Portが同じになる
- Address-Dependent Mapping NAT (ADM-NAT)
- 宛先IPアドレスに依存するマッピングを行うNAT
- 宛先IPアドレスが異なる場合、宛先ごとにマッピングされるIPアドレス:Portは異なるが、同じ宛先IPアドレスでPortだけが異なる場合は、同じIPアドレス:Portにマッピングされる
- Address and Port-Dependent Mapping NAT (APDM-NAT)
- 宛先IPアドレス:Portに依存するマッピングを行うNAT
- 異なる宛先IPアドレス:Portに対しては、異なるIPアドレス:Portがマッピングされる
- 宛先IPアドレスが同じでも、Portが異なれば違うIPアドレス:Portにマッピングされる
これらのうち、どのNATタイプに該当するか、STUNプロトコルを使って調べることができます。
ただし、宛先を変えたときの振る舞いを確認する必要があるため、異なるIPアドレスとPortで起動しているSTUNサーバが必要になります。
言葉で説明するよりは、以下のマッピング動作によるNATタイプの判定方法のシーケンス図を見ていただいた方が早いと思います。
概ねわかるかもしれませんが、以下の図を理解するために最低限必要なSTUNプロトコルの属性(プロトコルの中身みたいなもの)を記載します。
- XOR-MAPPED-ADDRESS
- 自分のグローバルから見たときのIPアドレス:Portが入っている
- RESPONSE-ORIGIN
- STUNサーバーがレスポンスを返した時のサーバ側のIPアドレス:Portが入っている
- OTHER-ADDRESS
- リクエストを投げた宛先のSTUNサーバのIPアドレスとポート(これをプライマリとする)とは別の、セカンダリのIPアドレスとポートが入っている
- 前提として1.1.1.1:3478でサーバが起動している場合、1.1.1.1:3479, 2.2.2.2:3478, 2.2.2.2:3479でもSTUNサーバが起動しているという想定。
- これをサポートしていないSTUNサーバもある
- この場合は単純に2台のSTUNサーバを使えばマッピング動作の判定自体は可能
- coturnはサポートしている
- リクエストを投げた宛先のSTUNサーバのIPアドレスとポート(これをプライマリとする)とは別の、セカンダリのIPアドレスとポートが入っている
- CHANGE-REQUEST
- クライアントが送るもの
- NATのマッピング動作判定では実質使わない
- 後述のNATのフィルタリング動作で使用するのでここでは省略
判定方法のシーケンス
NATのフィルタリング動作の判定方法
NATのフィルタリング動作は、クライアントがSTUN Binding Requestを送る宛先IPアドレス:Port 以外 からレスポンスを受信できるかどうかによって判定します。
フィルタリング動作によるNATタイプには、以下があります。
- Endpoint-Independent Filtering NAT (EIF-NAT)
- 宛先エンドポイントに依存せず、アウトバウンドパケットとは異なるソースIPアドレス:Portからのインバウンドパケットが許可されているNAT
- ファイヤウォールが全くないのでどこからでもパケットを受信可能
- Address-Dependent Filtering NAT (ADF-NAT)
- 宛先IPアドレスに依存するフィルタリングを行うNAT
- アウトバウンドパケットと同じIPアドレスを持つインバウンドパケットであれば、異なるPortからの通信も許可されている
- Address and Port-Dependent Filtering NAT (APDF-NAT)
- 宛先IPアドレス:Portに依存するフィルタリングを行うNAT
- アウトバウンドパケットと同じIPを持つインバウンドパケットであっても、異なるPortからの通信は許可されていない
Change Requestについて
CHANGE-REQUEST属性について簡単に説明します。
NATのフィルタリング動作は、先ほど説明を省略したSTUNの属性 CHANGE-REQUEST を変えながら、レスポンスを受信できるかどうか確認します。
これには、「change IP」と「change port」というフラグがあります。
NATのマッピング動作判定方法のOTHER-ADDRESSのところで、STUNサーバ側にプライマリとセカンダリのIPアドレスとポートがあるということを説明しましたが、
フィルタリング動作の判定でも、1.1.1.1:3478でサーバが起動している場合、1.1.1.1:3479, 2.2.2.2:3478, 2.2.2.2:3479でもSTUNサーバが起動しているという前提があります。
プライマリのIPとして1.1.1.1、セカンダリのIPとして2.2.2.2、
プライマリのPortとして3478、セカンダリのPortとして3479がある場合、
プライマリのIP:PortにSTUN Binding Requestを送った際は、 CHANGE-REQUEST属性のフラグを変えると、以下のソースIP:Portを使ってサーバからレスポンスが返ってるようになります。
- change IP = 0, change port = 0 の場合、 1.1.1.1:3478 からレスポンスが返ってくる
- change IP = 1, change port = 1 の場合、 2.2.2.2:3479 からレスポンスが返ってくる
- change IP = 1, change port = 0 の場合、 2.2.2.2:3478 からレスポンスが返ってくる
- change IP = 0, change port = 1 の場合、 1.1.1.1:3479 からレスポンスが返ってくる
NATのフィルタリング動作判定においては、
インバウンドパケットの送信元IPが異なれば判定としては十分で2と3を区別する必要はありません。
アウトバウンドパケットと異なるIPからのインバウンドパケットで、ソースのPortが異なることによってフィルタリング動作が変わるということは考えにくいためです。
判定方法のシーケンス
NATを越えてP2Pするには?
NATタイプ判定自体の話ではありませんが、実際にNATを越えてP2Pで通信をする場合、どうすれば良いのでしょうか?
冒頭で、外から見たときに「どのIPアドレス:Portにマッピングされているか」両ピア間でわかってしまえば、NATタイプがわからなくてもP2P通信を試みることができると書きましたが、この具体的な流れもついでに記載します。
まず、両ピアはSTUNプロトコルを使って自分のIPアドレス:Portを調べます。
当然、通信をする際は相手の宛先が必要なので、これを両ピア間で交換する必要がありますが、この仕組みをP2Pの文脈ではシグナリングと呼び、シグナリングサーバを介してピア間でNATにマッピングされたIP:Portを交換し合います。(詳細略)
P2P通信が可能かどうかは、言い換えれば相手のピアに到達可能かどうかということです。
マッピング動作に関してだけいえば、相手のピアのIP:Portが、相手側がSTUNによって調べたIP:Portで通信可能かどうかだけなので、単純にそこに対してパケットを送ればP2P通信可能か分かります。
マッピング動作とフィルタリング動作両方を考慮すると少し厄介で、ADF-NATやAPDF-NATの場合、宛先に依存するフィルタリングを行うため、一度相手に対してパケットを送った後でないと、その相手からのパケットを受信することができません。
逆に送ったことがある宛先であれば、相手はフィルタリングを突破できるのでP2Pが可能になります。
これを突破するための通信の流れをUDPホールパンチングと呼びますが、以下のようなシーケンスになります。
NATタイプ判定方法まとめ
上記の通り、RFC 5780におけるNATタイプ判定は、NATのマッピング動作とフィルタリング動作の組み合わせによって判定します。
実際にNATタイプによってP2P通信が可能かどうかは、EIM-NATかつEIF-NATのようなNATタイプであればすぐに可能であるとわかりますが、APDM-NATのような場合はポート割り当ての規則性を見つけてポート番号を予測するといったような方法( RFC 5128 記載のN+1技術 など)もあり、一概に可能か不可能か判定することが難しかったりします。
また、この記事を見ていただいてお気づきかもしれませんが、P2PでもSTUNやシグナリングでサーバが必要になります。
さらにP2Pできないケースも当然あるので、その場合の救済手段としてリレーサーバを用意することにもなると思います。
(リレーサーバには、STUNの拡張としてTURNというプロトコルがありますが必ずしもTURNである必要はありません。)
自分の家のNATタイプを判定してみたい!という方は、Rustのクライアント側実装サンプルを以下に置いておきますので参考にしてみてください。
STUNのクライアント自体も自前実装で作りは大分雑ですがNATタイプ判定の流れを理解するには十分なのではないかと思います。
Goなら https://github.com/pion/stun などのSTUNライブラリがあるのでNATタイプ判定を自分で実装してみるのも面白いです。
なお、STUNサーバも必要なのですが、この判定を行う場合、STUNサーバーはBinding ResponseでOTHER-ADDRESSを返し、Binding RequestのCHANGE-REQUEST属性をサポートする必要があります。
coturnでそれがサポートされており、いくつかやり方があるのですが、手っ取り早いのはサーバのVMに複数のNICと異なるグローバルIPアドレスを割り当てて設定するのが簡単です。(とはいえちょっと面倒くさいです)
たとえばGoogleCloudであれば、coturnの設定は以下のようなものを追加すれば可能です。
# 複数のグローバルIPを割り当てる必要がある。coturnの設定に以下を追加する。
# listening-ipがプライーベートIPなのは、GCEではグローバルIPはVMのNICに直接アサインされないのでbindに失敗するため。
listening-ip=PRIVATE_IP_1
listening-ip=PRIVATE_IP_2
# external-ipにはGLOBAL_IP/PRIVATE_IPのように設定する。
# これによって、OTHER-ADDRESSでBinding requestを受けとったIP:Portとは別のIP:Portを返すことができる
external-ip=GLOBAL_IP_1/PRIVATE_IP_1
external-ip=GLOBAL_IP_2/PRIVATE_IP_2
alt-listening-port=3479
(なお、Googleが公開しているSTUNサーバは、現時点ではOTHER-ADDRESSおよびCAHNGE-REQUESTをサポートしていないようです。)
その他
RFC 3489 vs. RFC 5780
RFC3489では4つのNATタイプが定義されていましたが、そこで定義されていた4タイプだけではNATタイプを十分網羅できなかったという背景があるようです。
なので、RFC5780で新たにNATタイプが再定義され、NATタイプの分類アルゴリズムが変更されたようです。
この記事でいうNATタイプに、詳しい人はおなじみのフルコーンNATであるとか、シンメトリックNATであるとかが無いのは上記のためです。
RFC 5780自体はExperimentalなものですが、上記の問題があるので現代においてNATタイプ判定を新たに実装するなら、RFC 5780ベースで良いのではないでしょうか。
おわりに
DeNAでは今年、2021年度新卒エンジニア・2022年度新卒内定エンジニアの Advent Calendar もあります!
本 Advent Calendar とは違った種類、違った視点での記事をぜひお楽しみください!
▼DeNA 2021年度新卒エンジニア・2022年度新卒内定エンジニアによる Advent Calendar 2021
この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします!
また DeNA 公式 Twitter アカウント @DeNAxTech では、 Blog記事だけでなく様々な登壇の資料や動画も発信してます。ぜひフォローして下さい!
Discussion