Dart で UPnP によるポートマッピング

Dart 言語 で P2Pアプリを作ろうかと検討しています。それは可能な事なのでだろうか？Dartで利用できるAPIで実現可能なものなのだろうか？

検証して見た結果、chrome extension を利用して実現するのが良いのではないかと考えるに至りました。また、実際にupnpを用いてnat越えを行い。
P2P通信が可能な事を確認しました。

### ネットワークAPIは何を使うべきか

Dart で、ネットワーク機能を使いたいならば、ioパッケージを使うのが普通でしょう。

```lang:こんな感じで宣言する
import 'dart:io'
```
しかし、この方法だと、htmlパッケージが使えません。簡易にGUIアプリを作る方法がなくなってしまうため、とても不便です。
※ Dartでは、コマンドライン用に用意されている機能と、ブラウザー用に用意されている機能を併用できないため。

そこで、chrome extension api を使う事を考えました。chrome extension api は豊富なネットワーク系のapiが用意されています。
これを使えば、P2Pアプリが可能だし、windows mac linux、将来的にはandroid, ios と言ったPFで動作するアプリを提供できます。

* https://developer.chrome.com/apps/app_network
* https://github.com/MobileChromeApps/mobile-chrome-apps/blob/master/docs/APIStatus.md

### P2Pアプリとして必要な条件
p2pアプリとして、必ず満たさなければならない条件は、何でしょうか？ ユーザーが所有する端末同士が直接データ通信をできる事です。P2Pの定義そのものですね。

しかし、意外と難しく。実現は困難です。

##### ルータ(NAT) が邪魔をする。
ネットワークにアクセスする場合、ルーターを通す事がほとんどではないでしょうか？
ルータを通すと、ローカル環境で立ち上げたサーバーに、外部からアクセスしてもらう事が困難になります。
ルーターを通す事で、外部に公開されているアドレスと異なるアドレスが端末にふられるためです。
実際に確認してみましょう。

```lang:コマンドラインとかで
netstat
```
「192.168.100.1」といったアドレスが端末に割り振られている事が確認できます。これは、プライベートネットワーク用のアドレスですね。このアドレスを相手に教えてもアクセスしてもらう事はできません。

##### upnpを使って外部へ接続する。
外部へのアドレスはルーターが知っています。ならば、ルーターに教えてもらうと良いでしょう。ほとんどのルータでは、UPNPという仕組みで依頼を出すことができます。
今回はこれを使用して、他の端末でデータ通信をできるようにしました。

※upnp以外にも、UDP hole punching STUN、ICE といった仕組みがあります。


### UPNPを用いて、ポートマッピングを実現しよう


UPNPを実現するには、UDPとTCPを用いて通信できる必要があります。
具体的には、

* UDP Multicast を利用して、使用中のルーターに、ポートマッピングを依頼すめためのアドレスを教えてもらう。
* TCP を使って、教えてもらったアドレスからポートマッピングの依頼をだす。

といった事をします。Chrome extension では、これらの機能がサポートしています。
なので、実装可能なはずです。

具体的には、

##### 依頼先を調べる

```html:UDPで以下のようなメッセージを通知する
M-SEARCH * HTTP/1.1
MX: 3
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
ST: urn:schemas-upnp-org:service:WANIPConnection:1

```

```html:UDPで以下のような返答がある
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
ST: urn:schemas-upnp-org:service:WANIPConnection:1
USN: uuid:E8088BD3-E808-8BD3-A042-E8088BD3A042::urn:schemas-upnp-org:service:WANIPConnection:1
EXT:
SERVER: E588 UPnP/1.0 MiniUPnPd/1.6
LOCATION: http://192.168.100.1:54616/rootDesc.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: 1
BOOTID.UPNP.ORG: 1
CONFIGID.UPNP.ORG: 1337
DATE: Sun, 14 Sep 2014 00:42:14 GMT
```

LOCATIONで指定されたアドレスのXMLファイルの\<controlURL>タグの中に依頼先のアドレスが記載されています。このアドレスへ依頼をだせば、インターネットから、あなたの端末へアクセスできるようになります。


##### ポートマッピングの依頼をだす
POSTリクエストをだす。

```html:ポートマッピングする
POST 192.168.100.1 HTTP/1.1
Host: 192.168.100.1
Connection: close
SOAPACTION: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"
Content-Length: 629

<?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:WANIPConnection:1">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>48083</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
<NewInternalPort>8083</NewInternalPort>
<NewInternalClient>192.168.100.100</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>test</NewPortMappingDescription>
<NewLeaseDuration>0</NewLeaseDuration>
</m:AddPortMapping></SOAP-ENV:Body>
</SOAP-ENV:Envelope>

```


```html:200OKと返答と返答がある
HTTP/1.1 200 OK
Content-Type: text/xml; charset="utf-8"
Connection: close
Content-Length: 263
Server: E588 UPnP/1.0 MiniUPnPd/1.6
EXT:
DATE: Sun, 14 Sep 2014 01:39:27 GMT

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:AddPortMappingResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"/>
</s:Body>
</s:Envelope>

```

##### 外部の端末から見えているアドレスを調べる

```html:リクエストをだす
POST 192.168.100.1 HTTP/1.1
Host: 192.168.100.1
Connection: close
SOAPACTION: "urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress"
Content-Length: 324
```

```html:レスポンス
HTTP/1.1 200 OK
Content-Type: text/xml; charset="utf-8"
Connection: close
Content-Length: 360
Server: E588 UPnP/1.0 MiniUPnPd/1.6
EXT:
DATE: Sun, 14 Sep 2014 01:46:07 GMT
```

### 実装しよう

実際に上手く動作できたので、githubにあげました。

* https://github.com/kyorohiro/dart_hetimalib/tree/master/lib/upnp
* https://github.com/kyorohiro/HetimaPortMap

無事、端末同士の通信が出来ることが実証できましたので、Dart & Chrome extension を使用することで、
簡単にP2Pアプリケーションを作れる事が解りました。

##### こんな感じ動作する

```html:プライベートネットワークを調べる 

HetiSocketBuilderChrome#getNetworkInterfaces().then((List<HetiNetworkInterface> il) {
  });

```

```html:ルータを発見する
UpnpDeviceSearcher.createInstance(new hetimacl.HetiSocketBuilderChrome()).then((hetima.UpnpDeviceSearcher searcher) {
    searcher.onReceive().listen((hetima.UPnpDeviceInfo info) {
    });
    searcher.searchWanPPPDevice();
  });

```
```html:ポートマッピングする
UpnpPPPDevice#addPortMapping(localIP, localPort, remotePort, UPnpPPPDevice.VALUE_PORT_MAPPING_PROTOCOL_TCP)
           .then((UpnpPortMappingResult r) {
});
```

```html:グローバルアドレスを調べる
UpnpPPPDevice#requestGetExternalIPAddress().then((String address){
});
```