🦆

家までsshリバーストンネルを掘る

2023/06/13に公開

おうち(NATの内側)に設置されたRaspberry Pi(以下RasPi)と、インターネットに存在する踏み台VPSとの間にsshでトンネルを掘っておき、世界中どこからでも帰宅できるようにしておきます。

Tailscaleすればいい[1]じゃん?それはそう…。

NATの内側から外側へしか接続できないので、RasPiから踏み台VPSに対してssh接続してリバースsshトンネルをつくり、踏み台VPSの適当なlocalhost:portからRasPiのsshdへ接続できる状態を維持することを目標にします。

Raspberry Pi側の設定

ssh + systemd

ServerAliveIntervalを設定してsshプロセスを起動するとsshのkeep aliveを送るようになり、pingへの応答がServerAliveCountMaxに達するとsshプロセスが落ちます。ここでsystemdをつかってsshプロセスをおもりさせると、sshプロセスが落ちるたびに再起動してくれるようにになります。多少の気休め[2]としてExitOnForwardFailure onをつけてport forward初期化に失敗すると落ちるようにします。

StrictHostKeyCheckingはお好み[3]ですがyesにするなら一度はトンネル用鍵対使ってsshセッションを発行して、known_hostsに書いておく必要があります。

あとは、Tでptyを割り当てず、Nでコマンド実行なし、qで静かに。

[Unit]
Description=ssh reverse tunneling
After=network-online.target ssh.service
Wants=ssh.service

[Service]
User=pi
ExecStart=/usr/bin/ssh -TNq -o "StrictHostKeyChecking yes" -o "ExitOnForwardFailure yes" -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" walkure@example.jp -R 50022:localhost:22  -i ~/.ssh/id_tunnel
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

autossh

ServerAliveIntervalでssh接続が腐った場合を検出できるようになりますが、autosshを使うとechoトンネルを別途掘ってforwardを監視させることができます。

autosshは接続監視のecho用にトンネルを2本掘ります。

https://medium.com/veltra-engineering/autossh-6aae10b5eb12

後述のforwarding port制限を使う場合は、-Mを使って明示的に監視トンネルで使うポートを指定しておきます。なお、-M 0で起動すると、トンネル掘っての監視をしなくなります。

また、環境変数AUTOSSH_GATETIME0指定[4]して、autosshプロセスが終了しないようにします。

ExecStart=/usr/lib/autossh/autossh -M 50000 -TNq -o "StrictHostKeyChecking yes" -o "ExitOnForwardFailure yes" -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" walkure@example.jp -R 50022:localhost:22  -i ~/.ssh/id_tunnel
Environment=AUTOSSH_GATETIME=0

踏み台の設定

authorized_keysの設定

RasPi起動したとき勝手にトンネル掘ってほしい[5]ので、秘密鍵にパスワードをつけられない。なので、この鍵をauthorized_keysのoptionでトンネル専用にします。ぐぐるとno-ptyなどを書き連ねる記事が出てくるんですが、manを見たらOpenSSH 7.2restrictというoptionが増えていて、no-*を書き連ねなくても良くなりました。

このoptionsは前から読まれてゆきrestrictを見つけるとすべてのpermit flagsを倒す実装になっているので、restrictより先にport-forwardingを書いても効果がありません(はまった)。
https://github.com/openssh/openssh-portable/blob/2709809fd616a0991dc18e3a58dea10fb383c3f0/auth-options.c#L339-L345

また、好き勝手にforwardされても困るので、permitlistenでremote forwardを制限し、permitopenでinvalidなhostnameを指定してlocal forwardを潰します[6]

restrict,port-forwarding,permitlisten="localhost:50022",permitopen="_invalid_host_:1",command="/usr/sbin/nologin" ssh-なんたら~~~~

permitlistenhostを省略してportのみ書いた場合何が起きるかドキュメントに明示されていないので実装を見に行くと、単にportのみ制限するようです。

https://github.com/openssh/openssh-portable/blob/2709809fd616a0991dc18e3a58dea10fb383c3f0/auth-options.c#L265-L270

今回はlocalhost[7]にbindするので、明記します。

autosshの監視を使う場合は追加で「-Mで指定したportへのlocal forward」と「127.0.0.1:(-Mで指定したport)[8]からのremote forward」を行うので、これらを許可します。

restrict,port-forwarding,permitlisten="localhost:50022",permitlisten="localhost:50000",permitopen="127.0.0.1:50000",command="/usr/sbin/nologin" ssh-なんたら~~~~

.ssh/configの設定

これで踏み台VPS上のポート(今回は50022)でRasPiのsshがlistenしてる(と見なせる)ようになります。あとは.ssh/configでよしなに端折れるようにしておくと便利。

Host myhome
        HostName localhost
        User pi
        Port 50022

この踏み台VPSを経由して別のホストから繋ぐ場合はProxyJumpを使う感じで。

Host myhome
    HostName localhost
    User pi
    Port 50022
    ProxyJump bastion
脚注
  1. Tailscaleのデフォルト180日で認証切れる(cf. Key Expiry · Tailscale)設定外すのを完全に忘れていて、豪雨で新幹線が止まって帰れない2023年のお盆に認証切れてsshで乗り込む羽目になりました。 ↩︎

  2. manにも「ExitOnForwardFailure does not apply to connections made over port forwardings 」って書いてあるし、ソースコードのコメントにはExit if bind(2) fails for -L/-Rという書き方がされていたりします。 ↩︎

  3. StrictHostKeyChecking noにした上で、UserKnownHostsFile/dev/nullにすることで、問答無用で接続させることも出来ます。 ↩︎

  4. 0にしない場合、default valuteとして30が設定されautosshがsshプロセスをを起動して30秒以内にsshプロセスが落ちた場合は初期設定失敗と見做してautosshが終了するようになります。 ↩︎

  5. 出先にいるとき死んで沈黙されると辛いので、WDT入れたりしますよね。 ↩︎

  6. SSH: how to allow only local port forwarding and forbid remote one (or vise versa) selectively on key basis - Super Userを参照。 ↩︎

  7. manに書かれていますがsshコマンドはホスト名を端折った場合localhostを送ってきます。なので127.0.0.1と書くとpermitされません。 ↩︎

  8. ここで出てくる127.0.0.1はautossh実装に埋め込まれていて変更できません。local/remoteどちらも127.0.0.1:監視portにbindできることを仮定しているので、loがIPv4アドレスを持たない場合はつらい気持ちになりそう。 ↩︎

Discussion