静止衛星(GEO)は、高度 36,000 km で地球の直径の3倍ぐらいのところにあるそうです。結構遠い。光や電波の速度が 299,792.458 km/s なので、地球から静止衛星まで届くのに約 120ms かかる計算となります。地上にある host1 と host2 が静止衛星を経由した通信をすると片道 240ms で、往復で 480 ms になる計算です。
この時間(遅延)を 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 さんの 衛星通信サービス | いざという時にも繋がる安心の回線を のページにあった 通信速度上り/下り 最大3Mbps/最大10Mbps の数値を利用しています。
docker による環境の作成
docker compose を使って、作りました。作りやすそうだったので静止衛星をルータとしました。
link1 と link2 は、docker compose の networks で作成します。docker compose の出来上がりのちょっと詳細のイメージは次のような感じです。
作成した 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
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 コンテナの設定用するクリプトをつかって、デフォルトルートを変更しました。
nsenter による docker コンテナの設定用するクリプト
コンテナのプロセスIDを取得し nsenter の target に設定しています。
#!/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 $*
$ 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 を設定しました。
実際はパケットロスとかも設定したほうがよさそうですが、ここではロスなしで設定しています。
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
$
netem によるジッターの設定
delay のところに遅延時間と一緒にジッターも設定できます。下記は、遅延 120ms とジッター 10ms を設定した例です。
tc qdisc add dev eth0 root netem delay 120ms 10ms rate 3mbit
netem によるパケットロスの設定
今回は、パケットロスは設定していませんが、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%
遅延を設定した場合で 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
$
netem 設定の参照
$ 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
$
netem 設定の削除方法
$ 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
$
簡単なインタラクティブを試す
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 の組み合わせでもっと色々できるみたいです。以下のサイトを読んで何が出来るか考えたい