Last active
January 30, 2023 01:58
-
-
Save blacknon/c4348860fe15ef4d7158b26d52f1e39f to your computer and use it in GitHub Desktop.
goでhttp proxy経由でsshでシェルに接続する検証・サンプルコード
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"fmt" | |
"net" | |
"net/http" | |
"net/url" | |
"os" | |
"os/signal" | |
"syscall" | |
"golang.org/x/crypto/ssh" | |
"golang.org/x/crypto/ssh/terminal" | |
"golang.org/x/net/proxy" | |
) | |
// 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) | |
// ----------------------------------- | |
// ↓以降はプロキシの処理とは無関係なコード | |
// ----------------------------------- | |
// Sessionを作成 | |
session, err := client.NewSession() | |
defer session.Close() | |
// キー入力を接続先が認識できる形式に変換する(ここがキモ) | |
fd := int(os.Stdin.Fd()) | |
state, err := terminal.MakeRaw(fd) | |
if err != nil { | |
fmt.Println(err) | |
} | |
defer terminal.Restore(fd, state) | |
// ターミナルサイズの取得 | |
w, h, err := terminal.GetSize(fd) | |
if err != nil { | |
fmt.Println(err) | |
} | |
modes := ssh.TerminalModes{ | |
ssh.ECHO: 1, | |
ssh.TTY_OP_ISPEED: 14400, | |
ssh.TTY_OP_OSPEED: 14400, | |
} | |
// 仮想端末のリクエスト | |
err = session.RequestPty("xterm", h, w, modes) | |
if err != nil { | |
fmt.Println(err) | |
} | |
// sessionの標準入出力の処理 | |
session.Stdout = os.Stdout | |
session.Stderr = os.Stderr | |
session.Stdin = os.Stdin | |
// ssh接続先でShellの起動 | |
err = session.Shell() | |
if err != nil { | |
fmt.Println(err) | |
} | |
// ターミナルサイズの変更検知・処理 | |
signal_chan := make(chan os.Signal, 1) | |
signal.Notify(signal_chan, syscall.SIGWINCH) | |
go func() { | |
for { | |
s := <-signal_chan | |
switch s { | |
case syscall.SIGWINCH: | |
fd := int(os.Stdout.Fd()) | |
w, h, _ = terminal.GetSize(fd) | |
session.WindowChange(h, w) | |
} | |
} | |
}() | |
err = session.Wait() | |
if err != nil { | |
fmt.Println(err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment