UPnPでNAT越え ~オンラインゲーム開発における課題~

#ゲーム開発の課題・NAT越え(ポート開放)
今回の記事はオンラインゲームなどを開発する際における難関の一つ「NAT越え」についてです。元の記事は[こちら](https://younaship.com/2019/01/20/%e3%82%aa%e3%83%b3%e3%83%a9%e3%82%a4%e3%83%b3%e3%82%b2%e3%83%bc%e3%83%a0%e3%82%92%e3%81%a4%e3%81%8f%e3%82%8b%e3%81%be%e3%81%a7%e3%81%ae%e8%a8%98%e9%8c%b2%ef%bd%9evr%e3%82%92%e4%bd%bf%e3%81%a3%e3%81%9f/)の中にある記事になります。こちらも参考にしてみてください～＾
##そもそも「NAT越え」とは
簡単に説明すると「オンラインネットワークでつながった複数ホスト間の接続を確立すること」です。詳しくはGoogleで検索や元の記事を参考にしてみてください。

##NAT越えをする手段
オンラインゲームを作成する上でプロトコルは早さを求めるためUDPを使用したいと考えました。その上でNAT越えをする為に私が思いついた方法は、
・UDP ホールパンチング
・UPnP
でした。
どちらでも出来そうでしたが、今回は簡単そうなUPnPを使って挑戦してみることにしました。

#UPnPを使ったポートマッピング
![image-4-1024x593.png](https://qiita-image-store.s3.amazonaws.com/0/371710/56e6ddd6-1f49-7729-686f-4a999f9b017f.png)
今回はゲームということだったので作成・動作環境はUnityを使いました。(のちにXamarin.Form + Android での動作確認済み)
##ネットワーク内のルーターを探索する
探索の為に**「M-Search」**リクエストを送信先**「239.255.255.250:1900」**(ブロードキャスト)に**UDP**で送信します。

**リクエスト内容**

```C#:
//送信リクエスト内容 C#
static readonly string REQUEST_MESSAGE = String.Concat(
            "M-SEARCH * HTTP/1.1\r\n",
            "MX: 3\r\n",
            "HOST: 239.255.255.250:1900\r\n",
            "MAN: \"ssdp: discover\"\r\n",
            "ST: service:WANIPConnection:1\r\n" // ST:例
        );
```
**ビルドしたアプリでスマホからM-Searchした時のキャプチャ**
![fire_m-search.png](https://qiita-image-store.s3.amazonaws.com/0/371710/7bcd4e5a-546b-2c1b-826d-34e1ea01c302.png)

リクエストに成功すると**送信元IPアドレス+送信ポート**(上キャプチャの場合 PORT:5999)に応答が来ます。成功すると応答は**HTTP/1.1 200 OK**です。
##ルーターの情報を取得
先ほどのルーターからの応答を元に詳細情報(xmlデーター)をルーターに要求します。
HTTP/GETリクエストで受信要求をします。C#でリクエストを送信するのに以下のようなプログラムを用いました。

```C#:さんぷる♡
public void GetRequest(string url,Action<string> getCallBack)
    {
        Encoding enc = Encoding.GetEncoding("UTF-8");

        WebRequest req = WebRequest.Create(url);
        WebResponse res = req.GetResponse();

        Stream st = res.GetResponseStream();
        StreamReader sr = new StreamReader(st, enc);
        string html = sr.ReadToEnd();
        sr.Close();
        st.Close();

        getCallBack(html);
    }
```
要求に成功するとこちらも「HTTP 200 OK」が返され、XML形式のルーターの詳細情報を入手することができます。なお、今回はXMLの中のタグ<controlURL>内の情報を使います。
##リクエストの送信
先ほど得た情報**controlURL**はルーター操作用のアドレスになっているのでこのアドレス宛に **HTTP / POST** でリクエスト内容(**SOAP**プロトコル)を送信します。 

```xml:SOAPリクエスト例
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>12345</NewExternalPort>
      <NewProtocol>UDP</NewProtocol>
      <NewInternalPort>12345</NewInternalPort>
      <NewInternalClient>192.168.0.12</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>YounashiProgram</NewPortMappingDescription>
      <NewLeaseDuration>0</NewLeaseDuration>
    </m:AddPortMapping>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
```
上の例ではポート「12345」の通信をipアドレス「192.168.0.12」の端末に転送するといった設定をする例です。

リクエストを送信し処理に成功すると、
![fire_port_req.png](https://qiita-image-store.s3.amazonaws.com/0/371710/be6d4252-b7e2-2bdb-19fc-64d913b9fd9e.png)

このようにHTTP/1.1 200 OKで成功したとルーターが教えてくれます。
上の矢印のPOSTで送信している部分が依頼したリクエストです。

なお、失敗だと「 HTTP/1.1 500 Internal Server Error 」が返されます。

主な失敗原因としては
・**既にポートが使用中**
・**UPnP設定の上限まで達している**
などが挙げられます。

何も返ってこない場合、そもそもどこかが間違えているはずですので再確認してみてくださいね。

##動作確認
ルーターから成功と返ってきたとしてもこのリクエストが正しく動作しているか確認する必要があります。
###ルーターの管理者ページから確認
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/dfa17dc1-0b67-b31f-ee73-71cf098dc9b7.png)
リクエストに成功しているとこのように**UPnP**のところに登録されているはずです。
上の画像は先ほどのリクエストをルーター(HUMAX)に行った後に確認したものになります。
なお、Activeになっていることも確認してくださいね。
###外部からパケットを送信してみる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/046d51ea-05dc-8ba5-1248-0571079f4000.png)
今回検証に使ったのはAndroidのスマートフォンです。
UDPの送信テストに使ったのは[以前作ったアプリ](https://younaship.com/2019/02/07/arduino%e3%81%a7%e5%a7%8b%e3%82%81%e3%82%8biot%ef%bd%9e%e5%a4%96%e3%81%8b%e3%82%89%e5%ae%b6%e9%9b%bb%e3%81%ae%e6%93%8d%e4%bd%9c%e3%82%92%e8%a1%8c%e3%81%86%e3%81%be%e3%81%a7%ef%bd%9e/)でシンプルなパケットを送るだけのものです。(※ソースコードは記事内にあり)

スマートフォンのWi-fiをOFFにした後(キャリア回線で試さないと外部ではない)アプリでパケット(0101)を送信してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/371710/f7ddb0dd-d311-9042-54c8-e3622af540c0.png)
成功するとこのように外部から登録したIPアドレスにデーターが送信されていることが確認できるはずです。
#まとめ
NAT越えってゲーム開発だけでなくIoT開発とか様々な分野でも必要な知識だから大切だと思った。この記事が何かの参考になれば幸いですー

※このソースコードを使ったUPnPリクエストを送信する[アプリ](https://play.google.com/store/apps/details?id=com.younaship.UPnP_PortTool)を友達の為に作ったのでパケットキャプチャーなどを用いてもしよければ参考程度ににしてみてください。