静止衛星を使った時の遅延の簡易エミュレーション

静止衛星(GEO)は、高度 36,000 km で地球の直径の3倍ぐらいのところにあるそうです。結構遠い。光や電波の速度が 299,792.458 km/s なので、地球から静止衛星まで届くのに約 120ms かかる計算となります。地上にある host1 と host2 が静止衛星を経由した通信をすると片道 240ms で、往復で 480 ms になる計算です。 

```plantuml
@startuml
title 片道 240ms
node GEO
node host1
node host2
GEO <-[#green]- host1 : ①\n120ms
GEO -[#green]-> host2 : ②\n120ms
@enduml
```
```plantuml
@startuml
title 往復 480ms
node GEO
node host1
node host2
GEO <-[#green]- host1 : ①\n120ms
GEO <-[#blue]- host2 : ③\n120ms
GEO -[#blue]-> host1 : ④\n120ms
GEO -[#green]-> host2 : ②\n120ms
@enduml
```

この時間（遅延）を docker と tc の netem を使って簡易的なエミュレーションをして体感できればと思っています。

静止衛星を使ったことがないので、どんな感じなのかなという興味から始まっています。（スターリンクも使ったことはないです。）

## 伝搬遅延時間の計算

遅延時間 = 静止衛星高度 / 光や電波の速度

静止衛星高度: 約 36,000 km
光や電波の速度: 299,792.458 m/s

0.120 s = 36,000 / 299,792.458
遅延時間は約 120 ms

## 試した時の環境

OS: Ubuntu 24.04
docker: 27.4.0
コンテナイメージ: alpine

## 参考にした通信帯域

帯域の設定は、スカパーJSAT さんの [衛星通信サービス | いざという時にも繋がる安心の回線を](https://www.skyperfectjsat.space/jsat/service/lp/lp_exbird/) のページにあった 通信速度上り/下り 最大3Mbps/最大10Mbps の数値を利用しています。

# docker による環境の作成

docker compose を使って、作りました。作りやすそうだったので静止衛星をルータとしました。

```plantuml
@startuml
title docker compose による構成のイメージ
node router
node host1
node host2
router -- host1 : link1
router -- host2 : link2
@enduml
```

link1 と link2 は、docker compose の networks で作成します。docker compose の出来上がりのちょっと詳細のイメージは次のような感じです。

```plantuml
@startuml
title docker compose の出来上がりのちょっと詳細のイメージ
node router\n(service) {
    circle "eth0\n(192.169.1.1)" as router_eth0
    circle "eth1\n(192.169.2.1)" as router_eth1
}
node host1\n(service) {
    circle "eth0\n(192.169.1.2)" as host1_eth0
}
node host2\n(service) {
    circle "eth0\n(192.169.2.2)" as host2_eth0
}
node link1\n(network) {
    circle veth as link1_veth0 
    circle veth as link1_veth1
}
node link2\n(network) {
    circle veth as link2_veth0 
    circle veth as link2_veth1
}
router_eth0 -- link1_veth0
router_eth1 -- link2_veth0
host1_eth0 -- link1_veth1
host2_eth0 -- link2_veth1
@enduml
```

<details>
<summary>作成した docker-compose.yml</summary>

```yaml:docker-compose.yml
name: test
services:
  host1:
    image: alpine
    hostname: host1
    container_name: host1
    networks:
        link1:
          ipv4_address: 192.168.1.2
    entrypoint: sleep infinity
  host2:
    image: alpine
    depends_on:
      - host1
    hostname: host2
    container_name: host2
    networks:
        link2:
          ipv4_address: 192.168.2.2
    entrypoint: sleep infinity
  router:
    image: alpine
    depends_on:
      - host1
    hostname: router
    container_name: router
    networks:
        link1:
          ipv4_address: 192.168.1.1
        link2:
          ipv4_address: 192.168.2.1
    entrypoint: sleep infinity

networks:
  link1:
    name: link1
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: link1
    ipam:
      config:
        - subnet: 192.168.1.0/24
          gateway: 192.168.1.254
  link2:
    name: link2
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: link2
    ipam:
      config:
        - subnet: 192.168.2.0/24
          gateway: 192.168.2.254
```
</details>

#  docker compose の実行

docker compose up -d を実行

```
$ sudo docker compose up -d
[+] Running 5/5
 ✔ Network link2     Created                                                                                       0.1s
 ✔ Network link1     Created                                                                                       0.1s
 ✔ Container host1   Started                                                                                       0.6s
 ✔ Container host2   Started                                                                                       0.8s
 ✔ Container router  Started                                                                                       0.8s
$
```

docker compose ps や docker network ls でコンテナやネットワークが作成されたのが確認できます。

```
$ sudo docker compose ps
NAME      IMAGE     COMMAND            SERVICE   CREATED         STATUS         PORTS
host1     alpine    "sleep infinity"   host1     8 seconds ago   Up 7 seconds
host2     alpine    "sleep infinity"   host2     8 seconds ago   Up 7 seconds
router    alpine    "sleep infinity"   router    8 seconds ago   Up 7 seconds
$ sudo docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
2f2e69c7a428   bridge    bridge    local
17c880397ec4   host      host      local
b7d4487ff67f   link1     bridge    local
7472333ccaf4   link2     bridge    local
805c54a55165   none      null      local
$
```

## デフォルトルートの設定

このままでは、通信経路がないので host1 と host2 では通信できません。host1 と host2 の デフォルトルートを静止衛星と想定した router に設定します。
手抜きかもしれませんが、ホスト OS 側から nsenter による docker コンテナの設定用するクリプトをつかって、デフォルトルートを変更しました。

<details>
<summary>nsenter による docker コンテナの設定用するクリプト</summary>

コンテナのプロセスIDを取得し nsenter の target に設定しています。

```shell:nsenter.sh
#!/bin/sh

if [ $# -lt 2 ]; then
    echo "Usage: $0 host prog"
    exit 1
fi
HOST=$1
shift

UID=`id -u`
if [ "$UID" != "0" ]; then
    echo "Please run as root."
    exit 1
fi

PID=`docker inspect --format {{.State.Pid}} $HOST`
if [ -z "$PID" ]; then
    echo "${HOST}: not found"
    exit 1
fi

exec nsenter --target $PID $*
```
</details>

```
$ sudo ./nsenter.sh host1 --net ip route change default via 192.168.1.1
$ sudo ./nsenter.sh host2 --net ip route change default via 192.168.2.1
$
```

遅延を設定していない環境で host1 から host2 へ ping をしてみます。私の環境たと平均 0.260 ms と表示されました。

```
$ sudo docker compose exec host1 ping -c 5 192.168.2.2
PING 192.168.2.2 (192.168.2.2): 56 data bytes
64 bytes from 192.168.2.2: seq=0 ttl=63 time=0.846 ms
64 bytes from 192.168.2.2: seq=1 ttl=63 time=0.130 ms
64 bytes from 192.168.2.2: seq=2 ttl=63 time=0.112 ms
64 bytes from 192.168.2.2: seq=3 ttl=63 time=0.116 ms
64 bytes from 192.168.2.2: seq=4 ttl=63 time=0.097 ms

--- 192.168.2.2 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.097/0.260/0.846 ms
$
```

# tc の netem による遅延と帯域の設定

tc コマンドの netem を使って遅延と帯域を設定します。
ここでは、 遅延時間 120 ms と 上り帯域 2Mbit、下り帯域 10Mbit を設定しました。
実際はパケットロスとかも設定したほうがよさそうですが、ここではロスなしで設定しています。

:::note warn
Windows の WSL2 の ubuntu では、netem は動きませんでした。使えるようにするにはカーネルを netem を有効にしてビルドする必要がありそうです。
:::

```
$ sudo ./nsenter.sh host1 --net tc qdisc add dev eth0 root netem delay 120ms rate 3mbit
$ sudo ./nsenter.sh router --net tc qdisc add dev eth0 root netem delay 120ms rate 10mbit
$ sudo ./nsenter.sh host2 --net tc qdisc add dev eth0 root netem delay 120ms rate 3mbit
$ sudo ./nsenter.sh router --net tc qdisc add dev eth1 root netem delay 120ms rate 10mbit
$
```
<details>
<summary>netem によるジッターの設定</summary>

delay のところに遅延時間と一緒にジッターも設定できます。下記は、遅延 120ms とジッター 10ms を設定した例です。

```
tc qdisc add dev eth0 root netem delay 120ms 10ms rate 3mbit
```
</details>

<details>
<summary>netem によるパケットロスの設定</summary>

今回は、パケットロスは設定していませんが、loss random 0.1% を入れるとランダムで 0.1% のパケットがロスします。random はランダムにパケットをロスさせるモデルで、ranndom 以外にも state、gemodel といったモデルがあるようです。

下記は、ランダムで 10% のパケットロスを設定する例です。

```
tc qdisc add dev eth0 root netem delay 120ms rate 3mbit loss random 10%
```
</details>

遅延を設定した場合で host1 から host2 へ ping をしてみます。
平均 481 ms と表示されました。設定はうまくいったと思っています。

```
$ sudo docker compose exec host1 ping -c 5 192.168.2.2
PING 192.168.2.2 (192.168.2.2): 56 data bytes
64 bytes from 192.168.2.2: seq=0 ttl=63 time=481.078 ms
64 bytes from 192.168.2.2: seq=1 ttl=63 time=480.985 ms
64 bytes from 192.168.2.2: seq=2 ttl=63 time=481.003 ms
64 bytes from 192.168.2.2: seq=3 ttl=63 time=480.953 ms
64 bytes from 192.168.2.2: seq=4 ttl=63 time=481.008 ms

--- 192.168.2.2 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 480.953/481.005/481.078 ms
$
```
<details>
<summary>netem 設定の参照</summary>

```
$ sudo ./nsenter.sh host1 --net tc qdisc show dev eth0
qdisc netem 802d: root refcnt 3 limit 1000 delay 120ms rate 3Mbit
$ sudo ./nsenter.sh router --net tc qdisc show dev eth0
qdisc netem 802e: root refcnt 3 limit 1000 delay 120ms rate 10Mbit
$ sudo ./nsenter.sh host2 --net tc qdisc show dev eth0
qdisc netem 802f: root refcnt 3 limit 1000 delay 120ms rate 3Mbit
$ sudo ./nsenter.sh router --net tc qdisc show dev eth0
qdisc netem 802e: root refcnt 3 limit 1000 delay 120ms rate 10Mbit
$
```
</details>

<details>
<summary>netem 設定の削除方法</summary>

```
$ sudo ./nsenter.sh host1  --net tc qdisc del dev eth0 root
$ sudo ./nsenter.sh router --net tc qdisc del dev eth0 root
$ sudo ./nsenter.sh host2  --net tc qdisc del dev eth0 root
$ sudo ./nsenter.sh router --net tc qdisc del dev eth1 root
$
```
</details>


# 簡単なインタラクティブを試す

host1 から host2 の shell にアクセスして簡単なインタラクティブを試してみます。
host1 と host2 用の window を用意します。

## host2 で nc のサーバモードで shell を実行し待ち受け

nc で標準エラーもクライアントに返したいので、下記のように実行しました。
（素直に ssh サーバとか動かせばよいのかもしれませんん。）

```
$ sudo docker compose exec host2 nc -l -p 5000 -e /bin/sh -c 'sh -i 2>&1'
```

## host1 で nc のクライアントモードで host2 に接続

別の window から host1 で nc のクライアントモードで host2 に接続して ls -l を実行してみました。ちょっと遅く感じますが、すごく遅いという感じではなかった。

```
$ sudo docker compose exec host1 nc 192.168.2.2 5000
/ # ls -l
total 56
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 bin
drwxr-xr-x    5 root     root           340 Dec 15 08:50 dev
drwxr-xr-x    1 root     root          4096 Dec 15 08:50 etc
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 home
drwxr-xr-x    6 root     root          4096 Dec  5 12:17 lib
drwxr-xr-x    5 root     root          4096 Dec  5 12:17 media
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 mnt
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 opt
dr-xr-xr-x  208 root     root             0 Dec 15 08:50 proc
drwx------    2 root     root          4096 Dec  5 12:17 root
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 run
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 sbin
drwxr-xr-x    2 root     root          4096 Dec  5 12:17 srv
dr-xr-xr-x   13 root     root             0 Dec 15 08:50 sys
drwxrwxrwt    2 root     root          4096 Dec  5 12:17 tmp
drwxr-xr-x    7 root     root          4096 Dec  5 12:17 usr
drwxr-xr-x   11 root     root          4096 Dec  5 12:17 var
/ #
```

# iperf による帯域の確認

次は、 host1、host2 に iperf をインストールして帯域も確認してみます。

```
$ sudo docker compose exec host1 apk --no-cache add iperf
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz
(1/3) Installing libgcc (14.2.0-r4)
(2/3) Installing libstdc++ (14.2.0-r4)
(3/3) Installing iperf (2.2.0-r0)
Executing busybox-1.37.0-r8.trigger
OK: 10 MiB in 18 packages
$ sudo docker compose exec host2 apk --no-cache add iperf
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz
(1/3) Installing libgcc (14.2.0-r4)
(2/3) Installing libstdc++ (14.2.0-r4)
(3/3) Installing iperf (2.2.0-r0)
Executing busybox-1.37.0-r8.trigger
OK: 10 MiB in 18 packages
$
```

## host2 で iperf サーバモードを実行

host2 で iperf サーバモードを実行します。

```
$ sudo docker compose exec host2 iperf -s
------------------------------------------------------------
Server listening on TCP port 5001
TCP window size:  128 KByte (default)
------------------------------------------------------------
```

## host1 で iperf クライアントモードで host2 へアクセス

別の window から host1 で iperf クライアントモードを実行します。実行した結果、帯域が 2.54 Mbits/sec と出ました。上りの帯域を 3Mbit にしたので、その値に近いのがでています。帯域の設定もできている感じです。

```
$ sudo docker compose exec host1 iperf -c 192.168.2.2
------------------------------------------------------------
Client connecting to 192.168.2.2, TCP port 5001
TCP window size: 16.0 KByte (default)
------------------------------------------------------------
[  1] local 192.168.1.2 port 45170 connected with 192.168.2.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  1] 0.00-15.91 sec  4.81 MBytes  2.54 Mbits/sec
$
```

# 感想

docker と tc の netem を使って遅延の管理エミュレーションが簡易ながらもできた。単純にコマンド打つのなら特に問題はない感じです。ファイル編集とか色々やると遅く感じるのかも。
今回は、パケットロスをどれぐらい入れればよいのか不明だったので入れませんでした。パケットロスがあると再送が 480ms の遅延が起きるはずなので、簡単なインタラクティブでももっと遅く感じるのかもしれない。

スターリンクのように低軌道衛星だと衛星までの距離も変動するので遅延も変動するはず。netem で遅延幅も設定できるのですがランダムになってしまうので、ちょっと違うかもしれない。低軌道衛星のように遅延変動もよい方法があれば試してみたい。

追記

tc-netem に slot というのもあり、最小遅延、最大遅延が設定出来るみたい。

tc-netem と eBPF の組み合わせでもっと色々できるみたいです。以下のサイトを読んで何が出来るか考えたい

[ebpf-network-emulation](https://github.com/srnbckr/ebpf-network-emulation)