Skip to content

Instantly share code, notes, and snippets.

@blacknon
Last active January 30, 2023 01:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blacknon/c4348860fe15ef4d7158b26d52f1e39f to your computer and use it in GitHub Desktop.
Save blacknon/c4348860fe15ef4d7158b26d52f1e39f to your computer and use it in GitHub Desktop.
goでhttp proxy経由でsshでシェルに接続する検証・サンプルコード
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