2018年9月6日開催のDevelopers Summit 2018 FUKUOKA(デブサミ2018福岡)に登壇しました。
発表内容としては、先日書いた下のブログの内容です。
スライドはこちらです。
今回の登壇はLTセッションで、発表時間が7分と短かったため、話きれなかった部分も多かったです。
なので、またどこかでガッツリ話す場があれば嬉しいなーと思ってます!その際はもう少しSSHプロトコルに踏み込んだ話だったり、実装時に苦労したことなどを話せればいいなーと思ってます!
2018年9月6日開催のDevelopers Summit 2018 FUKUOKA(デブサミ2018福岡)に登壇しました。
発表内容としては、先日書いた下のブログの内容です。
スライドはこちらです。
今回の登壇はLTセッションで、発表時間が7分と短かったため、話きれなかった部分も多かったです。
なので、またどこかでガッツリ話す場があれば嬉しいなーと思ってます!その際はもう少しSSHプロトコルに踏み込んだ話だったり、実装時に苦労したことなどを話せればいいなーと思ってます!
今回は、ユーザが接続先を意識しないSSHプロキシサーバを作った話です。
SSHのユーザ名から動的に接続先ホストを決定し、SSH接続をプロキシします。
比較的規模の大きなサーバ群を管理しており、そこに対して接続してくるユーザに特定のサーバを使ってもらいたい場合を考えます。
すなわち「ユーザtsurubeeには、ssh102サーバを使ってほしい」といったようにユーザとマシン間が紐づいている場合の一番単純な運用方法は、個々のユーザが接続先ホストの情報を知っていることです。
これでも問題ないのですが、何かしらのサーバ管理の理由でユーザに使ってもらいたいサーバが変更した場合、ユーザに通知するなどして意識的に接続先を変更してもらう必要があります。
このようなユーザとそのユーザに使ってもらいたいサーバの紐付け情報をサーバ管理側が一元的に管理して、ユーザに意識させることなく、ユーザとサーバの紐付けを自由にコントロールすることができないかと考え、sshrというプロキシサーバを作りました。
sshr導入後のSSHの流れは以下の通りです。
ここで、重要なのは上図のPluggable Hooks
の部分です。ここにsshrを利用する開発者が自由にロジックを組み込めるようにしています。
例えば、サーバ数・ユーザ数の規模がそんなに大きくない場合は、tomlファイルのようなもので簡単に管理したいかもしれませんし、規模が大きくなると、ユーザとそのユーザに使ってもらいたいサーバの紐付け情報をDBで管理したい場合もあるでしょう。後者の場合はPluggable Hooks
の部分にDBからSELECTする、もしくはAPIサーバ経由でGETするといった処理を書けばよいわけです。
まずはsshrをインストールします。
$ go get github.com/tsurubee/sshr
sshrのトップディレクトリに移動して、docker-compose up
を叩くと、sshrサーバが一台、接続先サーバ用としてhost-tsurubee
(tsurubeeユーザの接続先)とhost-hoge
(hogeユーザの接続先)のコンテナが立ち上がります。
$ docker-compose up Creating sshr_host-hoge_1 ... done Creating sshr_host-tsurubee_1 ... done Creating sshr_ssh-proxy_1 ... done Attaching to sshr_host-tsurubee_1, sshr_host-hoge_1, sshr_ssh-proxy_1 ssh-proxy_1 | ==> Installing Dependencies host-tsurubee_1 | Starting crond: [ OK ] host-hoge_1 | Starting crond: [ OK ] ssh-proxy_1 | go get -u github.com/golang/dep/... ssh-proxy_1 | dep ensure ssh-proxy_1 | go run main.go ssh-proxy_1 | time="2018-09-01T07:15:03Z" level=info msg="Start Listening on [::]:2222"
sshrサーバはローカルホストのポート2222でListenしているため、まずはtsurubeeユーザで接続してみます。パスワードはtestとしています。
$ ssh tsurubee@127.0.0.1 -p 2222 tsurubee@127.0.0.1's password: [tsurubee@host-tsurubee ~]$ hostname host-tsurubee [tsurubee@host-tsurubee ~]$ ls / bin dev etc home lib lib64 lost+found media mnt opt proc root sbin selinux srv sys tmp usr var
すると、host-tsurubee
にSSHログインできていることがわかります。
ログイン後は通常SSHでリモートサーバに繋いだ時と同じように対話的にコマンドが実行できます。
次にhogeユーザで繋いでみると、host-hoge
に繋がっていることがわかります。(こちらもパスワードはtest)
$ ssh hoge@127.0.0.1 -p 2222 hoge@127.0.0.1's password: [hoge@host-hoge ~]$ hostname host-hoge
ポイントは、クライアントはユーザ名以外は同じで
$ ssh ユーザ名@127.0.0.1 -p 2222
と接続しているが、sshrによって接続先が動的に振り分けられている点です。
ちなみに、SCPもできます。
$ scp -P 2222 main.go tsurubee@127.0.0.1: tsurubee@127.0.0.1's password: main.go 100% 853 424.8KB/s 00:00 $ ssh tsurubee@127.0.0.1 -p 2222 tsurubee@127.0.0.1's password: Last login: Sat Sep 1 07:18:08 2018 from sshr_ssh-proxy_1.sshr_default [tsurubee@host-tsurubee ~]$ ls -l 合計 4 -rw-r--r-- 1 tsurubee tsurubee 853 9月 1 07:30 2018 main.go
実装についての補足をいくつか書きます。
今回SSHプロキシサーバを実装するにあたり、当然ですがSSHプロトコルの理解が必要でした。そしてプロトコルを理解するために以下のRFCを読みました。
RFC4251:SSH Protocol Architecture
SSHプロトコル全般(暗号アルゴリズムやキーなど)について規定
RFC4252:SSH Authentication Protocol
ユーザ認証(パスワード認証や公開鍵認証)について規定
中でも今回の実装で重要だったのが、SSH Transport Layer Protocol
とSSH Authentication Protocol
です。
sshr自体は認証を通して暗号化されたトランスポート層の確立ができると、あとはL4レベルでのプロキシをするだけの役割になるため、アプリケーションのレイヤーでどのような要求がきているか、などは解釈する必要がありません。(ユーザ名だけは取得する必要はある)
これについて図にすると以下のようなイメージです。
今回の実装で一番苦労したのは間違いなくユーザ認証です。それはなぜかというと、今回のようにクライアントとサーバの間に入ってSSHの仲介をするということは、クライアントやサーバにとっては中間者攻撃のように見えてしまうからです。
ユーザ認証とはサーバがクライアントの正当性を確認するもので、その認証方式にはパスワード認証や公開鍵認証などありますが、パスワード認証の場合、実装は簡単でした。パスワード認証の場合は、sshrサーバは特に意識することなくクライアントから受けた認証時のパケットをそのまま後ろの接続先サーバに対して流し、接続先サーバが返してきたレスポンスをそのままクライアントに返してやる方法で認証が確立できます。
一方、公開鍵認証の場合、クライアントはセッションIDというサーバ間のセッションで固有の乱数に対して、秘密鍵で署名して渡します。これによりお互いがなりすましでなく、接続したい相手であることを確認できます。
しかし、sshrサーバではクライアント・サーバに対して二つのコネクションを有しており、その間に入って意図的に中間者攻撃をしているような状態です。 当然二つのコネクションのセッションIDは異なるため、パスワード認証の時のようにクライアントから受け取ったパケットをそのままサーバ側に流すだけでは、認証ができず、二段階のユーザ認証が必要になってしまいます。(sshrの実装もそうなっています)そうなった場合、authorized_keys
の運用をどうするかなどの、運用・管理の問題が出てきそうなので、色々考え中です…
sshrと同様にユーザ名ベースでSSHの接続先を切り替えられるSSHプロキシサーバとして、sshpiperという素晴らしい既存のOSSがあり、私も実装の参考にさせていただいています。
sshpiperについては以前の記事でも言及しているのですが、今回こちらのOSSを利用しなかった一番大きな理由は、ユーザ名から接続先ホストを特定する処理がパッケージに組み込まれていて自由に拡張できないという点です。そのため、私のsshrでは、前述のPluggable Hooks
の部分で、自由にロジックを組み込めるようにしています。
フック以外の部分でも、より汎用性の高いOSSとして追加で機能を実装していくつもりです!
テストを書く
テストを実装し、CIで自動テストする。
より汎用性を高める
現在の実装だと所々でハードコーティングしてしまっている部分があるため、そこをConfigに寄せるなり、Middleware層に寄せてフックをかけるようにしたりなどすることでsshrの汎用性を高めていきたいと思っています。
もうかれこれ4ヶ月くらいGoを書いているんだけど、最初に文法を体系的に学ばずにいきなり書き始めたので、「A Tour of Go」で復習がてら文法を学びました。
実際にやってみると、理解が曖昧だったり、知らなかったことも出てきたので、そういったものだけピックアップして備忘録的にまとめていきます。
呼び出し元の関数がreturnするとき、 deferへ渡した関数はLIFO(last-in-first-out)の順番で実行される。
package main import "fmt" func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") fmt.Println("return") } //=> return 3 2 1
明示的な型を指定せずに変数を宣言する場合(:=
やvar =
のいずれか)、変数の型は右側の変数から型推論される。
package main import "fmt" func main() { i := 10 var j = 3.14 fmt.Printf("%T\n", i) fmt.Printf("%T\n", j) } //=> int float64
package main import "fmt" func main() { i := 10 p := &i // アドレス演算子「&」を使うと、任意の型からそのポインタ型を生成できる fmt.Println(*p) // 演算子*をポインタ型の変数の前に置くことで、ポインタ型が指し示すデータのデリファレンスができる *p = 20 // ポインタを通してiに値をセットする fmt.Println(i) } //=> 10 20
スライスはどんなデータも格納しておらず、単に元の配列の部分列を指し示している。
スライスの要素を変更すると、その元となる配列の対応する要素が変更される。
package main import "fmt" func main() { names := [3]string{ "John", "Paul", "George", } fmt.Println(names) a := names[0:2] fmt.Println(a) a[0] = "Tsurubee" fmt.Println(names) } //=> [John Paul George] [John Paul] [Tsurubee Paul George]
下は配列リテラル
[3]bool{true, true, false}
そして、これは上記と同様の配列を作成し、それを参照するスライスを作成する
[]bool{true, true, false}
package main import "fmt" func main() { s := []int{1, 2, 3, 4} fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) s = s[:2] fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) } //=> len=4 cap=4 [1 2 3 4] len=2 cap=4 [1 2]
キーに対する要素が存在するかどうかは、2つの目の返値で確認する。
もし、keyがあれば、変数okはtrueとなり、存在しなければ、okはfalseとなる
package main import "fmt" func main() { m := make(map[string]int) m["Answer"] = 10 v, ok := m["Answer"] fmt.Println(v, ok) } //=> 10 true
インターフェースの値が特定の型を保持しているかどうかをテストするために、型アサーションは2つの値(基になる値とアサーションが成功したかどうかを報告するブール値)を返すことができる。
package main import "fmt" func main() { var i interface{} = "hello" s, ok := i.(string) fmt.Println(s, ok) f, ok := i.(float64) fmt.Println(f, ok) } //=> hello true 0 false
型switchは通常のswitch文と似ているが、型switchのcaseは型(値ではない)を指定し、それらの値は指定されたインターフェースの値が保持する値の型と比較される。
package main import "fmt" func do(i interface{}) { switch v := i.(type) { case int: fmt.Println("Int type!") default: fmt.Printf("I don't know about type %T!\n", v) } } func main() { do(10) do("hello") do(true) } //=> Int type! I don't know about type string! I don't know about type bool!
selectは、複数あるcaseのいずれかが準備できるようになるまでブロックし、準備ができた caseを実行する
package main import "fmt" func main() { ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch2 <- 1 select { case <-ch1: fmt.Println("ch1!") case <-ch2: fmt.Println("ch2!") default: fmt.Println("default!") } } //=> ch2!
今回は、Golangのgolang.org/x/crypto/sshパッケージを使って、SSHサーバを構築してみました。
かなりミニマムな実装ですが、リモートからSSH接続して、対話的にコマンドが実行できるところまで実装しました。
package main import ( "golang.org/x/crypto/ssh" "log" "net" "io/ioutil" "fmt" "os/exec" "github.com/kr/pty" "sync" "io" ) func main() { serverConfig := &ssh.ServerConfig{ NoClientAuth: true, } privateKeyBytes, err := ioutil.ReadFile("id_rsa") if err != nil { log.Fatal("Failed to load private key (./id_rsa)") } privateKey, err := ssh.ParsePrivateKey(privateKeyBytes) if err != nil { log.Fatal("Failed to parse private key") } serverConfig.AddHostKey(privateKey) listener, err := net.Listen("tcp", "0.0.0.0:2222") if err != nil { log.Fatalf("Failed to listen on 2222 (%s)", err) } log.Print("Listening on 2222...") for { tcpConn, err := listener.Accept() if err != nil { log.Fatalf("Failed to accept on 2222 (%s)", err) } sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, serverConfig) if err != nil { log.Fatalf("Failed to handshake (%s)", err) } log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) go ssh.DiscardRequests(reqs) go handleChannels(chans) } } func handleChannels(chans <-chan ssh.NewChannel) { for newChannel := range chans { go handleChannel(newChannel) } } func handleChannel(newChannel ssh.NewChannel) { if t := newChannel.ChannelType(); t != "session" { newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("Unknown channel type: %s", t)) return } sshChannel, _, err := newChannel.Accept() if err != nil { log.Fatalf("Could not accept channel (%s)", err) return } bash := exec.Command("bash") close := func() { sshChannel.Close() _, err := bash.Process.Wait() if err != nil { log.Printf("Failed to exit bash (%s)", err) } log.Printf("Session closed") } f, err := pty.Start(bash) if err != nil { log.Printf("Could not start pty (%s)", err) close() return } var once sync.Once go func() { io.Copy(sshChannel, f) once.Do(close) }() go func() { io.Copy(f, sshChannel) once.Do(close) }() }
コードについていくつか補足します。
ローカルPCとSSHサーバのSSHセッションを確立する前に、まずTCPレイヤーのコネクションを確立する必要があります。私の中でTCPコネクションはローカルマシンとリモートマシンのソケット同士の論理的な回線というイメージで、SSHセッションというと、SSHのレイヤーすなわちアプリケーションレイヤーまで広がり、セッションの中でコネクションが管理されているイメージです。
実際のコードでは、net.ListenでTCPをListenし、Acceptして返ってきたTCPコネクションを、ssh.NewServerConnに渡すことで、認証を行った後、SSHのセッションが確立されます。
SSHのセッションが確立されると、あとはSSH経由でコマンドを実行して、その結果が対話的に表示されるようにしたいです。この対話的の部分にはio.Copyを使います。
io.Copyを使うと、io.Readerインターフェースからio.Writerインターフェースにそのままデータを渡すことができます。これを使って、擬似端末(pty)とSSHチャンネル(goのchannelとは違う、SSHのコネクションのようなもの)の間で双方向にCopyしてやると、入出力が対話的にやれます。
SSHサーバのホスト認証のために、公開鍵と秘密鍵のペアを作成し、リポジトリのルートディレクトリの置きます。
ssh-keygen -t rsa -N '' -f ./id_rsa
生成された公開鍵(id_rsa.pub)はローカルPCのknown_hosts
に以下のような感じで登録しておきます。
[localhost]:2222 ssh-rsa AAAAB3・・・・
あとは下のコマンドでDocker上にSSHサーバが起動します。
$ docker-compose up Starting gosshd_gosshd_1 ... done Attaching to gosshd_gosshd_1 gosshd_1 | ==> Installing Dependencies gosshd_1 | go get -u github.com/golang/dep/... gosshd_1 | dep ensure gosshd_1 | go run main.go gosshd_1 | 2018/07/28 12:44:31 Listening on 2222...
ローカルPCからポート2222にSSH接続すると、コンテナの中に繋がります。
$ ssh tsurubee@localhost -p 2222 root@9cd2bdaf33c0:/go/src/gosshd#
今回ユーザ認証の機能はOFFにしているのでユーザ名はなんでも大丈夫です。 あとは普通にサーバにSSHで繋いだときのようにコマンドが実行できます。
root@9cd2bdaf33c0:/go/src/gosshd# pwd /go/src/gosshd root@9cd2bdaf33c0:/go/src/gosshd# ls Gopkg.lock Makefile docker-compose.yml id_rsa.pub vendor Gopkg.toml README.md id_rsa main.go
今回はミニマム実装なので、実用的なSSHサーバになるまでには、認証周りの実装や、 SSHのグローバルリクエストやチャネルリクエストの取り回しも実装しないといけないです。
これからやりたいのはRFCを読んでSSHプロトコルに関する理解を深めるのと、SSHのリバースプロキシを実装してみることです。
GitHub - mattn/go-sshd: DEPRECATED: Please use https://github.com/gliderlabs/ssh instead
最近Golangを書いていると、自分でTCPをListenしたり、Acceptしたりする処理を書くことがよくあるのですが、何をやっているのか全くイメージが沸いてなかったので、「Working with TCP Sockets」を読んで勉強しました。
Working With TCP Socketswww.jstorimer.com
だいたいこの手のソケットプログラミング関連の本は、C言語で解説されていることが多いと思うのですが、本書はサンプルコードが全てRubyなので、非常に読みやすいです。残念ながら英語版しかありません。
以下、私がポイントだと思ったことだけをまとめた読書ノートです。
前半はソケットとは?から始まり、ソケットプログラミング自体の入門的な内容で、そのあたりの理解が曖昧だった私にはとても勉強になりました。
後半は、ソケットプログラミングを使った実用的なアーキテクチャパターンの話でした。Preforkとかイベント駆動とかWebサーバの実装において非常に重要な概念が解説されていましたが、今私が一番知りたいこととは離れていたため、斜め読みしました。
いわゆるクライアントとサーバ
サーバソケットの典型的なライフサイクル
1.create
2.bind
3.listen
4.accept
5.close
bindは、createしたソケットとlistenするポートをbind(結びつける)
他のソケットは同じポートをbindできない。
acceptメソッドでサーバが受付状態になる。
acceptは「ブロッキング」な処理である。新しい接続を受け取るまで、現在のスレッドの処理を停止させる
acceptの返り値としてconnectionがreturnされるが、connectionはただのSocketクラスのインスタンスである(Rubyの場合)
connectionのファイルディスクリプタはサーバソケットのものと異なる。各connectionは新しいSocketオブジェクトとして表され、サーバソケットはそのまま残り、新たなconnectionを受け続けることができる。
すべてをファイルとして扱うUnixの世界ではSocketもファイルだ。
それぞれのTCPコネクションは、ローカルホスト・ローカルポート・リモートホスト・リモートポートの組み合わせで一意に識別される
ソケットは双方向通信(読み/書き)なので、いずれか片方だけを閉じることができる
クライアントの典型的なライフサイクル
1.create
2.bind
3.connect
4.close
クライアントでは、bindを使うことはまれである。bindを使わなかった場合、クライアントソケットはランダムなエフェメラルポートが与えられる。クライアントは外部からの接続を受け付ける必要がないので、ポート番号を明らかにする必要もない。
connectを呼び出すと、リモートのソケットへの接続が始まる
UNIXにおいては、全てをファイルとして取り扱う。read(2)やwrite(2)はシステムコールレベルで共通しているので、ファイル、ソケット、パイプ等で共通して利用できる。
readの呼び出しは、全てのデータが届くまで待ちを発生させる
この問題に対する解決策は2つある
クライアントがEOFを送る
EOFは、文字というよりも、状態を表すイベントである。クライアントからEOFを送る際の最も簡単な方法は、ソケットを閉じることである。
サーバが部分的な読み取りを使用する
読み取るデータの最大長を指定して、ブロックせず、すぐに利用できるデータだけが返す。
writeを呼び出し、エラーなく完了した場合、それは「データの送信に成功し、クライアントが受け取った」ことを意味しない。それは、OSのカーネルによってデータが処理可能な状態になったことが保証されているだけである。
バッファリング
ネットワークを越えたデータ送信は遅いので、できるだけ回数を減らしてパフォーマンスを向上させたい。
一般的には、一度に全てを書き込むと良いパフォーマンスが得られるが、メモリに載らないよう大きなデータやファイルは分割すべきである。
readに読み込みの最大長を渡すと、最大長を超える部分はバッファリングされる
カーネルは、readの読み込みに指定したサイズのメモリを確保するため、そのサイズが大きすぎると、リソースの無駄が多くなる。
一方、サイズが小さすぎると、システムコールの呼び出し回数が増えるためパフォーマンスが劣化する。
どのくらいの読み込みサイズが適切かは、プログラムが取り扱うデータのサイズによって異なる。
ノンブロッキングIOは、コネクションの多重化によって実現される。
ノンブロッキングIOしたあと、読み込み書き込み可能になるまでを知るにはIO.selectを使う
selectは登録したソケットをブロッキング状態で監視し、いずれかがデータ受信するとブロッキング状態を解除する。
コネクションの多重化とは、同時に複数のアクティブなソケットを使用することである。
select(2)は監視対象のコネクション数が増えると、線形的にパフォーマンスが低下する
poll(2)はselect(2)の代替だがそれほどの差はない
(Linux)epoll(2) または (BSD)kqueue(2) は、select(2)およびpoll(2)のモダンな代替である
Preforkパターンでは、接続がくるたびに子プロセスをforkするのではなく、サーバ起動時にプロセスをまとめてforkしておく
ワークフローは以下の通りである
イベント駆動のパターンはNginxを始めとした有名なライブラリで採用されている。
このパターンはシングルスレッド・シングルプロセスで、並列性を実現する
ワークフローは以下の通りである
Working With TCP Sockets (English Edition)
今回は、勉強のために簡単なSSHプロキシサーバを実装してみました。
動作としては、ユーザがプロキシサーバに対してSSH接続した際に、ユーザ名からプロキシ先ホストを動的に決定し、SSH接続します。そして、接続したホストに対してhostname
コマンドを実行し、実行結果をクライアント側が受け取るという感じのものです。
今回の実装はまだ実用的なものではありませんが、最終的にはSSHのユーザ名ベースで接続先のバックエンドを切り替えられるSSHプロキシサーバを構築したいと思っています。
イメージは下のような感じです。
これは一見ProxyCommandなどを使って踏み台サーバ経由でSSH接続をしているだけのように見えるのですが、大きな違いは ユーザ側が接続先サーバのことを全く意識しない ことです。
ProxyCommandの場合、ユーザ側が接続先サーバと踏み台サーバの情報を知っていますが、私がやりたいことは、ユーザはSSHプロキシサーバにSSH接続するだけで、勝手に特定のホストに接続が振り分けられる、というものです。
実際にこのような動作をするSSHプロキシサーバはOSSとして存在します。
このsshpiperは非常によくできていて、ユーザと接続先サーバの紐付け情報をMySQLで管理して、SSH接続をトリガーに接続先ホストの情報をDBからSELECTするような処理も実装されています。
一方で、DBに投げるクエリがコードに直書きされていて、使う側がテーブル構成等を合わせないといけなかったり、若干柔軟性に欠ける実装になっています。
package main import ( "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" "log" "bytes" "errors" "io" "strings" ) func findUpstreamByUsername(username string) (string, error) { if username == "tsurubee" { return "host-tsurubee", nil } else if username == "bob" { return "host-bob", nil } return "", errors.New(username + "'s host is not found!") } func main() { ssh.Handle(func(sess ssh.Session) { username := sess.User() upstream, err := findUpstreamByUsername(sess.User()) if err != nil { log.Fatal(err.Error()) } log.Printf("Connecting for %s by %s\n", upstream, username) config := &gossh.ClientConfig{ User: username, Auth: []gossh.AuthMethod{ gossh.Password("test"), }, HostKeyCallback: gossh.InsecureIgnoreHostKey(), } clientConn, err := gossh.Dial("tcp", upstream + ":22", config) if err != nil { panic(err) } defer clientConn.Close() usess, err := clientConn.NewSession() if err != nil { panic(err) } defer usess.Close() var b bytes.Buffer usess.Stdout = &b if err := usess.Run("hostname"); err != nil { log.Fatal("Failed to run: " + err.Error()) } r := strings.NewReader(b.String()) io.Copy(sess, r) }) log.Println("Starting ssh server on port 2222") ssh.ListenAndServe(":2222", nil) }
検証用にDocker環境を用意したのでまずはそちらを立ち上げます。
docker-compose up
これでローカルホストの2222ポートでプロキシサーバが立ち上がり、プロキシ先ホストとして、host-tsurubee
とhost-bob
が立ち上がります。
すると、下記のように、ユーザ名tsurubeeでSSH接続したときは、プロキシサーバ経由で、tsurubee用ホスト(host-tsurubee)でhostname
コマンドを実行した結果がクライアント側に出力され、bobユーザのときはbob用ホスト(host-bob)のhostname
の結果が返ってきています。
$ ssh tsurubee@127.0.0.1 -p 2222 host-tsurubee $ ssh bob@127.0.0.1 -p 2222 host-bob
このように同じローカルホストの2222番に接続してもユーザ名を元にプロキシされるサーバが振り分けられています。
今回、ユーザ名からプロキシ先ホストを特定するfindUpstreamByUsername
関数に簡単に条件分岐を書きましたが、本来この処理はDBから取得するなり、APIサーバから取得するなりしておきたいところです。
また、SSHサーバの構築には、gliderlabs/sshを使いました。これを使うと簡単に指定のポートでListenするSSHサーバを立てられるので便利です。
gliderlabs/sshでListenしたSSHサーバにリクエストがきて、セッションが確立されると、ハンドラーが呼ばれるため、そのハンドラーはプロキシ先に対するSSHクライアントとして動作するように書いています。
本来は接続先のサーバに対して、ターミナルからサーバにログインした時のように対話的に操作できるようにしたいんだけど、いまいちセッションとかコネクションの取り回しが理解しきれていないため勉強中。
しかし、中途半端でも実装してみると、学びは多かったので、コードファーストで学んでいくスタイルは効率が良さそうに思えた。
もうちょいプロトコルに対する理解や、Golangの抽象化レイヤーへの理解を深めていって、いい感じに書けるようにしていきたい。
2018年6月30日に開催されたPyCon Kyushu 2018 Fukuokaの実行委員をしました。
実行委員は主に以下の4つの役割に別れて運営を行いました。
私はこの中でも会場の担当で、会場のレイアウトを考えたり、当日の設営・撤収の流れを考えたりしました。
設営中! #PyCon9shu pic.twitter.com/fIXQiO3ea9
— つるべー (@tsurubee3) 2018年6月30日
私はもともと初めてのエンジニアとしての仕事がPython案件で、非常にPythonには愛着があります。その当時の同僚とPyFukuokaという小さなコミュニティを運営しており、そのコミュニティ活動が今回実行委員をやるきっかけとなりました。
PyFukuokaというコミュニティを始めたきっかけは自分自身のアウトプットの場が欲しいとかそんなんだった気がしますが、コミュニティをやる中で結果としていろんな方々と繋がりができて、今回もこのような素晴らしい機会もいただけたため、改めてコミュニティ活動の大事さを実感しました。
今回のイベント運営を通して大きな反省点があります。それは私自身「Pythonを多くの方に知ってもらいたい」とか「Pythonをもっと普及したい」といった信念がないままに実行委員として参加したことです。
私はPythonという言語が大好きです。でも、それを広めたいといった気持ちは強くありません。それはおそらく人がどうこう言ってる場合じゃなく、自分が技術・知識をつけることにいっぱいいっぱいだからだと思います。
そういった強い信念がないまま実行委員をした場合、自然な思考回路として、「なるべく時間を割きたくない」という風になってしまいます。
これは本来の実行委員のあるべき姿ではないと思うので、もし次やる機会があれば、強い信念を持って取り組みたい。(持てないならやらない)
まぁ何はともあれ、イベントは大成功!?したのでよかった。
来年は夏に沖縄開催が決定している!!有給取ってバカンスがてらオーディエンスとして参加したい!!