Tailscale越しのSSHとMTU 1280
Tailscale を挟んでも、SSH の設定はほとんど変わりません。Tailscale が解くのは到達性だけで、認証は従来どおり sshd が行います。
つまり公開鍵認証で作った鍵も ~/.ssh/config もそのまま使えて、変えるのは HostName に書く宛先だけです。ただし経路のMTU (1個のIPパケットで運べる最大の大きさ) が 1280 バイトに固定されるので、ある条件下だけ鍵交換がこの境界に当たります。実際のtailnet (Tailscale でつないだ端末の集まり) で測った値で示します。
変わるのは宛先だけ
Host pi
HostName 100.x.y.z # tailnet 内のIP、または MagicDNS 名
User deploy
IdentityFile ~/.ssh/lab
宛先は tailnet の中の IP か、MagicDNS (tailnet の中だけで引ける端末名) の名前です。踏み台の行が消えます。NATの内側に居る機械へ、踏み台を置かずに直接届くようになるからです。authorized_keys も PasswordAuthentication no も、そのまま効いたままです。

scp と rsync を実測で使い分けるの章で測った scp と rsync も、同じ ~/.ssh/config を読むので同じように乗るはずです。tailnet 越しの転送量は測っていません。
経路のMTUを測る
tailscale0 の MTU は 1280 です。
$ ip link show tailscale0
3: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 ...
インターフェースの表示ではなく、相手まで実際に何バイト通るかを測ります。ping -M do は分割を禁止するので、通らないサイズで落ちます。
payload 1200B (IP総長 1228B): 0% packet loss
payload 1232B (IP総長 1260B): 0% packet loss
payload 1252B (IP総長 1280B): 0% packet loss
payload 1280B (IP総長 1308B): 100% packet loss
IP パケット全長 1280 バイトちょうどまでが通り、それを超えると落ちます。 経路のMTUが 1280 であることが、インターフェースの設定値ではなく実測で確定しました。

1280 という値は Tailscale の都合ではなく、IPv6 が RFC 8200 で定めている最小MTUです。どんな経路でも通る値として選ばれています。
TCP から見える境界
上位の TCP が1セグメントに載せられるのは、ここからヘッダを引いた分です。
1280 (MTU) − 20 (IPv4) − 20 (TCP) = 1240 バイト
1280 (MTU) − 20 − 20 − 12 (TCPタイムスタンプ、Linuxは既定で有効) = 1228 バイト
1228 バイトが実効の境界です。これを超えるデータを1回で書くと、TCP はフルサイズのセグメントを作ります。
この経路でフルサイズのセグメントが通るかを確かめました。相手の22番へ実際に書き込んで、送信キューが空になるかを見ます。
1回の write サイズ -> 1.5秒後の Send-Q 残量
1000B -> 0 全部ACK済み
1208B -> 0 全部ACK済み
1232B -> 0 全部ACK済み
1400B -> 0 全部ACK済み
4000B -> 0 全部ACK済み
この組み合わせは健全でした。ついでに言うと、この経路は直接接続が張れておらず Tokyo の DERP リレー (直接つながらない端末どうしの通信を Tailscale のサーバーが中継する仕組み) 経由です。
pong from autocrew-wsl (100.72.192.8) via DERP(tok) in 50ms
direct connection not established
リレー経由でもフルサイズのセグメントは落ちていません。 遅延は増えますが、サイズの問題とは別の話です。
鍵交換が境界に当たる条件
問題が出うるのは、クライアントが公開鍵を載せて送る鍵交換パケット (SSH2_MSG_KEX_ECDH_INIT) が 1228 バイトを超えたときです。そしてこのサイズは、両端の OpenSSH の版で決まります。
| 交渉された鍵交換 | クライアントが送る鍵交換パケット (実測) | 1228 との関係 |
|---|---|---|
mlkem768x25519-sha256 | 1232 バイト | 超える |
sntrup761x25519-sha512@openssh.com | 1208 バイト | 収まる |
curve25519-sha256 | 48 バイト | 収まる |
表のバイト数は計算値ではありません。偽の SSH サーバーを立て、OpenSSH 10.0p2 のクライアントが実際に送ってきたパケットの大きさを測った値です (測り方はWSL2 Tailscale SSHがKEXで停止に書きました)。
post-quantum な鍵交換 (量子計算機でも解けないとされる暗号を組み込んだ鍵交換) では、公開鍵そのものがパケットに載るので 25 倍に膨らみます。そして mlkem768x25519-sha256 と sntrup761x25519-sha512 の差はわずか 24 バイトですが、その 24 バイトが境界をまたいでいます。

(MTU は高さ制限のあるトンネルに似ています。高すぎる荷物は通れませんが、TCP は荷を分けて積み直すので、1568 バイトの KEXINIT も届きました。止まりうるのは、その積み直しがどこかでうまくいかないときです)
実tailnet越しに OpenSSH 10.0p2 のクライアントから接続すると、こうなりました。
debug1: Remote protocol version 2.0, remote software version OpenSSH_9.6p1
debug1: kex: algorithm: sntrup761x25519-sha512@openssh.com
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
iris@100.72.192.8: Permission denied (publickey,password).
サーバーが 9.6p1 で mlkem768x25519-sha256 を持っていないため、sntrup761(1208 バイト)で交渉が成立し、境界の内側に収まって認証まで到達しています。両端が 10.x だと mlkem768(1232 バイト)が選ばれ、このパケットが境界を超えます。
ただし、この接続でもクライアントが最初に送るアルゴリズム一覧 (SSH2_MSG_KEXINIT) は、同じ 10.0p2 の既定設定で測ると 1568 バイトあり、1228 バイトを超えています。それでもサーバーに届き、交渉は成立しました。メッセージが 1228 バイトを超えても TCP が分けて送るので、それだけでは落ちない、ということです。

詰まったときに見る順番
ssh -vvの最終行をssh -vv で読む接続の5段階を実測の5段階に当てる。4段階目なら鍵ではないping -M do -sでサイズを振って、経路のMTUを実測するssh -Vを両端で見る。mlkem768が選ばれる組み合わせかを確かめる-o KexAlgorithms=curve25519-sha256 -o Ciphers=aes128-ctrで繋ぎ直す。通ればサイズの問題
4 が通った場合の恒久的な書き方と、なぜ2つ指定しないと効かないのかはWSL2 Tailscale SSHがKEXで停止に書きました。
この章の結論
Tailscale は到達性を解くだけなので、鍵も authorized_keys も人が増えるたびに配り続けることになります。7軸の表で言えば、到達性と公開面は上がりましたが、鍵の配布と失効は公開鍵認証のときのままです。
そこを変える方法が1つ残っています。次の章で扱います。
この記事は役に立ちましたか?