diff --git a/dialer.go b/dialer.go new file mode 100644 index 0000000..aae6e6d --- /dev/null +++ b/dialer.go @@ -0,0 +1,123 @@ +package obfssh + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "sync" + "time" + + log "github.com/fangdingjun/go-log/v5" + "github.com/gorilla/websocket" + "golang.org/x/crypto/ssh" +) + +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + Proxy func() (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + // If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake + // is done there and TLSClientConfig is ignored. + TLSClientConfig *tls.Config + + NetConf *Conf +} + +func (d *Dialer) Dial(addr string, conf *ssh.ClientConfig) (*Client, error) { + if d.NetConf.Timeout == 0 { + d.NetConf.Timeout = 15 * time.Second + } + if d.NetConf.KeepAliveInterval == 0 { + d.NetConf.KeepAliveInterval = 10 + } + if d.NetConf.KeepAliveMax == 0 { + d.NetConf.KeepAliveMax = 3 + } + var dialFunc func(network, addr string) (net.Conn, error) + if d.NetDial == nil { + dialFunc = dialer.Dial + } + + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + + if d.Proxy != nil { + dialFunc = func(network, addr string) (net.Conn, error) { + var conn net.Conn + var err error + u1, _ := d.Proxy() + if u1 == nil { + return dialer.Dial(network, addr) + } + log.Debugf("connect to proxy %s", u1.String()) + switch u1.Scheme { + case "http": + conn, err = dialHTTPProxy(addr, u1) + case "https": + conn, err = dialHTTPSProxy(addr, u1) + case "socks5": + conn, err = dialSocks5Proxy(addr, u1) + default: + return nil, fmt.Errorf("unknown proxy scheme %s", u1.Scheme) + } + if err != nil { + log.Errorf("connect to proxy error %s", err) + } + return conn, err + } + } + + switch u.Scheme { + case "": + conn, err := dialFunc("tcp", u.Host) + if err != nil { + return nil, err + } + return NewClient(&TimedOutConn{Conn: conn, Timeout: d.NetConf.Timeout}, conf, u.Host, d.NetConf) + case "tls": + conn, err := dialFunc("tcp", u.Host) + if err != nil { + return nil, err + } + conn = tls.Client(&TimedOutConn{Conn: conn, Timeout: d.NetConf.Timeout}, d.TLSClientConfig) + return NewClient(conn, conf, u.Host, d.NetConf) + case "ws": + fallthrough + case "wss": + _addr := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path) + _dailer := websocket.Dialer{ + NetDial: func(network, addr string) (net.Conn, error) { + c, err := dialFunc(network, addr) + return &TimedOutConn{Conn: c, Timeout: d.NetConf.Timeout}, err + }, + TLSClientConfig: d.TLSClientConfig, + } + wsconn, res, err := _dailer.Dial(_addr, nil) + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusSwitchingProtocols { + return nil, fmt.Errorf("websocket connect failed, http code %d", res.StatusCode) + } + _conn := &wsConn{Conn: wsconn, buf: new(bytes.Buffer), mu: new(sync.Mutex), ch: make(chan struct{})} + go _conn.readLoop() + return NewClient(_conn, conf, u.Host, d.NetConf) + default: + return nil, fmt.Errorf("unknow scheme %s", u.Scheme) + } +} + +func (d *Dialer) DialContext(ctx context.Context, addr string, conf *ssh.ClientConfig) (*Client, error) { + return nil, nil +} diff --git a/obfssh/proxy.go b/obfssh/proxy.go index eb14645..629ddf1 100644 --- a/obfssh/proxy.go +++ b/obfssh/proxy.go @@ -1,61 +1,14 @@ package main import ( - "bufio" - "crypto/tls" - "fmt" - "io" "net" - "net/textproto" "net/url" "os" "strconv" - "strings" - "time" "github.com/fangdingjun/go-log/v5" - socks "github.com/fangdingjun/socks-go" ) -type httpProxyConn struct { - c net.Conn - r io.Reader -} - -func (hc *httpProxyConn) Read(b []byte) (int, error) { - return hc.r.Read(b) -} - -func (hc *httpProxyConn) Write(b []byte) (int, error) { - return hc.c.Write(b) -} - -func (hc *httpProxyConn) Close() error { - return hc.c.Close() -} -func (hc *httpProxyConn) LocalAddr() net.Addr { - return hc.c.LocalAddr() -} - -func (hc *httpProxyConn) RemoteAddr() net.Addr { - return hc.c.RemoteAddr() -} - -func (hc *httpProxyConn) SetDeadline(t time.Time) error { - return hc.c.SetDeadline(t) -} - -func (hc *httpProxyConn) SetReadDeadline(t time.Time) error { - return hc.c.SetReadDeadline(t) -} - -func (hc *httpProxyConn) SetWriteDeadline(t time.Time) error { - return hc.c.SetWriteDeadline(t) -} - -// validate the interface implements -var _ net.Conn = &httpProxyConn{} - func updateProxyFromEnv(cfg *config) { if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 { log.Debugf("proxy already specified by config, not parse environment proxy") @@ -108,99 +61,3 @@ func updateProxyFromEnv(cfg *config) { } } } - -func httpProxyHandshake(c net.Conn, host string, port int) (net.Conn, error) { - fmt.Fprintf(c, "CONNECT %s:%d HTTP/1.1\r\n", host, port) - fmt.Fprintf(c, "Host: %s:%d\r\n", host, port) - fmt.Fprintf(c, "User-Agent: go/1.7\r\n") - fmt.Fprintf(c, "\r\n") - - r := bufio.NewReader(c) - tp := textproto.NewReader(r) - - // read status line - statusLine, err := tp.ReadLine() - if err != nil { - return nil, err - } - - if statusLine[0:4] != "HTTP" { - return nil, fmt.Errorf("not http reply") - } - - status := strings.Fields(statusLine)[1] - - statusCode, err := strconv.Atoi(status) - if err != nil { - return nil, err - } - - if statusCode != 200 { - return nil, fmt.Errorf("http status error %d", statusCode) - } - - // read header - if _, err = tp.ReadMIMEHeader(); err != nil { - return nil, err - } - - return &httpProxyConn{c: c, r: r}, nil -} - -func dialHTTPProxy(host string, port int, p proxy) (net.Conn, error) { - c, err := dialer.Dial("tcp", net.JoinHostPort(p.Host, fmt.Sprintf("%d", p.Port))) - if err != nil { - return nil, err - } - - c1, err := httpProxyHandshake(c, host, port) - if err != nil { - c.Close() - return nil, err - } - return c1, nil -} - -func dialHTTPSProxy(host string, port int, p proxy) (net.Conn, error) { - hostname := p.Host - if p.SNI != "" { - hostname = p.SNI - } - - tlsconfig := &tls.Config{ - ServerName: hostname, - InsecureSkipVerify: p.Insecure, - } - - c, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(p.Host, fmt.Sprintf("%d", p.Port)), tlsconfig) - if err != nil { - return nil, err - } - - if err := c.Handshake(); err != nil { - c.Close() - return nil, err - } - - c1, err := httpProxyHandshake(c, host, port) - if err != nil { - c.Close() - return nil, err - } - return c1, nil -} - -func dialSocks5Proxy(host string, port int, p proxy) (net.Conn, error) { - c, err := dialer.Dial("tcp", net.JoinHostPort(p.Host, fmt.Sprintf("%d", p.Port))) - if err != nil { - return nil, err - } - - c1 := &socks.Client{Conn: c} - c2, err := c1.Dial("tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port))) - if err != nil { - c1.Close() - return nil, err - } - return c2, err -} diff --git a/obfssh/ssh.go b/obfssh/ssh.go index a42c79c..b0b1214 100644 --- a/obfssh/ssh.go +++ b/obfssh/ssh.go @@ -34,7 +34,6 @@ func main() { flag.StringVar(&cfg.Password, "pw", "", "ssh password") flag.IntVar(&cfg.Port, "p", 22, "remote port") flag.StringVar(&cfg.PrivateKey, "i", "", "private key file") - flag.BoolVar(&cfg.TLS, "tls", false, "use tls or not") flag.BoolVar(&cfg.TLSInsecure, "tls-insecure", false, "insecure tls connnection") flag.Var(&cfg.LocalForwards, "L", "forward local port to remote, format [local_host:]local_port:remote_host:remote_port") flag.Var(&cfg.RemoteForwards, "R", "forward remote port to local, format [remote_host:]remote_port:local_host:local_port") @@ -148,10 +147,13 @@ func main() { cmd = strings.Join(args, " ") } + var serverName string if strings.Contains(host, "@") { - ss := strings.SplitN(host, "@", 2) - cfg.Username = ss[0] - host = ss[1] + u, _ := url.Parse(host) + cfg.Username = u.User.Username() + u.User = nil + host = u.String() + serverName, _, _ = net.SplitHostPort(u.Host) } // process user specified private key @@ -193,74 +195,33 @@ func main() { // parse environment proxy updateProxyFromEnv(&cfg) - var c net.Conn - var rhost string - - if strings.HasPrefix(host, "ws://") || strings.HasPrefix(host, "wss://") { - c, err = obfssh.NewWSConn(host) - u, _ := url.Parse(host) - rhost = u.Host - } else { - rhost = net.JoinHostPort(host, fmt.Sprintf("%d", cfg.Port)) - if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 { - switch cfg.Proxy.Scheme { - case "http": - log.Debugf("use http proxy %s:%d to connect to server", - cfg.Proxy.Host, cfg.Proxy.Port) - c, err = dialHTTPProxy(host, cfg.Port, cfg.Proxy) - case "https": - log.Debugf("use https proxy %s:%d to connect to server", - cfg.Proxy.Host, cfg.Proxy.Port) - c, err = dialHTTPSProxy(host, cfg.Port, cfg.Proxy) - case "socks5": - log.Debugf("use socks proxy %s:%d to connect to server", - cfg.Proxy.Host, cfg.Proxy.Port) - c, err = dialSocks5Proxy(host, cfg.Port, cfg.Proxy) - default: - err = fmt.Errorf("unsupported scheme: %s", cfg.Proxy.Scheme) - } - } else { - log.Debugf("dail to %s", rhost) - c, err = dialer.Dial("tcp", rhost) - } - } - - if err != nil { - log.Fatal(err) - } - - log.Debugf("dail success") - timeout := time.Duration(cfg.KeepaliveInterval*2) * time.Second - var _conn net.Conn = &obfssh.TimedOutConn{Conn: c, Timeout: timeout} - - if cfg.TLS { - log.Debugf("begin tls handshake") - _conn = tls.Client(_conn, &tls.Config{ - ServerName: host, - InsecureSkipVerify: cfg.TLSInsecure, - }) - if err := _conn.(*tls.Conn).Handshake(); err != nil { - log.Fatal(err) - } - log.Debugf("tls handshake done") - } - conf := &obfssh.Conf{ Timeout: timeout, KeepAliveInterval: time.Duration(cfg.KeepaliveInterval) * time.Second, KeepAliveMax: cfg.KeepaliveMax, } - log.Debugf("ssh negotation") - client, err := obfssh.NewClient(_conn, config, rhost, conf) + dialer := &obfssh.Dialer{ + Proxy: func() (*url.URL, error) { + if cfg.Proxy.Scheme != "" && cfg.Proxy.Host != "" && cfg.Proxy.Port != 0 { + return &url.URL{ + Scheme: cfg.Proxy.Scheme, + Host: fmt.Sprintf("%s:%d", cfg.Proxy.Host, cfg.Proxy.Port), + }, nil + } + return nil, nil + }, + TLSClientConfig: &tls.Config{ServerName: serverName, InsecureSkipVerify: cfg.TLSInsecure}, + NetConf: conf, + } + + client, err := dialer.Dial(host, config) if err != nil { log.Fatal(err) } - log.Debugf("ssh negotation success") - if agentClient != nil { client.SetAuthAgent(agentClient) } @@ -394,13 +355,19 @@ func passwordAuth() (string, error) { func usage() { usageStr := `Usage: obfssh -N -d -D [bind_address:]port -f configfile - -tls -tls-insecure -log_file /path/to/file + -tls-insecure -log_file /path/to/file -log_count 10 -log_size 10 -log_level INFO -i identity_file -L [bind_address:]port:host:hostport -l login_name -pw password -p port -http [bind_addr:]port - -R [bind_address:]port:host:hostport [user@]hostname [command] + -R [bind_address:]port:host:hostport host [command] + + host can be multiple forms, example: + user@example.com + tls://hostname + ws://hostname/path + wss://hostname/path Options: -d verbose mode @@ -461,11 +428,8 @@ Options: encrypt ssh connection similar like -D, but input is http, not socks5 - -tls - connect to server via TLS - -tls-insecure - do not verify server's tls ceritificate + do not verify server's tls ceritificate when use tls:// or wss:// -log_file log file, default stdout diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..21f596a --- /dev/null +++ b/proxy.go @@ -0,0 +1,151 @@ +package obfssh + +import ( + "bufio" + "crypto/tls" + "fmt" + "io" + "net" + "net/textproto" + "net/url" + "strconv" + "strings" + "time" + + log "github.com/fangdingjun/go-log/v5" + socks "github.com/fangdingjun/socks-go" +) + +type httpProxyConn struct { + c net.Conn + r io.Reader +} + +func (hc *httpProxyConn) Read(b []byte) (int, error) { + return hc.r.Read(b) +} + +func (hc *httpProxyConn) Write(b []byte) (int, error) { + return hc.c.Write(b) +} + +func (hc *httpProxyConn) Close() error { + return hc.c.Close() +} +func (hc *httpProxyConn) LocalAddr() net.Addr { + return hc.c.LocalAddr() +} + +func (hc *httpProxyConn) RemoteAddr() net.Addr { + return hc.c.RemoteAddr() +} + +func (hc *httpProxyConn) SetDeadline(t time.Time) error { + return hc.c.SetDeadline(t) +} + +func (hc *httpProxyConn) SetReadDeadline(t time.Time) error { + return hc.c.SetReadDeadline(t) +} + +func (hc *httpProxyConn) SetWriteDeadline(t time.Time) error { + return hc.c.SetWriteDeadline(t) +} + +// validate the interface implements +var _ net.Conn = &httpProxyConn{} + +func httpProxyHandshake(c net.Conn, addr string) (net.Conn, error) { + log.Debugf("http handshake with %s", addr) + fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", addr) + fmt.Fprintf(c, "Host: %s\r\n", addr) + fmt.Fprintf(c, "User-Agent: go/1.7\r\n") + fmt.Fprintf(c, "\r\n") + + r := bufio.NewReader(c) + tp := textproto.NewReader(r) + + // read status line + statusLine, err := tp.ReadLine() + if err != nil { + return nil, err + } + + if statusLine[0:4] != "HTTP" { + return nil, fmt.Errorf("not http reply") + } + + status := strings.Fields(statusLine)[1] + + statusCode, err := strconv.Atoi(status) + if err != nil { + return nil, err + } + + if statusCode != 200 { + return nil, fmt.Errorf("http status error %d", statusCode) + } + + // read header + if _, err = tp.ReadMIMEHeader(); err != nil { + return nil, err + } + + return &httpProxyConn{c: c, r: r}, nil +} + +func dialHTTPProxy(addr string, p *url.URL) (net.Conn, error) { + log.Debugf("dial to %s", p.Host) + c, err := dialer.Dial("tcp", p.Host) + if err != nil { + return nil, err + } + + c1, err := httpProxyHandshake(c, addr) + if err != nil { + c.Close() + return nil, err + } + return c1, nil +} + +func dialHTTPSProxy(addr string, p *url.URL) (net.Conn, error) { + hostname := p.Host + + tlsconfig := &tls.Config{ + ServerName: hostname, + InsecureSkipVerify: true, + } + + c, err := tls.DialWithDialer(dialer, "tcp", p.Host, tlsconfig) + if err != nil { + return nil, err + } + + if err := c.Handshake(); err != nil { + c.Close() + return nil, err + } + + c1, err := httpProxyHandshake(c, addr) + if err != nil { + c.Close() + return nil, err + } + return c1, nil +} + +func dialSocks5Proxy(addr string, p *url.URL) (net.Conn, error) { + c, err := dialer.Dial("tcp", p.Host) + if err != nil { + return nil, err + } + + c1 := &socks.Client{Conn: c} + c2, err := c1.Dial("tcp", addr) + if err != nil { + c1.Close() + return nil, err + } + return c2, err +} diff --git a/ws.go b/ws.go index 07912fb..1b0d71a 100644 --- a/ws.go +++ b/ws.go @@ -3,14 +3,12 @@ package obfssh import ( "bytes" "errors" - "fmt" "io" "net" - "net/http" "sync" "time" - "github.com/fangdingjun/go-log/v5" + log "github.com/fangdingjun/go-log/v5" "github.com/gorilla/websocket" ) @@ -23,29 +21,6 @@ type wsConn struct { var _ net.Conn = &wsConn{} -// NewWSConn dial to websocket server and return net.Conn -func NewWSConn(p string) (net.Conn, error) { - conn, resp, err := websocket.DefaultDialer.Dial(p, nil) - if err != nil { - return nil, err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, fmt.Errorf("http status %d", resp.StatusCode) - } - - c := &wsConn{Conn: conn, - buf: bytes.NewBuffer(nil), - mu: new(sync.Mutex), - ch: make(chan struct{}), - } - - go c.readLoop() - - return c, nil -} - func (wc *wsConn) readLoop() { for { _, data, err := wc.ReadMessage()