異なるNAT内のPC間でP2P通信をしてファイル転送したり、パイプをつないだりできるツールを作ってみた

ryo_gridです。

新しいソフトウェアを作ったのでご紹介します。

[Over-NAT-Lib](https://github.com/ryogrid/Over-NAT-Lib)

# これ何？
- 異なるNAT内のPC間でP2P通信をしてファイル転送したり、パイプをつないだりできるツール
    - [Piping Server](https://qiita.com/nwtgck/items/78309fc529da7776cba0) にインスパイアされた感じです
- WebRTCのデータチャネルの機能を利用している
- UDPホールパンチングでNAT超えをする
- P2Pの通信路は SCTP over DTLS over UDP (TCP程度には？信頼性のある通信路で、暗号化もされている)

# 仕組み・実装
- WebRTCと基本的には同じです
- NAT超えをするための情報を取得するためにSTUNサーバ(実質的に利用するのはICEプロトコルによって提供されるサービス？）を利用します
- STUNサーバはテスト用にGoogleが公開しているものを利用します
- WebRTCでは、STUNサーバ以外にシグナリングサーバと呼ばれる、通信したい2者の間を取り持つサーバが必要です
- シグナリングサーバは [Piping Server](https://qiita.com/nwtgck/items/78309fc529da7776cba0) を参考に、複数ユーザで共用できるものを私が公開しています
- シグナリングサーバのアドレスは wss://ryogrid.net:11985/ になります。このURI?の表現から分かる通り、シグナリングサーバはWebSocketプロトコルを用いて実装しています
    - 利用者とシグナリングサーバ間の通信は暗号化されています
- 実装言語はPython。WebRTCのデータチャネル構築の部分は aiortc というライブラリを利用しています

# 利用するための要件
- OSは Windows, Mac, Linux での動作を確認しています
- これが何気に厳しいのですが、Pythonランタイムのバージョン3.7以上が必要です。インストールされていない場合は pyenv の利用なども検討しつつよろしくやっていただければ
  - Windows環境の場合は、exe化したものを配布しているので不要です（この記事の一番下にリンクがあります）

# 【重要】注意
- UDPホールパンチングがNATの仕様・機能・設定や、ネットワーク構成によって通らない場合もあるので、必ず通信路が張れるわけではありません
- ですが、8割程度はうまくいくのではいかと思います
- あまり面白くないですが、1つのPCで動かしてみる（シグナリングサーバはインターネット上のものを利用）ということもできて、その場合は基本的には通るはずです
- シグナリングサーバは基本的に通信路を張るための情報の橋渡しぐらいしかしませんが、見ようと思えば私はその内容を見ることができてしまします
    - 特に知ってどうこうという秘密情報などは通過しないので仮に見たところであまり意味はないです
    - 一番危なそう？なのはSDPと呼ばれる2者が通信路を張るための行動を起こすための情報（相手のNATの外側の通れそうなポート番号とか）、互いのメディアデバイスや利用したいコーデックなどの情報を含むデータですが、せいぜい、一時的に外からUDPパケットを送ると、この記事で扱うツールにパケットが届くかも、ぐらいのものです
    - 一方で、通信路を張ろうとしたところから、通信相手に成りすますことは現在の実装上可能なので、この点は危険と考えることはできると思います
    - ただ、なりすましたところで、すぐに気付かれてしまうだろうと思いますし（通信相手の状態と照らし合わせた時に齟齬が生じるため）、当然やるつもりもありません。
    - ですが、2者両方を騙すような成りすましプログラムも書こうと思えば書けるかもしれません
    - というわけで、極論、信じて下さい、と言うしかありません
    - 私の素性については以下の通りです
        - http://ryogrid.net/profile/index.html
        - https://www.linkedin.com/in/ryo-kanbayashi-3a78266a/
- まだバグがそれなりにある、かもしれません（テストはしたつもりですが）

# インストール（pipモジュールをインストールする）
```bash
$ pip install onatlib
```

https://pypi.org/project/onatlib/

pipモジュールをインストールした場合は、これ以降のサーバやツールの実行についての記述は以下のように読み替えて下さい

- python p2p_com_local_server.py [arguments] -> p2p_com_local_serv [arguments]
- python pync.py [arguments] -> pync [arguments]
- python shareable_ws_signailing_serv.py [arguments] -> shareable_ws_signaling_serv [arguments]


# インストール (githubにあるコードをとってくる)
- pythonのセットアップについては省略します
- virtualenv などで環境を隔離しておいた方が何かと安心だとは思います


```bash
リポジトリを clone してきます
$ git clone -b first-release-0624 https://github.com/ryogrid/Over-NAT-Lib.git
必要なpipモジュールをインストールします
$ cd Over-NAT-Lib
$ pip install -r requirements.txt
```

# 使ってみる（下準備編）
まず、clone してきたリポジトリのtoolsディレクトリに移動します

```bash
$ cd Over-Nat-Lib/tools
```

- シグナリングサーバは、通信する2者が10文字以上の他者に推測されない識別子（英数字の文字列）を共有（LineとかSlackとかメールとかで）し用いて送ってくることでマッチングをとることができます。従って、まず、この識別子を決めて共有して下さい。簡単に推測されるものは、他のユーザと接続してしまったり、うまく動かなかったりして面倒なので利用しないでください
- 実装の都合で、2者のどっちがどっちかを識別するために、bob と tom という識別子？を用意してあるので、それぞれ好きな方を選んで下さい
- 選んだ名前に特に理由はありません

# 【重要】注意(2)
- Windowsの場合、シェル？には コマンドプロンプト を利用して下さい。Power Shellの場合、パイプやstdoutの扱いが特殊だったりして、うまく動きません
- 以下でデータ転送について説明しますが、確立した通信路の特性から、送信側のローカルサーバは1MB分データが溜まるまでは、リモートに送出しません（1MB溜まってなくてもEOFを検知した場合はその時点で貯めていたデータをサイズ関係なく送出します）

# 使ってみる (通信路確立)
- カレントディレクトリはtoolsディレクトリになっている前提で始めます
- 内部で、同じp2P_com_local_server.pyを別オプションを与える形で2プロセス立ち上げています。なので結果として3プロセス起動します。親をCtrl-Cで終了すれば、他の2プロセスも終了するように作ってはありますが、親がなんらかの理由で終了処理を行えずに死んだ場合、子の2プロセスはそのまま残ってしまう実装なので、孤児になってしまった場合は、適宜 kill してあげて下さい・・・

まず通信路を確立します。ここで実行する者はローカルホストで動作するサーバプログラムのようなものとなります。
確立された通信路はこのサーバプログラムが維持し、他のプログラムでその通信路を利用するという設計になっています。

識別子は "d5ocew761d" としたとします。
なお、以下では stderr を ファイルにリダイレクトしていますが、これは stderr に結構な量のデバッグ出力を出力してしまっているので、それをコンソールに出すとわけがわからなくなるためです。必ずしも必要なものではありません。
通信路確立のための細かいデバッグ出力（利用しているライブラリが出力するもの）を見たいという方は -v オプションを付け足すと面白いです。

## tom側
```bash
$ python p2p_com_local_server.py --signaling share-websocket --signaling-host ryogrid.net --signaling-port 11985 --name tom --secure-signaling d5ocew761d 2> hogehoge.txt
```

## bob側
```bash
$ python p2p_com_local_server.py --signaling share-websocket --signaling-host ryogrid.net --signaling-port 11985 --name bob --secure-signaling d5ocew761d 2> hogehoge.txt
```

起動する順番は前後しても構いません。

両者が起動したあとで長くとも3分程度で通信路が確立されます。
以下が出力されれば確立されていることが確認できます。

```
sender_proc: datachannel established
recv_proc: datachannel established
```

NAT越えに失敗した場合は以下のようにその旨を出力するようにしてありますが、10分ぐらい待ってダメなら、NAT超えできないんだな、と思ってもいいかもしれません。

```
hole punching to remote machine failed.
```

これにより、それぞれのホストで、ローカルホストの10100番に通信路の書き込み口、10200番に読み出し口が開きます。

単一のPCで試したい場合は、２つのローカルサーバを起動することになりますが、何も考えずにやるとポートがバッティングしてしまうので、一方の起動時に --slide-stream-ports を指定することで、指定した方のポート番号がそれぞれ10101と10201 になります。

終了させたい場合は、実行したターミナルでCtrl-c して下さい。

# 使ってみる (パイプ転送編)
- 以下は通信路が確立されている状態を前提とます
- pync.py という Netcat(ncコマンド)もどきを用意してあるので、それを利用します
- シェルのパイプやリダイレクトによる標準入力、標準出力を利用した転送データの取り扱いの例になります
- カレントディレクトリは tools ディレクトリ

## 送信側
sometexts.txt をリモートに転送します

```bash
$ cat sometexts.txt | python pync.py -c -p 10100
```
**ここではlinxやmacを想定してcatコマンドを用いていますが、Windowsであればtypeコマンドが代わりに使えます。**



## 受信側
受信したデータの行数を求めます

```bash
$ python pync.py -r -p 10200 | wc -l
```

なお、起動の順序はどちらが先でも構いません。

## 転送終了
- 転送が終わると pync.py は勝手に終了します
- ローカルサーバのソケット（書き込みポート）のところでの受信バッファのサイズがそれなりにあるので、転送するデータが数KB程度のサイズの場合、受信側がpync.py を接続していなくても、送信側のpync.pyが送出（ローカルサーバへの）を終えて終了してしまう場合がありますが、誤動作ではないのでご心配なく（Piping Serverのようにブロックさせたかったのですがうまくいかなかった）
- 確立された通信路はそのまま維持されているので、例えば同様のことをもう一度やっても動作するはずです

# 使ってみる (ファイル転送編)
- 以下は通信路が確立されている状態を前提とます
- カレントディレクトリは tools ディレクトリ
- 送信側主体で受信側の承認なしにファイルを転送できてしまう機能です
- 迷惑な使い方はやめましょう
- 転送されたファイルは基本的に、受信側の p2p_com_local_server.py 実行時のカレントディレクトリに書き出されますが、ファイル名を悪意を持って指定した場合、その限りではありません（チェック機構は未実装・・・）

## ファイル送信側
ファイル名は somefile.tar.gz とします。ここでは、cat & パイプで中身を流したファイルと同じ名前を指定していますが、別のファイル名を指定しても別に構わなくて、ただ、そのファイル名で書き出されるだけです。
上記のパイプ転送の例と異なるのは、pync.py に -f オプションを与えて 書き出す先でのファイル名を指定しているところです。

```bash
$ cat somefile.tar.gz | python pync.py -f somefile.tar.gz -c -p 10100
```

受信側は特に何もする必要がありません。
転送が終了すると、pync.py は終了します。

# 確立した通信路を利用するためのライブラリ
- シンプルなライブラリを書きました (大したことはしていません)
- 一応、自分でゼロからソケット接続するよりはラクになるはずです
- 受信側はメッセージ受信時に指定したコールバック関数でハンドリングさせるような実装も可能です
- ライブラリはローカルサーバと接続した後、Pythonのただのソケットオブジェクトを返します。従って、そのあとの利用方法はただのソケットプログラミングです
- 簡単なエコーサーバ・クライアントが一つになったコードをサンプルコードとして用意したので、使い方はそちらをご参照下さい
    -  https://github.com/ryogrid/Over-NAT-Lib/blob/f042801b29b6865d875d35417cda9031d7afcee5/example/echo_test.py

# 自分でシグナリングサーバを立てる
- お前のサーバなんて信用ならんから使えん！という方は自分でシグナリングサーバを立てることもできます
- 以下はtoolsディレクトリをカレントディレクトリにしたところからスタート

## 暗号化ありで立てる
- 暗号化あり(wss)
- toolsディレクトリに証明書を置く
    - ファイル名は privkey.pem と fullchain.pem

サーバ起動

```bash
$ python python shareable_ws_signaling_serv.py --secure --port listenするポート番号 &> sigserv_log.txt
```

終了はCtrl-Cでいけます。
これまでに説明した、データ転送のために起動するローカルサーバ(p2p_com_local_server.py) には --secure-signaling オプションをつけたままで大丈夫です。

## 暗号化なしで立てる
- お前は信用できないが、証明書を取得するのは面倒だ
- 以下はtoolsディレクトリをカレントディレクトリにしたところからスタート

サーバ起動

```bash
$ python python shareable_ws_signaling_serv.py --port listenするポート番号 &> sigserv_log.txt
```

終了はCtrl-Cでいけます。
これまでに説明した、データ転送のために起動するローカルサーバ(p2p_com_local_server.py) には シグナリングサーバと暗号化した通信をするよう指示する --secure-signaling オプションはつけないようにして下さい。




# Pythonの環境を作ったりするのはめんどくさいという方
Windowsだけですが、ローカルサーバ（p2p_com_local_server.py） と pync.py を exeファイル化して、環境を作らなくても使えるようにしました。
動作確認はWindows10 64bit環境だけでしか行っていませんが、Windows7以上くらいなら動作するのではないかと思います。
（ちなみに、exe化にあたってはpyinstallerを利用しました）

以下からzipをダウンロードし、展開してご利用下さい。
利用方法は python コマンドで実行する必要がないだけで、上記の説明と変わりません。
パスが通っていれば、exeのあるディレクトリ以外でも動作すると思います。

[Windowsバイナリの配布](https://github.com/ryogrid/Over-NAT-Lib/releases/tag/0.0.2)

ひとまず、以上！