From 9b52e165f778b200b3abec21547e4fd4a19fdf7e Mon Sep 17 00:00:00 2001 From: Jaime Pillora Date: Sat, 14 Mar 2015 03:40:18 +1100 Subject: [PATCH] working, 4x slower than yamux :( --- chisel-forward/client/client.go | 114 +++++++++++++------------------- chisel-forward/client/proxy.go | 46 +++++++------ chisel-forward/main.go | 1 + chiseld/main.go | 1 + chiseld/server/endpoint.go | 47 ------------- chiseld/server/server.go | 79 +++++++++++----------- chiseld/server/websocket.go | 95 -------------------------- config.go | 24 ++----- pipe.go | 3 +- ssh.go | 81 +++++++++++++++++++++++ test/chisel_test.go | 11 +-- 11 files changed, 207 insertions(+), 295 deletions(-) delete mode 100644 chiseld/server/endpoint.go delete mode 100644 chiseld/server/websocket.go create mode 100644 ssh.go diff --git a/chisel-forward/client/client.go b/chisel-forward/client/client.go index 3ba2b59..67e0836 100644 --- a/chisel-forward/client/client.go +++ b/chisel-forward/client/client.go @@ -1,28 +1,29 @@ package chiselclient import ( - "errors" "fmt" + "io" "net" "net/url" "regexp" "strings" "time" - "github.com/hashicorp/yamux" "github.com/jpillora/backoff" "github.com/jpillora/chisel" + "golang.org/x/crypto/ssh" "golang.org/x/net/websocket" ) type Client struct { *chisel.Logger - config *chisel.Config - encconfig string - proxies []*Proxy - session *yamux.Session - running bool - runningc chan error + config *chisel.Config + sshConfig *ssh.ClientConfig + proxies []*Proxy + sshConn ssh.Conn + fingerprint string + running bool + runningc chan error } func NewClient(auth, server string, remotes ...string) (*Client, error) { @@ -63,18 +64,24 @@ func NewClient(auth, server string, remotes ...string) (*Client, error) { config.Remotes = append(config.Remotes, r) } - encconfig, err := chisel.EncodeConfig(config) - if err != nil { - return nil, fmt.Errorf("Failed to encode config: %s", err) + c := &Client{ + Logger: chisel.NewLogger("client"), + config: config, + running: true, + runningc: make(chan error, 1), } - return &Client{ - Logger: chisel.NewLogger("client"), - config: config, - encconfig: encconfig, - running: true, - runningc: make(chan error, 1), - }, nil + c.sshConfig = &ssh.ClientConfig{ + ClientVersion: "chisel-client-" + chisel.ProtocolVersion, + User: "jpillora", + Auth: []ssh.AuthMethod{ssh.Password("t0ps3cr3t")}, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + c.fingerprint = chisel.FingerprintKey(key) + return nil + }, + } + + return c, nil } //Start then Wait @@ -91,81 +98,54 @@ func (c *Client) Start() { func (c *Client) start() { c.Infof("Connecting to %s\n", c.config.Server) - //proxies all use this function - openStream := func() (net.Conn, error) { - if c.session == nil || c.session.IsClosed() { - return nil, c.Errorf("no session available") - } - stream, err := c.session.Open() - if err != nil { - return nil, err - } - return stream, nil - } - //prepare proxies for id, r := range c.config.Remotes { - proxy := NewProxy(c, id, r, openStream) + proxy := NewProxy(c, id, r) go proxy.start() c.proxies = append(c.proxies, proxy) } + //connection loop! var connerr error b := &backoff.Backoff{Max: 5 * time.Minute} - - //connection loop! for { if !c.running { break } if connerr != nil { - connerr = nil d := b.Duration() c.Infof("Retrying in %s...\n", d) + connerr = nil time.Sleep(d) } - ws, err := websocket.Dial(c.config.Server, c.encconfig, "http://localhost/") + ws, err := websocket.Dial(c.config.Server, chisel.ConfigPrefix, "localhost:80") if err != nil { connerr = err continue } - buff := make([]byte, 0xff) - n, _ := ws.Read(buff) - if msg := string(buff[:n]); msg != "handshake-success" { - //no point in retrying - c.runningc <- errors.New(msg) - ws.Close() - break - } - - // Setup client side of yamux - c.session, err = yamux.Client(ws, nil) + sshConn, chans, reqs, err := ssh.NewClientConn(ws, "", c.sshConfig) if err != nil { connerr = err + c.Infof("Handshake failed: %s", err) continue } + c.Infof("Connected (%s)", c.fingerprint) + //connected b.Reset() - - //signal is connected - connected := make(chan bool) - c.Infof("Connected\n") - - //poll websocket state - go func() { - for { - if c.session.IsClosed() { - connerr = c.Errorf("disconnected") - c.Infof("Disconnected\n") - close(connected) - break - } - time.Sleep(100 * time.Millisecond) - } - }() - //block! - <-connected + c.sshConn = sshConn + go ssh.DiscardRequests(reqs) + go chisel.RejectStreams(chans) + err = sshConn.Wait() + //disconnected + c.sshConn = nil + if err != nil && err != io.EOF { + connerr = err + c.Infof("Disconnection error: %s", err) + continue + } + c.Infof("Disconnected\n") } close(c.runningc) } @@ -178,8 +158,8 @@ func (c *Client) Wait() error { //Close manual stops the client func (c *Client) Close() error { c.running = false - if c.session == nil { + if c.sshConn == nil { return nil } - return c.session.Close() + return c.sshConn.Close() } diff --git a/chisel-forward/client/proxy.go b/chisel-forward/client/proxy.go index 01c5047..f189cd0 100644 --- a/chisel-forward/client/proxy.go +++ b/chisel-forward/client/proxy.go @@ -1,7 +1,7 @@ package chiselclient import ( - "encoding/binary" + "io" "net" "github.com/jpillora/chisel" @@ -9,18 +9,19 @@ import ( type Proxy struct { *chisel.Logger - id int - count int - remote *chisel.Remote - openStream func() (net.Conn, error) + client *Client + id int + count int + + remote *chisel.Remote } -func NewProxy(c *Client, id int, remote *chisel.Remote, openStream func() (net.Conn, error)) *Proxy { +func NewProxy(c *Client, id int, remote *chisel.Remote) *Proxy { return &Proxy{ - Logger: c.Logger.Fork("%s:%s#%d", remote.RemoteHost, remote.RemotePort, id+1), - id: id, - remote: remote, - openStream: openStream, + Logger: c.Logger.Fork("%s:%s#%d", remote.RemoteHost, remote.RemotePort, id+1), + client: c, + id: id, + remote: remote, } } @@ -43,27 +44,28 @@ func (p *Proxy) start() { } } -func (p *Proxy) accept(src net.Conn) { +func (p *Proxy) accept(src io.ReadWriteCloser) { p.count++ cid := p.count - clog := p.Fork("conn#%d", cid) + l := p.Fork("conn#%d", cid) - clog.Debugf("Open") + l.Debugf("Open") - dst, err := p.openStream() - if err != nil { - clog.Debugf("Stream error: %s", err) + if p.client.sshConn == nil { + l.Debugf("No server connection") src.Close() return } - //write endpoint id - b := make([]byte, 2) - binary.BigEndian.PutUint16(b, uint16(p.id)) - dst.Write(b) + remoteAddr := p.remote.RemoteHost + ":" + p.remote.RemotePort + dst, err := chisel.OpenStream(p.client.sshConn, remoteAddr) + if err != nil { + l.Debugf("Stream error: %s", err) + src.Close() + return + } //then pipe s, r := chisel.Pipe(src, dst) - - clog.Debugf("Close (sent %d received %d)", s, r) + l.Debugf("Close (sent %d received %d)", s, r) } diff --git a/chisel-forward/main.go b/chisel-forward/main.go index 866e916..8d1ca53 100644 --- a/chisel-forward/main.go +++ b/chisel-forward/main.go @@ -44,6 +44,7 @@ var help = ` Read more: https://github.com/jpillora/chisel + ` func main() { diff --git a/chiseld/main.go b/chiseld/main.go index f8f809d..cf9cf77 100644 --- a/chiseld/main.go +++ b/chiseld/main.go @@ -36,6 +36,7 @@ var help = ` Read more: https://github.com/jpillora/chisel + ` func main() { diff --git a/chiseld/server/endpoint.go b/chiseld/server/endpoint.go deleted file mode 100644 index 92cc7e4..0000000 --- a/chiseld/server/endpoint.go +++ /dev/null @@ -1,47 +0,0 @@ -package chiselserver - -import ( - "net" - - "github.com/jpillora/chisel" -) - -type endpoint struct { - *chisel.Logger - id int - count int - addr string - sessions chan net.Conn -} - -func newEndpoint(w *webSocket, id int, addr string) *endpoint { - return &endpoint{ - Logger: w.Logger.Fork("%s#%d", addr, id+1), - id: id, - addr: addr, - sessions: make(chan net.Conn), - } -} - -func (e *endpoint) start() { - e.Debugf("Enabled") - //service incoming streams - for stream := range e.sessions { - go e.pipe(stream) - } -} - -func (e *endpoint) pipe(src net.Conn) { - dst, err := net.Dial("tcp", e.addr) - if err != nil { - e.Debugf("%s", err) - src.Close() - return - } - - e.count++ - eid := e.count - e.Debugf("stream#%d: Open", eid) - s, r := chisel.Pipe(src, dst) - e.Debugf("stream#%d: Close (sent %d received %d)", eid, s, r) -} diff --git a/chiseld/server/server.go b/chiseld/server/server.go index a720d92..2c356f9 100644 --- a/chiseld/server/server.go +++ b/chiseld/server/server.go @@ -1,25 +1,30 @@ package chiselserver import ( + "log" "net/http" "net/http/httputil" "net/url" "strings" "github.com/jpillora/chisel" + "golang.org/x/crypto/ssh" "golang.org/x/net/websocket" ) type Server struct { *chisel.Logger - auth string - wsCount int - wsServer websocket.Server - httpServer *chisel.HTTPServer - proxy *httputil.ReverseProxy + auth string + fingerprint string + wsCount int + wsServer websocket.Server + httpServer *chisel.HTTPServer + proxy *httputil.ReverseProxy + sshConfig *ssh.ServerConfig } func NewServer(auth, proxy string) (*Server, error) { + s := &Server{ Logger: chisel.NewLogger("server"), auth: auth, @@ -28,6 +33,23 @@ func NewServer(auth, proxy string) (*Server, error) { } s.wsServer.Handler = websocket.Handler(s.handleWS) + key, _ := chisel.GenerateKey() + + config := &ssh.ServerConfig{ + ServerVersion: "chisel-server-" + chisel.ProtocolVersion, + NoClientAuth: true, + } + private, err := ssh.ParsePrivateKey(key) + if err != nil { + log.Fatal("Failed to parse key") + } + + s.fingerprint = chisel.FingerprintKey(private.PublicKey()) + + config.AddHostKey(private) + + s.sshConfig = config + if proxy != "" { u, err := url.Parse(proxy) if err != nil { @@ -62,6 +84,8 @@ func (s *Server) Start(host, port string) error { if s.proxy != nil { s.Infof("Default proxy enabled") } + s.Infof("Fingerprint %s", s.fingerprint) + s.Infof("Listening on %s...", port) return s.httpServer.GoListenAndServe(":"+port, http.HandlerFunc(s.handleHTTP)) @@ -92,44 +116,23 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) } -func (s *Server) handshake(h string) (*chisel.Config, error) { - if h == "" { - return nil, s.Errorf("Handshake missing") - } - c, err := chisel.DecodeConfig(h) - if err != nil { - return nil, err - } - if chisel.ProtocolVersion != c.Version { - return nil, s.Errorf("Version mismatch") - } - if s.auth != "" { - if s.auth != c.Auth { - return nil, s.Errorf("Authentication failed") - } - } - return c, nil -} - func (s *Server) handleWS(ws *websocket.Conn) { - - ps := ws.Config().Protocol - p := "" - if len(ps) == 1 { - p = ps[0] - } - - config, err := s.handshake(p) + // Before use, a handshake must be performed on the incoming net.Conn. + sshConn, chans, reqs, err := ssh.NewServerConn(ws, s.sshConfig) if err != nil { - msg := err.Error() - ws.Write([]byte(msg)) - ws.Close() + log.Printf("Failed to handshake (%s)", err) return } - // s.Infof("success %+v\n", config) - ws.Write([]byte("handshake-success")) s.wsCount++ + id := s.wsCount + l := s.Fork("conn#%d", id) - newWebSocket(s, config, ws).handle() + l.Infof("Open") + + go ssh.DiscardRequests(reqs) + go chisel.ConnectStreams(l, chans) + + sshConn.Wait() + l.Infof("Close") } diff --git a/chiseld/server/websocket.go b/chiseld/server/websocket.go deleted file mode 100644 index b04483b..0000000 --- a/chiseld/server/websocket.go +++ /dev/null @@ -1,95 +0,0 @@ -package chiselserver - -import ( - "encoding/binary" - "net" - - "github.com/hashicorp/yamux" - "github.com/jpillora/chisel" -) - -type webSocket struct { - *chisel.Logger - id int - config *chisel.Config - conn net.Conn -} - -func newWebSocket(s *Server, config *chisel.Config, conn net.Conn) *webSocket { - id := s.wsCount - return &webSocket{ - Logger: s.Logger.Fork("conn#%d", id), - id: id, - config: config, - conn: conn, - } -} - -func (w *webSocket) handle() { - - w.Debugf("Open") - - // queue teardown - defer w.teardown() - - // Setup server side of yamux - session, err := yamux.Server(w.conn, nil) - if err != nil { - w.Debugf("Yamux server: %s", err) - return - } - - endpoints := make([]*endpoint, len(w.config.Remotes)) - - // Create an endpoint for each required - for id, r := range w.config.Remotes { - addr := r.RemoteHost + ":" + r.RemotePort - e := newEndpoint(w, id, addr) - go e.start() - endpoints[id] = e - } - - for { - stream, err := session.Accept() - if err != nil { - if session.IsClosed() { - break - } - w.Debugf("Session accept: %s", err) - continue - } - go w.handleStream(stream, endpoints) - } -} - -func (w *webSocket) handleStream(stream net.Conn, endpoints []*endpoint) { - // extract endpoint id - b := make([]byte, 2) - n, err := stream.Read(b) - if err != nil { - stream.Close() - w.Debugf("Stream initial read: %s", err) - return - } - if n != 2 { - stream.Close() - w.Debugf("Should read 2 bytes...") - return - } - id := binary.BigEndian.Uint16(b) - - if int(id) >= len(endpoints) { - stream.Close() - w.Debugf("Invalid endpoint index: %d [total endpoints %d]", id, len(endpoints)) - return - } - - // pass this stream to the - // desired endpoint - e := endpoints[id] - e.sessions <- stream -} - -func (w *webSocket) teardown() { - w.Debugf("Closed") -} diff --git a/config.go b/config.go index b3a96f7..cc4c13d 100644 --- a/config.go +++ b/config.go @@ -1,10 +1,8 @@ package chisel import ( - "encoding/base64" "encoding/json" "fmt" - "strings" ) type Config struct { @@ -14,29 +12,17 @@ type Config struct { Remotes []*Remote } -const ConfigPrefix = "chisel-" +const ConfigPrefix = "chisel" -func DecodeConfig(s string) (*Config, error) { - if !strings.HasPrefix(s, ConfigPrefix) { - return nil, fmt.Errorf("Invalid chisel config") - } - s = strings.TrimPrefix(s, ConfigPrefix) - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, fmt.Errorf("Invalid base64 config") - } +func DecodeConfig(b []byte) (*Config, error) { c := &Config{} - err = json.Unmarshal(b, c) + err := json.Unmarshal(b, c) if err != nil { return nil, fmt.Errorf("Invalid JSON config") } return c, nil } -func EncodeConfig(c *Config) (string, error) { - b, err := json.Marshal(c) - if err != nil { - return "", err - } - return ConfigPrefix + base64.StdEncoding.EncodeToString(b), nil +func EncodeConfig(c *Config) ([]byte, error) { + return json.Marshal(c) } diff --git a/pipe.go b/pipe.go index 2bd0f7d..42d9f7e 100644 --- a/pipe.go +++ b/pipe.go @@ -2,11 +2,10 @@ package chisel import ( "io" - "net" "sync" ) -func Pipe(src net.Conn, dst net.Conn) (int64, int64) { +func Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) { var sent, received int64 var c = make(chan bool) diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..7c901f9 --- /dev/null +++ b/ssh.go @@ -0,0 +1,81 @@ +package chisel + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/md5" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "io" + "net" + + "golang.org/x/crypto/ssh" +) + +func GenerateKey() ([]byte, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + b, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, fmt.Errorf("Unable to marshal ECDSA private key: %v", err) + } + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil +} + +func FingerprintKey(k ssh.PublicKey) string { + f := md5.Sum(k.Marshal()) + return hex.EncodeToString(f[:]) +} + +func OpenStream(conn ssh.Conn, remote string) (io.ReadWriteCloser, error) { + stream, reqs, err := conn.OpenChannel("chisel", []byte(remote)) + if err != nil { + return nil, err + } + go ssh.DiscardRequests(reqs) + return stream, nil +} + +func RejectStreams(chans <-chan ssh.NewChannel) { + for ch := range chans { + ch.Reject(ssh.Prohibited, "Tunnels disallowed") + } +} + +func ConnectStreams(l *Logger, chans <-chan ssh.NewChannel) { + + var streamCount int + + for ch := range chans { + stream, reqs, err := ch.Accept() + if err != nil { + l.Debugf("Failed to accept stream: %s", err) + continue + } + + streamCount++ + id := streamCount + + go ssh.DiscardRequests(reqs) + go handleStream(l.Fork("stream#%d", id), stream, string(ch.ExtraData())) + } +} + +func handleStream(l *Logger, src io.ReadWriteCloser, remote string) { + + dst, err := net.Dial("tcp", remote) + if err != nil { + l.Debugf("%s", err) + src.Close() + return + } + + l.Debugf("Open") + s, r := Pipe(src, dst) + l.Debugf("Close (sent %d received %d)", s, r) +} diff --git a/test/chisel_test.go b/test/chisel_test.go index 1160a3f..df8fcec 100644 --- a/test/chisel_test.go +++ b/test/chisel_test.go @@ -50,9 +50,10 @@ func TestBenchDirect(t *testing.T) { func TestBenchChisel(t *testing.T) { benchSizes("2001", t) } -func TestBenchrowbar(t *testing.T) { - benchSizes("4001", t) -} + +// func TestBenchrowbar(t *testing.T) { +// benchSizes("4001", t) +// } func benchSizes(port string, t *testing.T) { for size := 1; size < 100*MB; size *= 10 { @@ -113,6 +114,7 @@ func TestMain(m *testing.M) { time.Sleep(100 * time.Millisecond) hd := exec.Command("chiseld", "--port", "2002", "--auth", "foobar") + // hd.Stdout = os.Stdout if err := hd.Start(); err != nil { log.Fatal(err) } @@ -120,12 +122,11 @@ func TestMain(m *testing.M) { "--auth", "foobar", "127.0.0.1:2002", "2001:3000") - if err := hf.Start(); err != nil { log.Fatal(err) } - time.Sleep(100 * time.Millisecond) + time.Sleep(300 * time.Millisecond) fmt.Println("Running!") code := m.Run()