新幹線でもQUICで快適にSSHする

# はじめに

今日は、劣悪な通信環境でも快適にSSHができる[quicssh-rs](https://github.com/oowl/quicssh-rs)をご紹介します。そして、実際に改善されるかを試してみます。

# 長くなったので最初に結論

quicssh-rsを使うと、

- パケットロスに対して明らかに強くなった
- こちらのIPアドレスが変わっても通信が継続できた
- 遅延(pingのRTTが大きいこと)は改善されない模様

30%パケットロス環境での比較

![パケットロスlslコマンド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/7813ad50-7013-7a20-8373-c8b22f0a8640.gif)

IPアドレスを変更しても切れない

![コネクションマイグレーション.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/5b5bf4ed-4625-27c2-4a8b-ba29d74c2c3d.gif)

# SSHは新幹線(劣悪通信環境)に弱い

鉄道での移動時間が長いと、パソコンを開いて各種開発の続きをしたくなります。
…なりませんか？
時には、別のサーバーにSSHでログインして何かコマンドを打ったり。
在来線[^在来線]であれば概ね問題ありませんが、新幹線になると通信品質が大きく悪くなるため、SSHでの操作は大きなストレスが溜まりがちです。なんとかならないかとずっと思っていました。

[^在来線]: 新幹線ではない鉄道のこと。

# 新幹線の通信環境

## 通信手段

新幹線でインターネット接続すると言っても特別なことはなく、普段スマホを屋外で使うことの延長で、携帯電話回線(5G/LTE)を使うことが基本です。ただし、高速移動[^新幹線速度]による基地局のハンドオーバー多発や、トンネルや山間部などがあったりで、通信品質の悪化要因が多くなります。

[^新幹線速度]: 区間にもよるが、260～320km/h程度。最も利用者が多い東海道新幹線(東京～新大阪)は最高285km/h。在来線はまちまちだが、京成電鉄のスカイライナーが160km/h、それ以外は130km/hが上限。

他の候補として、多くの新幹線で提供されている無料WiFiサービスがありますが、WiFiから先は携帯電話回線を使っている[^新幹線WIFI1][^新幹線WIFI2]ようです。

[^新幹線WIFI1]:
    [JR東日本の新幹線Wi-Fi、前倒しで5月から提供 - ケータイ Watch](https://k-tai.watch.impress.co.jp/docs/news/1107456.html)
    「なお、車両内Wi-Fiサービスは携帯電話網を利用しているため、携帯電話の不通区間では利用できない」

[^新幹線WIFI2]:
    [JR西日本に聞く、新幹線の無料Wi-Fiサービス「Shinkansen Free Wi-Fi」開始の舞台裏](https://travel.watch.impress.co.jp/docs/news/1139399.html)
    「ネットワークはJR西日本さんがauのLTEネットワーク、JR東海さんとJR九州さんはNTTグループの回線をそれぞれ利用しています」

## 実際通信環境は悪いのか？

新幹線に頻繁に乗る方の中には、
「え、新幹線ってそんなに通信環境悪いかな？いや特に困ったことないな。動画とかも問題無く見れるし。」
と思った方もいるかもしれません。この認識の差はおそらく、通信内容によって求められる通信環境の要件が異なることが原因です。

動画視聴は、平均して一定以上のスループット(通信速度)が下り方向で出ることが重要ですが、それ以外の品質は求められません。
一方SSHは一般に、応答速度やその安定性が重要であり、それも繋ぎっぱなしであるため長時間安定している必要があります。通信速度(スループット)は基本的には求められません[^SFTP]。

新幹線でpingコマンドを打ってみると分かりますが、表示される時間がまちまちだったり、応答がないことがあると思います。SSHはこのような環境に弱いです。

[^SFTP]: SSHを使ってデータをやりとりするSFTPや、SSHコマンドをパイプで繋いで大きなデータを流す場合などは事情が異なりますが。

# quicssh-rsで改善

そこで、QUICを使ってSSHを中継できる[quicssh-rs](https://github.com/oowl/quicssh-rs)を使って改善してみたいと思います。

## QUICとは

[QUIC](https://ja.wikipedia.org/wiki/QUIC)とは、TCPと[TLS(SSL)](https://ja.wikipedia.org/wiki/Transport_Layer_Security)を置き換えるプロトコルです。TCPで提供されるコネクション管理や信頼性の担保と、TLSで提供される認証や暗号化をUDPの上に実装したものです。

QUICはもともとHTTPを高速化するために、Googleによって開発されました。Google独自プロトコル時代はQUICの中にHTTPを含んでいましたが、IEFTでの標準化後はQUICと[HTTP/3](https://ja.wikipedia.org/wiki/HTTP/3)に分離され、HTTP以外の用途にQUICを使う事ができるようになりました。

HTTPとHTTP/3のプロトコルスタックを比較すると以下の通りです。


<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/ec218c4b-07e5-4b17-c02e-bda52f82b149.png">


QUICの上位に位置するHTTP/3の代わりに任意のデータの送受信を行う事ができます。

SSHを使う場合に重要になるTCPに比べたQUICのメリットとして以下が挙げられます。

#### 再送制御の改善

TCPよりも再送制御が改善しており、パケットロスした場合の再送に伴う遅延がTCPより少ないことが期待できます。
新幹線での厳しい通信環境下での、SSHの改善に役立ちそうです。

#### IPアドレスが変わっても切断されない

TCPでは、1つのコネクションでは通信元と通信先のIPアドレスとポート番号は固定です。つまり、SSHにおいて通信断などでクライアントのIPアドレスが変わってしまえば、SSHは必ず切断されてしまいます。
QUICではコネクションIDというものを用意して通信元・通信先を識別するため、IPアドレスが変わっても通信を継続できます。

新幹線では回線の切断・復帰により頻繁にIPアドレスが変わるため、そのたびにSSHの再接続を行う必要があります。QUICならこれが不要になるため、とても大きなメリットです。

## quicssh-rsとは

SSHはクライアントがサーバーにまずTCP接続を確立し、その上でSSH固有の通信を行いますが、quicssh-rsはその間に入り、QUICで通信を中継します。
以下の図は、[quicssh-rsのREADMEから引用](https://github.com/oowl/quicssh-rs/blob/8ae7c0f161b9314ab1e36eb8dc876a5bfe633d57/README.md#architecture)したものです。

通常時

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/521f9220-0692-0c09-3f5b-be8d91159b5b.png)

quicssh-rsで中継した場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/12ff6332-01d8-2a3c-795d-e128319e08f5.png)


quicssh-rsは上図の通り、クライアントとサーバー双方で動作し、お互いはQUICで通信を行い、ssh/sshdとの中継を行います。

# 実際に試してみる

実際にquicssh-rsを使うことで、劣悪通信環境下でのSSHが改善されるかを確認してみます。

## 実験環境

今回は、AWSの東京リージョンにEC2インスタンスをSSHサーバーとして1個立て、自宅からインターネット経由でSSH接続します。

いくら私が鉄道好きだとしても、この記事を書くために新幹線に乗る訳にはいきません(金銭的な理由)ので、エミュレーションで対処します。幸い、Linuxカーネルには"Traffic Control"という機能が備わっており、劣悪な通信環境を再現できます。

構築内容は以下の通りです。

| 要素                      | 値 | コメント                                                         |
| -------------------- | -------------------- | ---------------------------------------------------------------- |
| リージョン           | 東京 (ap-northeast-1) | 最も(RTT的に)近いリージョンであるため                            |
| OS                   | Amazon Linux 2023    | 個人的にはUbuntuのほうが好きだが、記事にするには万人向けの方が良いかと思い |
| quicssh-rsバージョン | 0.1.1                | 当記事執筆時の最新                                               |

サーバーには、quicssh-rsをダウンロードして起動しました。またTraficc Controlを制御するための`tc`コマンドを使えるようにするため、`iproute-tc`パッケージをyumコマンドでインストールしました。

```shell:quicssh-rsをバックグラウンドで起動
nohup ./quicssh-rs server &
```

SSHのクライアントはWSLのUbuntu 22.04です。

今回は、SSH直接接続とquicssh-rs経由の接続を比較できるように、tmuxを使い、ターミナル画面を左右に分割し、それぞれでSSH接続をします。そして、キーボード操作を左右両方に行うことで、操作性を比較します。

参考: 

https://qiita.com/mogulla3/items/fe2c13c23ec1bf266317

これで、quicssh-rs有りとなしでの操作性の違いを比較できるようにします。

## 実証実験

### エミュレーションなし

まずは、tcによる制御を行わない、素の状態で動作確認します。

#### ping

```
PS C:\> ping -t 18.176.224.222

18.176.224.222 に ping を送信しています 32 バイトのデータ:
18.176.224.222 からの応答: バイト数 =32 時間 =3ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =5ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118

18.176.224.222 の ping 統計:
    パケット数: 送信 = 5、受信 = 5、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 3ms、最大 = 5ms、平均 = 4ms
Ctrl+C
```

いずれのパケットも4ミリ秒前後
で往復し、低遅延で安定しています。パケットロスもありません。

#### SSH操作

![遅延無し-ssh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/717ef91c-8d35-ff27-c0ed-7b297b3de14f.gif)

左が素のSSH、右がquicssh-rs経由です。両者に違いは見られません。

## パケット遅延(一定)

以下のコマンドで、一律200ミリ秒の遅延を入れます。

```shell
sudo tc qdisc add dev ens5 root netem delay 200ms
```


#### ping

```
PS C:\> ping -t 18.176.224.222

18.176.224.222 に ping を送信しています 32 バイトのデータ:
18.176.224.222 からの応答: バイト数 =32 時間 =191ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =197ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =190ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =190ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =189ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =196ms TTL=118

18.176.224.222 の ping 統計:
    パケット数: 送信 = 6、受信 = 6、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 189ms、最大 = 197ms、平均 = 192ms
Ctrl+C
```

200ミリ秒の遅延を入れましたが、pingの結果は180～190ミリ秒ぐらい遅くなりました。

#### SSH操作

![遅延200ms-ssh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/b7071756-6ab5-b23a-66e2-b6d03257f2d1.gif)

遅延を入れたことで、明らかに操作性が悪くなりました。
注目すべき差は、最初のSSHが接続完了するまでの時間です。quicssh-rs経由の方が明らかに接続が高速に完了しています。TCPの3ハンドシェイクより、QUICのほうが高速なことが影響しているのだと思います。
接続されたあとは、両者に違いは特に見られません。QUICを使っても遅延自体が改善する訳ではないからだと思います。

### パケット遅延(ランダム)

今回は遅延にばらつきを持たせてみます。200ミリ秒の遅延に600ミリ秒のジッターを追加します。

```shell
sudo tc qdisc add dev ens5 root netem delay 200ms 600ms
```

#### ping

```
PS C:\> ping -t 18.176.224.222

18.176.224.222 に ping を送信しています 32 バイトのデータ:
18.176.224.222 からの応答: バイト数 =32 時間 =560ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =582ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =471ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =681ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =68ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =5ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =200ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =99ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =663ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =546ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118

18.176.224.222 の ping 統計:
    パケット数: 送信 = 14、受信 = 14、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 4ms、最大 = 681ms、平均 = 277ms
Ctrl+C
```

最小4ミリ秒、最大681ミリ秒と、かなりのばらつきが出ました。

#### SSH操作

![遅延200ms-600ms-ssh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/845008c3-7a7a-a55e-8c9f-542d49f2425d.gif)

遅延がランダムに挟まるため、突然引っかかったりといった操作感です。
ログイン完了はquickssh-rsの方が遅くなってしまいましたが、その後は両者に差はほぼ常にありつつ、どちらが優位かはよく分かりませんでした。
ランダムな遅延では、両者に入る遅延も異なってしまうため、比較は難しそうです。

### パケットロス

さらに、パケットロスを追加します。3割の確率でパケットが消滅します。なお遅延はこれまで通りです。

```shell
sudo tc qdisc add dev ens5 root netem delay 200ms 600ms loss 30%
```

#### ping

```
PS C:\> ping -t 18.176.224.222

18.176.224.222 に ping を送信しています 32 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
要求がタイムアウトしました。
18.176.224.222 からの応答: バイト数 =32 時間 =59ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =234ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =674ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =571ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
18.176.224.222 からの応答: バイト数 =32 時間 =4ms TTL=118
要求がタイムアウトしました。

18.176.224.222 の ping 統計:
    パケット数: 送信 = 11、受信 = 7、損失 = 4 (36% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 4ms、最大 = 674ms、平均 = 221ms
Ctrl+C
```

応答が返ってこないパケットが出てきました。

#### SSH操作

![パケットロス.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/40441c83-b135-a7b3-58be-46f7b99d3630.gif)

パケットロスを入れると、時折詰まるような感じがあり、これまでの中でも操作性が最悪で、新幹線のトンネルに入った感じです。
そしてquicssh-rs経由と直接では、明らかにquicssh-rsのほうが快適です。
コマンドを入力していて詰まる感じはある程度どちらもありますが、quicssh-rsはせいぜい1秒にとどまる一方、直接SSHのほうは数秒レベルで待たされる事がありました。
再送処理がQUICのほうが優れているからでしょうか。

明らかにquickssh-rsが優位な場面を抜き出してみました。
`ls -l`コマンドは応答に数秒差が生じていますし、エディタの操作も右は概ねキーボード操作に追従していますが左はしばらく待ってまとめて表示される状況です。
![パケットロスlslコマンド.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/7813ad50-7013-7a20-8373-c8b22f0a8640.gif)
![パケットロスnano.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/fcfaec2a-c7c7-63c9-7d1d-641cdf671aea.gif)

### IPアドレスの変更

最後に、ネットワークの変更を試してみます。
ここでは、直接SSHとquicssh-rsともサーバーにSSHでログインした状態で、手元のPCを有線LANから無線LANに切り替えてみました。

![コネクションマイグレーション.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221308/5b5bf4ed-4625-27c2-4a8b-ba29d74c2c3d.gif)

上記の通り、直接SSHではSSHが切断されましたが、quicssh-rsでは接続が継続しました。
新幹線の中で一時的な環境の悪化でSSHが切断され再接続する煩わしさが無くなるのはとてもうれしいことです。

## 実証実験 まとめ

長くなりましたが、quicssh-rsを導入した結果は以下の通りまとめます。

- パケットロスや一時的な不通に対して大幅な改善が見られた
- 通信遅延に対する効果はなさそう


# その他

## quicssh-rsについて

### quicsshは使い物にならない

quicsshというものもあります。

https://github.com/moul/quicssh

quicssh-rsはREADMEで「`quicssh-rs` is quicssh rust implementation.」と言っているぐらいで、quicsshのほうが先発です。quicsshはGoで実装されています。

quicssh-rsのスター数はわずか34ですが、quicsshは732です。バージョンもquicssh-rsは0.1.1でベータ版ですらない雰囲気を感じますが、quicsshは1.0.0でなんだか安定感は感じます。
しかし私が使ってみた限り、quicsshは使い物になりませんでした。SSH接続して利用中にファイアウォールでUDPのポートを一時的に塞ぎ、しばらくして再開しても、コネクションマイグレーションもされず、かと言ってエラーも発生せずフリーズしてしまい、強制終了するしかなくなります。quicssh-rsではこういう挙動は起きませんでした。

### SSHを中継してセキュリティは大丈夫なのか？ ⇒ 大丈夫！

SSHをスター数がたった37の得体の知れないソフトウェアに中継させて大丈夫なのか？と思いませんか。実はまったく大丈夫です。

quicssh-rsはSSHプロトコルの中身には一切介入せず、単にSSHのバイトストリームをそのままQUICという手段で転送しているだけです。SSHのネゴシエーションはEnd-to-Endで行われており[^SSH_E2E]、quicssh-rsが介入する余地はありません。

[^SSH_E2E]: SSHは一度サーバーに接続するとその公開鍵を`~/.ssh/known_hosts`に保存します。これが変更になっていなければ、中間者攻撃など受けておらずEnd-to-Endで暗号化されていることが保証されています。

## QUIC-based UDP Transport for SSH

今回は、quicssh-rsでSSHを中継するという、ある意味ハック的な手法を使いました。しかし実は、ちゃんとSSHをQUICの上に流す規格を策定しよう、という動きもあります

[[WIP] QUIC-based UDP Transport for SSH雑まとめ](https://zenn.dev/ubnt_intrepid/scraps/6209f29d0b5166)
[QUIC for SSH の提案仕様が出たよ](https://asnokaze.hatenablog.com/entry/2020/07/09/011947)

個人的にはかなり期待しているのですが、最近あまり動きが見られません。

# 最後に

QUICは素晴らしい！と改めて思いました。通信環境が悪ければ切断されて当たり前、そんな常識は常識ではなかったのだと思います。
スター数たった2桁のソフトウェアでも素晴らしいものがあり、それを発掘できたのも良かったなと思います。