VPN上でのROS（ROS2対応）

下準備
---------

この文書ではVPNとしてtailscaleを想定しています。
tailscaleをまだインストールしていない場合は、下のコマンドでインストールします。

```console
$ curl -fsSL https://tailscale.com/install.sh | sh
$ sudo tailscale up
```

以降、`sudo`なしでtailscaleコマンドが使えるように、以下のコマンドも実行してください。

```console
$ sudo tailscale set --operator=$USER
```

ROS通信の仕組みとROS over VPN
-----------------------

VPN上でROSを理解した上で使うために、まずはROS通信の仕組みを説明します。

`talker`と`listener`の2ノードからなる通信サンプルを立ち上げます。

```console
$ roslaunch roscpp_tutorials talker_listener.launch
```

普段は意識しないで使っていますが、`roslaunch`を起動すると、`rosmaster`というプロセスがポート11311上で自動で立ち上がります。

```console
$ lsof -sTCP:LISTEN -i:11311
COMMAND   PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
rosmaster  99 yosuke    4u  IPv4 24080870      0t0  TCP *:11311 (LISTEN)
```

通信の中身を確認するために、`xmlrpc`コマンドをインストールします。

```console
$ sudo apt install libxmlrpc-core-c3-dev
```

`rosmaster`が使用するxmlrpc APIは、以下に定義されています。

https://wiki.ros.org/ROS/Master_API

ここでは、`rosmaster`からノード`talker`の情報を取得してみましょう。

```console
$ xmlrpc localhost:11311 lookupNode client1 talker
Result:

Array of 3 items:
  Index  0 Integer: 1
  Index  1 String: 'node api'
  Index  2 String: 'http://myhostname:45405/'
```

ホスト`myhostname`のポート45405でノード用のサーバ（ROS slave）が立ち上がっているようです。

slaveが使うxmlrpcを呼び出すことで、ノードの詳細を見ることもできます。

https://wiki.ros.org/ROS/Slave_API

```console
$ xmlrpc myhostname:45405 getBusInfo client1
Result:

Array of 3 items:
  Index  0 Integer: 1
  Index  1 String: ''
  Index  2 Array of 2 items:
             Index  0 Array of 7 items:
                        Index  0 Integer: 0
                        Index  1 String: '/rosout'
                        Index  2 String: 'o'
                        Index  3 String: 'TCPROS'
                        Index  4 String: '/rosout'
                        Index  5 Boolean: TRUE
                        Index  6 String: 'TCPROS connection on port 38369 to [127.0.0.1:36152 on socket 12]'
             Index  1 Array of 7 items:
                        Index  0 Integer: 1
                        Index  1 String: '/listener'
                        Index  2 String: 'o'
                        Index  3 String: 'TCPROS'
                        Index  4 String: '/chatter'
                        Index  5 Boolean: TRUE
                        Index  6 String: 'TCPROS connection on port 38369 to [127.0.0.1:36166 on socket 10]'
```

各ROSノードは、rosmasterにホスト名とポート番号を問い合わせた後で、以降はノード間で直接TCP/IP接続することで効率的に通信します。各ポート番号を追いかけていくことで、ノード間通信の詳細も確認できます。

ここで問題は、`myhostname`というホスト名です。このホスト名には`127.0.1.1`というIPアドレスが割り振られており、VPN内での通信に使うことはできません。

```console
$ ping myhostname
PING myhostname (127.0.1.1): 56 data bytes
64 bytes from 127.0.1.1: icmp_seq=0 ttl=64 time=0.044 ms
64 bytes from 127.0.1.1: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 127.0.1.1: icmp_seq=2 ttl=64 time=0.036 ms
64 bytes from 127.0.1.1: icmp_seq=3 ttl=64 time=0.047 ms
```

VPN内での通信には、以下のコマンドで得られるtailscale内で割り振られたIPアドレスを使う必要があります。

```console
$ tailscale ip -4
100.121.86.85
```

ここで、環境変数`ROS_IP`を使って、上記のIPアドレスを設定してみます。

```console
$ ROS_IP=$(tailscale ip -4) roslaunch roscpp_tutorials talker_listener.launch
```

無事に、ROSが使用するIPアドレスをVPN通信可能なものに変更することができました。

```console
$ xmlrpc localhost:11311 lookupNode client1 talker
Result:

Array of 3 items:
  Index  0 Integer: 1
  Index  1 String: 'node api'
  Index  2 String: 'http://100.121.86.85:36595/'
```

ROS1の場合
---------

ROSノードを立ち上げる前に、環境変数`ROS_IP`と`ROS_MASTER_URI`を設定してください。

```shell
export ROS_MASTER_URI=http://$(tailscale ip -4 rosmasterを立ち上げたホスト名):11311
export ROS_IP=$(tailscale ip -4)
```

ROS2の場合
---------

ROS2ではブロードキャスト通信を使って各ノードのdiscoveryを行いますが、多くのVPNではブロードキャスト通信が利用できません。
ここでは、ブロードキャスト通信を使う代わりに、ROS1の`rosmaster`に相当する`fastdds discovery`を使用します。

```console
$ sudo apt install fastdds-tools
```

`fastdds discovery`を以下のオプションで起動します。

```console
$ fastdds discovery -i 0 -l $(tailscale ip -4) -p 11811
```

毎回の起動が面倒な場合は、supervisordを使って自動起動させるのがおすすめです。

```conf:/etc/supervisor/conf.d/fastdds-discovery.conf
[program:fastdds-discovery]
command=bash -c "fastdds discovery -i 0 -l $(tailscale ip -4) -p 11811"
autostart=true
autorestart=true
```

ROS2ノードを起動させる各ホスト上に、以下の内容のスクリプトを作成します。

ここで、`DS_IP`にはdiscoveryサーバを立ち上げたホストのIPアドレスを設定してください。

```bash:~/fastdds_tailscale.sh
DS_IP=$(tailscale ip -4 discoveryサーバを立ち上げたホスト名)
DS_PORT=11811
HOST_IP=$(tailscale ip -4)

cat > /tmp/fastdds_tailscale_configuration.xml <<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<dds>
    <profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
        <transport_descriptors>
            <transport_descriptor>
                <transport_id>TailscaleTransport</transport_id>
                <type>UDPv4</type>
            </transport_descriptor>
        </transport_descriptors>
        <participant profile_name="client_profile" is_default_profile="true">
            <rtps>
                <userTransports>
                    <transport_id>TailscaleTransport</transport_id>
                </userTransports>
                <useBuiltinTransports>true</useBuiltinTransports>
                <defaultUnicastLocatorList>
                    <locator>
                        <udpv4>
                            <address>${HOST_IP}</address>
                        </udpv4>
                    </locator>
                </defaultUnicastLocatorList>
                <builtin>
                    <discovery_config>
                        <discoveryProtocol>SUPER_CLIENT</discoveryProtocol>
                        <discoveryServersList>
                            <RemoteServer prefix="44.53.00.5f.45.50.52.4f.53.49.4d.41">
                                <metatrafficUnicastLocatorList>
                                    <locator>
                                        <udpv4>
                                            <address>${DS_IP}</address> 
                                            <port>${DS_PORT}</port>
                                        </udpv4>
                                    </locator>
                                </metatrafficUnicastLocatorList>
                            </RemoteServer>
                        </discoveryServersList>
                    </discovery_config>
                    <metatrafficUnicastLocatorList>
                        <locator>
                            <udpv4>
                                <address>${HOST_IP}</address>
                            </udpv4>
                        </locator>
                    </metatrafficUnicastLocatorList>
                </builtin>
            </rtps>
        </participant>
    </profiles>
</dds>
EOF

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export FASTRTPS_DEFAULT_PROFILES_FILE=/tmp/fastdds_tailscale_configuration.xml
ros2 daemon stop
ros2 daemon start
```

ROSノードを立ち上げる前に、以下のコマンドでROS2環境をセットアップします。

```console
$ source /opt/ros/jazzy/setup.bash
$ source ~/fastdds_tailscale.sh
```

余談
---------

ROS2のDDS（FastDDS）は、XML形式の設定ファイルを使うことでプロトコルの詳細までカスタマイズできる。

VPNは本来UDP通信はあまり得意でなく、下のURLで記述された設定によってUDPからTCPに切り替えることもできるが、ROS2ならではのQoS制御を有効化するため、今回はUDPを使う設定にした。
https://fast-dds.docs.eprosima.com/en/latest/fastdds/transport/tcp/tcp.html

tailscaleでは、IPv6アドレスも利用できるのだが、VPN上でのIPv6はまだ実装が枯れていない部分も多いので、今回はIPv4のユニキャスト通信を使う設定にした。