Python
RaspberryPi
WebRTC
P2P
aioice

aioice で 2台の Raspberry Pi を NAT 越しに P2P で直結

はじめに

aioice は、aiortcの作者、ジェレヌ・レネさん(って読み方であってると思うんですが...)による ICE(Interactive Connectivity Establishment) の python による実装です
examplesを見てて、この clientsignaling server そのままで P2P の connection が作れそうな気がしてきたので試してみた所、掲題の件、確認できましたので僭越ながらその旨のご報告をさせていただきます次第です

前提知識の整理と、全体のまとめ

こちらの記事、WebRTCの基本とP2P通信が成立するまでを学ぶ
が大変わかりやすく解説してくださっています

本稿は、上記記事の構成要素の中で、以下を aioice 及びその example の実装を利用して RaspberryPi 同士の P2P 通信を試みるます

  • ICE
  • Signaling Server

本稿を一言で説明すると、Signaling Server を通じて両Raspberry Pi の ICE候補を交換し、その中で成功した接続を利用して P2P 通信をおこないます

ちなみに、以下は Google のサービスを利用します

  • STUN

以下は利用しません

  • TUNE
  • WEB Browser 及び Web Server

また、WebRTC の data channel も利用していません

構成

スクリーンショット 2018-11-09 17.27.18.png

STUN, TUNE

サンプルそのままなので STUN は stun.l.google.com、TUNE はナシです

Signaling Server

Digital OCEAN に Ubuntu 18.04.1 の python 3.6 で examples の signaling-server.py を起動しました
こちらは async def を使っているので python 3.6 以上でないと動かないと思います

offer 側

2018年10月9日版 RASPBIAN STRETCH LITE 上で examples の ice-client.py を実行しました
offer 側は、USB 3G ドングルで、以下のように ppp でドコモの APN に接続しています
PPP 接続は こちらを利用して実現し、その歳に、soracom の SIM と DoCoMo L-05a を利用しました

pi@raspberrypi:~ $ ifconfig
eth0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether b8:27:eb:f4:79:d8  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 87  bytes 14662 (14.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 87  bytes 14662 (14.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1500
        inet 10.199.235.70  netmask 255.255.255.255  destination 10.64.64.64
        ppp  txqueuelen 3  (Point-to-Point Protocol)
        RX packets 109  bytes 16263 (15.8 KiB)
        RX errors 5  dropped 0  overruns 0  frame 0
        TX packets 106  bytes 16485 (16.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

answer 側

ソフトウェア構成は offer 側と同じです
ネットワークは ETHER と WLAN 経由で(なんで2個つけちゃったんだろ ^^;;;; カッコ悪い)OCNに接続しています

pi@raspberrypi:~/aioice/examples $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.12  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::7a5d:edd6:e62e:b813  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:44:90:3e  txqueuelen 1000  (Ethernet)
        RX packets 823  bytes 110646 (108.0 KiB)
        RX errors 0  dropped 4  overruns 0  frame 0
        TX packets 440  bytes 75355 (73.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.11  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::ef62:2274:88bf:c84c  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:11:c5:6b  txqueuelen 1000  (Ethernet)
        RX packets 356  bytes 80065 (78.1 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 53  bytes 9607 (9.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

実行結果

answer 側の実行結果(長いので途中省略しつつ、何をしているのかコメントを追加しました)

pi@raspberrypi:~/aioice/examples $ python3 ice-client.py answer

#... 途中省略

# signaling server に接続
DEBUG:websockets.protocol:client - state = CONNECTING
DEBUG:websockets.protocol:client - event = connection_made(<_SelectorSocketTransport fd=8 read=idle write=<idle, bufsize=0>>)
DEBUG:websockets.protocol:client - state = OPEN

# ICE candidates の交換
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=1, data=b'{"candidates": ["4f87e19c5641aa12ca08de45eceb9ab9 1 udp 2130706431 10.199.235.70 37527 typ host", "f8ea0185abe4b4573095996bcde68ab9 1 udp 1694498815 103.67.223.29 37527 typ srflx raddr 10.199.235.70 rport 37527"], "username": "FHqM", "password": "Q59D1k099V8joRgcD5lgYy"}', rsv1=False, rsv2=False, rsv3=False)
received offer {'password': 'Q59D1k099V8joRgcD5lgYy', 'candidates': ['4f87e19c5641aa12ca08de45eceb9ab9 1 udp 2130706431 10.199.235.70 37527 typ host', 'f8ea0185abe4b4573095996bcde68ab9 1 udp 1694498815 103.67.223.29 37527 typ srflx raddr 10.199.235.70 rport 37527'], 'username': 'FHqM'}
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=1, data=b'{"password": "Hkx5hqTBu85lngdUfYGSyL", "candidates": ["65643b9f98192d70611f83879e2201cd 1 udp 2130706431 192.168.1.12 45479 typ host", "82106a16154025a8c79f9bf0de3a3f39 1 udp 2130706431 192.168.1.11 53408 typ host", "287b29102e73fb7f49e5fbccdf977807 1 udp 1694498815 123.218.7.133 53408 typ srflx raddr 192.168.1.11 rport 53408", "c7151c9e8b8c314d0e31a9180a96b358 1 udp 1694498815 123.218.7.133 45479 typ srflx raddr 192.168.1.12 rport 45479"], "username": "eEcp"}', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client - state = CLOSING

# この後、Candidate Pair のチェックがしばらく続くので省略
# ...

# Peer がつながった!いぇい!
INFO:ice:Connection(0) Check CandidatePair(('192.168.1.12', 45479) -> ('103.67.223.29', 37527)) State.IN_PROGRESS -> State.SUCCEEDED
INFO:ice:Connection(0) ICE completed
connected

# テストとして offer に 'hello' メッセージを送る
sending b'hello' on component 1

# 略...

# offer から 'hello' を受信
received b'hello' on component 1

# 以下、略...

offer側

pi@raspberrypi:~/aioice/examples $ python3 ice-client.py offer

#... 途中省略

# signaling server に接続
DEBUG:websockets.protocol:client - state = CONNECTING
DEBUG:websockets.protocol:client - event = connection_made(<_SelectorSocketTransport fd=7 read=idle write=<idle, bufsize=0>>)
DEBUG:websockets.protocol:client - state = OPEN

# ICE candidates の交換
DEBUG:websockets.protocol:client > Frame(fin=True, opcode=1, data=b'{"candidates": ["4f87e19c5641aa12ca08de45eceb9ab9 1 udp 2130706431 10.199.235.70 37527 typ host", "f8ea0185abe4b4573095996bcde68ab9 1 udp 1694498815 103.67.223.29 37527 typ srflx raddr 10.199.235.70 rport 37527"], "username": "FHqM", "password": "Q59D1k099V8joRgcD5lgYy"}', rsv1=False, rsv2=False, rsv3=False)
DEBUG:websockets.protocol:client < Frame(fin=True, opcode=1, data=b'{"password": "Hkx5hqTBu85lngdUfYGSyL", "candidates": ["65643b9f98192d70611f83879e2201cd 1 udp 2130706431 192.168.1.12 45479 typ host", "82106a16154025a8c79f9bf0de3a3f39 1 udp 2130706431 192.168.1.11 53408 typ host", "287b29102e73fb7f49e5fbccdf977807 1 udp 1694498815 123.218.7.133 53408 typ srflx raddr 192.168.1.11 rport 53408", "c7151c9e8b8c314d0e31a9180a96b358 1 udp 1694498815 123.218.7.133 45479 typ srflx raddr 192.168.1.12 rport 45479"], "username": "eEcp"}', rsv1=False, rsv2=False, rsv3=False)
received answer {'password': 'Hkx5hqTBu85lngdUfYGSyL', 'username': 'eEcp', 'candidates': ['65643b9f98192d70611f83879e2201cd 1 udp 2130706431 192.168.1.12 45479 typ host', '82106a16154025a8c79f9bf0de3a3f39 1 udp 2130706431 192.168.1.11 53408 typ host', '287b29102e73fb7f49e5fbccdf977807 1 udp 1694498815 123.218.7.133 53408 typ srflx raddr 192.168.1.11 rport 53408', 'c7151c9e8b8c314d0e31a9180a96b358 1 udp 1694498815 123.218.7.133 45479 typ srflx raddr 192.168.1.12 rport 45479']}

# この後、Candidate Pair のチェックがしばらく続くので省略
# ...


# Peer がつながった!いぇい!
INFO:ice:Connection(0) Check CandidatePair(('10.199.235.70', 37527) -> ('123.218.7.133', 45479)) State.IN_PROGRESS -> State.SUCCEEDED
INFO:ice:Connection(0) ICE completed
connected

# Anser に 'hello' を送信
sending b'hello' on component 1

# 略...

# Offer から 'hello' を受信
received b'hello' on component 1

# 以下、略...

無事につながって answer が送ってきた b'hello' を offer が受け取れてます、やったね!

感想

WEB とか関係なしで単にデバイス同士を直結させてセンサデータとかを送受信する時はwebrtc である必要性が限りなく希薄というか、動画だのファイル送信だのといったアプリケーションレイヤが必要にならない普通のデータ送受信なのにこの後、webrtc の data channel をつくらなくても、このまま ice が解決してくれた双方向の connection を生で使えばそれで十分な気がしますのですが、どうでしょうか?

蛇足

ぱっと見でこの signaling-server.py のコードがどう見てもチャットサーバにしか見えず、「多分、間違えて流用元のチャットサーバのコードを commit しちゃったんだろうな、そんな人って僕以外にもいるもんなんだなー」とか思ってその日は寝てしまったのですが、その夜、夢の中にレインボーマンのお師匠みたいな神様があらわれて
「N対Nのチャットサーバは、参加者が answert と offer だけだったら ICE Candidate の交換なのではないか」
とお告げをしてくれて、というのは嘘松で、本当は次の日の朝、朝ごはんを食べてて気がついたので今回の実験をしてみたのでした
朝ごはんはやっぱり食べたほうがいいみたいです

related works

references

  • aioice
    • github のリポジトリです
  • gc_modem
    • 3G モデムを端末ですぐに使えるようにする utility 集です