Golangでhttp Proxy経由でのssh接続を行わせる

ちょっと前にGolangでsshの多段プロキシSocks5プロキシ経由での接続について調べたが、今回はhttpプロキシ経由の場合どうするのかについて調べてみた。
残念ながらhttpプロキシ経由の通信(CONNECTメソッドを利用したプロキシ)で*net.Connを取得できるライブラリは無いようで、そのあたりについては書かないとならないようだ。

幸いにもサンプルコードを書かれている人がいたので、それを使ってhttpプロキシを経由したssh接続のコードを記載する。

Sponsored Links

■ssh_term_http_proxy.go(実行可能なコードはこちら)

// proxyで使用するdirect
type direct struct{}

func (direct) Dial(network, addr string) (net.Conn, error) {
	return net.Dial(network, addr)
}

// http(s)用プロキシのstruct
type httpProxy struct {
	host     string
	haveAuth bool
	username string
	password string
	forward  proxy.Dialer
}

// httpProxyのDial関数(CONNECTメソッドを呼び出している)
func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
	c, err := s.forward.Dial("tcp", s.host)
	if err != nil {
		return nil, err
	}

	reqURL, err := url.Parse("http://" + addr)
	if err != nil {
		c.Close()
		return nil, err
	}
	reqURL.Scheme = ""

	req, err := http.NewRequest("CONNECT", reqURL.String(), nil)
	if err != nil {
		c.Close()
		return nil, err
	}
	req.Close = false
	if s.haveAuth {
		req.SetBasicAuth(s.username, s.password)
	}
	req.Header.Set("User-Agent", "Poweredby Golang")

	err = req.Write(c)
	if err != nil {
		c.Close()
		return nil, err
	}

	resp, err := http.ReadResponse(bufio.NewReader(c), req)
	if err != nil {
		// TODO close resp body ?
		resp.Body.Close()
		c.Close()
		return nil, err
	}
	resp.Body.Close()
	if resp.StatusCode != 200 {
		c.Close()
		err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode)
		return nil, err
	}

	return c, nil
}

// httpプロキシの生成関数
func newHTTPProxy(uri *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
	s := new(httpProxy)
	s.host = uri.Host
	s.forward = forward
	if uri.User != nil {
		s.haveAuth = true
		s.username = uri.User.Username()
		s.password, _ = uri.User.Password()
	}
	return s, nil
}

// @note:
//    proxy1 ... プロキシサーバ
//    target ... プロキシサーバ経由でログインするサーバ
//    認証が必要な場合は、生成するURLを以下の様にする
//    (http://USERNAME:PASSWORD@PROXYIP:PROXYPORT)
//
// 参考:
//    https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b
func main() {
	// 最初にProxyのDialerTypeを登録する
	proxy.RegisterDialerType("http", newHTTPProxy)
	proxy.RegisterDialerType("https", newHTTPProxy)

	// ネットワークへ直接接続するプロキシの定義
	directProxy := direct{}

	// proxy1の情報
	proxy1Host := "proxy1.server.local"
	proxy1Port := "54321"
	// proxy1User := "user"
	// proxy1Pass := "password"

	// targetの情報
	targetHost := "target.server.local"
	targetPort := "22"
	targetUser := "user"
	targetPass := "password"

	// sshClientConfigの作成(target)
	targetSshConfig := &ssh.ClientConfig{
		User:            targetUser,
		Auth:            []ssh.AuthMethod{ssh.Password(targetPass)},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	// urlの生成
	proxy1URL := "http://" + proxy1Host + ":" + proxy1Port
	proxy1URI, _ := url.Parse(proxy1URL)
	proxy1Dialer, err := proxy.FromURL(proxy1URI, directProxy)

	proxy1Conn, err := proxy1Dialer.Dial("tcp", net.JoinHostPort(targetHost, targetPort))
	if err != nil {
		fmt.Println(err)
	}

	// TargetへのsshClientを作成
	pConnect, pChans, pReqs, err := ssh.NewClientConn(proxy1Conn, net.JoinHostPort(targetHost, targetPort), targetSshConfig)
	if err != nil {
		fmt.Println(err)
	}
	client := ssh.NewClient(pConnect, pChans, pReqs)

 

net/proxyで使うためのDialerを作って、それを経由するようにしてる感じ。
今どきあまりないだろうけど、環境によってはhttpプロキシを経由しないと外部サーバに繋げないようなところもあるので、そういうときに役に立ちそうだ。
(そんな環境でGolang使われるか?という疑問はあるけど)

 


Written by blacknon

インフラエンジニア(…のつもり)。 仕事で使うならクライアントはWindowsよりはUNIXの方が好き。 大体いつも眠い。

Leave a Comment

メールアドレスが公開されることはありません。

*