この記事はRetty Advent Calendar 2020の24日目の記事です。
- 前書き
- SSHポートフォワーディングのやり方
- なぜ自前で実装したいと思ったのか
- なぜGoで実装したのか
- どう実装するのか
- Yamlファイルでポートフォワーディングの設定をできるように
- ツールとして実装する上、工夫したところ
- どんな時に便利なのか
- 最後に
前書き
この記事は
向けです。
SSHポートフォワーディングのやり方
SSHポートフォワーディングは -L / -R 2つのオプションで実現できますが、それぞれ意味が違います。
-L - Local Forwarding
LocalからのリクエストをSSH Tunnel経由でRemoteへアクセスする際に使います。
# Localの13306ポートへのリクエストをgateway-server経由でmysql-serverの3306ポートへフォワーディングする ssh -L 13306:mysql-server:3306 user@gateway-server
-R - Remote Forwarding
RemoteからのリクエストをSSH Tunnel経由でLocalからアクセスさせる際に使います。
# gateway-serverの13306ポートへのリクエストをLocal経由でmysql-serverの3306ポートへフォワーディングする ssh -R 13306:mysql-server:3306 user@gateway-server
なぜ自前で実装したいと思ったのか
普段の開発において、SSHポートフォワーディングの-L - Local Forwarding(-R - Remote Forwardingはあまり利用しないので実装の方も割愛しています)をよく使うのですが、
sshコマンドをいちいち立ち上げるのは面倒でした。
ポートフォワーディングをssh configで行うこともできますが、複数のプロジェクトをやってると、
それぞれのプロジェクトにおいて必要となる設定が異なるので、ssh configの管理が面倒になります。
なので、もしポートフォワーディングの設定を各プロジェクトのコードベースと一緒に管理できるなら便利じゃないかと思ったのです。
なぜGoで実装したのか
一番の理由はやはりGoが好きだからです。
そして、Goは ssh · pkg.go.dev packageがあってSSH Clientの実装が簡単にできるからです。
どう実装するのか
上の図の通りですが、簡単にコードを貼りながら説明していきます。
(* 説明しやいため、下記のsshコマンド同等の機能を実現する例にしています。)
ssh -L 13306:mysql-server:3306 -i ~/.ssh/id_rsa user@gateway-server
// private key fileをパースしssh.AuthMethodを作る buf, _ := ioutil.ReadFile("~/.ssh/id_rsa") // ~が読めないので絶対パスに変換してください k, _ := ssh.ParsePrivateKey(buf) auth := []ssh.AuthMethod{ssh.PublicKeys(k)}
// 必要なSSH Client Configを作る cfg := &ssh.ClientConfig{ User: "user", Auth: auth, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // ドキュメントは It should not be used for production code. 書かれていて、ツールなので一旦よしとしよう Timeout: 2 * time.Second, // 2秒の接続タイムアウトを設定する }
// gateway-serverの22ポートへ接続したSSH Clientを作る sshClient := ssh.Dial("tcp", "gateway-server:22", cfg)
- LocalからのリクエスをListenする
// localhostの13306ポートをListenする listener, _ := net.Listen("tcp", "localhost:13306") for { // for-loopで来るリクエストを全部受け取る localConn, _ := listener.Accept() ... }
for { ... // mysql-serverの3306ポートへ接続する remoteConn, err := sshClient.Dial("tcp", "mysql-server:3306") }
- Localからの接続とMySQL Serverへの接続の間でデータを相互コピーする
var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() // localConnから読み込んだデータを全部remoteConnへ書き込む _, _ := io.Copy(remoteConn, localConn) }() go func() { defer wg.Done() // remoteConnから読み込んだデータを全部をlocalConnへ書き込む _, _ := io.Copy(localConn, remoteConn) }() wg.Wait()
ものすごくシンプルなコードですが、これでLocalの13306ポートに接続すると、Gatewayサーバ経由して、リモートにあるMySQLサーバへの接続ができるようになります。
Yamlファイルでポートフォワーディングの設定をできるように
好みの問題もありますが、ConfigファイルのフォーマットはYamlにしています。
YamlのパースはGo内製のpackageがないので、 GitHub - go-yaml/yaml: YAML support for the Go language. を利用します。
フォーマットは下記のような感じです。
key_files: - ~/.ssh/id_rsa gateways: - server: user@gateway-server:22 tunnels: - mysql-server:3306 -> 127.0.0.1:13306 - redis-server:6379 -> 127.0.0.1:16379
これで、各プロジェクトのコードベースにYamlのConfigファイルをおいて、ポートフォワーディング利用したいとなったらそのConfigファイルでコマンド起動するだけで良くなります。
(* 私個人は各プロジェクトのルートに .tunnel.yaml という名前でConfigファイルを置いていて、gitのglobal設定でgitのトラッキングから除外しています。)
ツールとして実装する上、工夫したところ
コマンドの終了をコントロールする
context.Context をうまく利用する
ctx, cancel := context.WithCancel(context.Background()) defer cancel() // SIGINT で全終了する go func() { defer cancel() signCh := make(chan os.Signal, 1) signal.Notify(signCh, os.Interrupt) <-signCh }()
内部実装は全部呼び出し側からctxを受け取って、接続作成/終了と待つ必要のある処理を全部ctxで制御する
func (t *tunnel) Forward(ctx context.Context) { ... } func (t *tunnel) startAccept(ctx context.Context, sshClient *reconnectableSSHClient, bindListener *closableListener) { ... } func (c *reconnectableSSHClient) Dial(ctx context.Context, n, addr string) (net.Conn, error) { ... } func (c *reconnectableSSHClient) KeepAlive(ctx context.Context) { ... } func (c *reconnectableSSHClient) reconnect(ctx context.Context) error { ... }
SSH接続が切断された場合の処理
パソコンがスリープモードに入ったり、Wi-Fiが切断したりすると、SSH Clientが維持している接続が切断されることがあります。(この点においてsshコマンドも同じ問題があります。)
なので、SSH接続のKeepAlive処理が必要になります。
具体的に、keepalive@openssh.com へ定期的に空のリクエストを送信し、送信が失敗すると接続を作り直すようにすれば解決します。
どんな時に便利なのか
ポートフォワーディングは様々な場合で便利ですが、私個人は普段の開発で主に以下の時に使用しています。
- アプリケーションがLocalからRemote(プライベートネットワーク)にある複数のデータソース(MySQL, Redis, etc.)へ接続したい時
key_files: - ~/.ssh/id_rsa gateways: - server: user@gateway-server:22 tunnels: - slave-mysql-server:3306 -> 127.0.0.1:13306 - master-mysql-server:3306 -> 127.0.0.1:13307 - session-redis-server:6379 -> 127.0.0.1:16379 - cache-redis-server:6379 -> 127.0.0.1:16380
- マイクロサービスアーキテクチャーを用いた開発において、アプリケーションがLocalからRemote(プライベートネットワーク)にある複数のマイクロサービスへ接続したい時
key_files: - ~/.ssh/id_rsa gateways: - server: user@gateway-server:22 tunnels: - service-A:50051 -> 127.0.0.1:150051 - service-B:50051 -> 127.0.0.1:150052 - service-C:50051 -> 127.0.0.1:150053
最後に
今回紹介したツールのソースコードは既にGitHubに公開しているので、ご興味ある方はぜひ読んでみてください。
Go製のツールなので、Goコマンドが入っている環境だったら簡単にインスートもできるので、ぜひ試してみてください。