diff --git a/README.md b/README.md index b250fb30..7afd752c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GoDoc](https://godoc.org/github.com/jpillora/chisel?status.svg)](https://godoc.org/github.com/jpillora/chisel) [![CI](https://github.com/jpillora/chisel/workflows/CI/badge.svg)](https://github.com/jpillora/chisel/actions?workflow=CI) -Chisel is a fast TCP tunnel, transported over HTTP, secured via SSH. Single executable including both client and server. Written in Go (golang). Chisel is mainly useful for passing through firewalls, though it can also be used to provide a secure endpoint into your network. +Chisel is a fast TCP/UDP tunnel, transported over HTTP, secured via SSH. Single executable including both client and server. Written in Go (golang). Chisel is mainly useful for passing through firewalls, though it can also be used to provide a secure endpoint into your network. ![overview](https://docs.google.com/drawings/d/1p53VWxzGNfy8rjr-mW8pvisJmhkoLl82vAgctO_6f1w/pub?w=960&h=720) @@ -193,17 +193,18 @@ $ chisel client --help s are remote connections tunneled through the server, each of which come in the form: - ::: + :::/ ■ local-host defaults to 0.0.0.0 (all interfaces). ■ local-port defaults to remote-port. ■ remote-port is required*. ■ remote-host defaults to 0.0.0.0 (server localhost). + ■ protocol defaults to tcp. which shares : from the server to the client as :, or: - R:::: + R::::/ which does reverse port forwarding, sharing : from the client to the server's :. @@ -220,6 +221,7 @@ $ chisel client --help R:socks R:5000:socks stdio:example.com:22 + 1.1.1.1:53/udp When the chisel server has --socks5 enabled, remotes can specify "socks" in place of remote-host and remote-port. diff --git a/client/client.go b/client/client.go index c88be69a..32338beb 100644 --- a/client/client.go +++ b/client/client.go @@ -172,7 +172,7 @@ func NewClient(c *Config) (*Client, error) { Auth: []ssh.AuthMethod{ssh.Password(pass)}, ClientVersion: "SSH-" + chshare.ProtocolVersion + "-client", HostKeyCallback: client.verifyServer, - Timeout: 30 * time.Second, + Timeout: settings.EnvDuration("SSH_TIMEOUT", 30*time.Second), } //prepare client tunnel client.tunnel = tunnel.New(tunnel.Config{ diff --git a/client/client_connect.go b/client/client_connect.go index 5a629724..0e9ff7bf 100644 --- a/client/client_connect.go +++ b/client/client_connect.go @@ -30,8 +30,12 @@ func (c *Client) connectionLoop(ctx context.Context) error { //connection error attempt := int(b.Attempt()) maxAttempt := c.config.MaxRetryCount + //dont print closed-connection errors + if strings.HasSuffix(err.Error(), "use of closed network connection") { + err = io.EOF + } //show error message and attempt counts (excluding disconnects) - if err != nil && err != io.EOF && !strings.HasSuffix(err.Error(), "use of closed network connection") { + if err != nil && err != io.EOF { msg := fmt.Sprintf("Connection error: %s", err) if attempt > 0 { msg += fmt.Sprintf(" (Attempt: %d", attempt) @@ -40,10 +44,11 @@ func (c *Client) connectionLoop(ctx context.Context) error { } msg += ")" } - c.Debugf(msg) + c.Infof(msg) } //give up? if !retry || (maxAttempt >= 0 && attempt >= maxAttempt) { + c.Infof("Give up") break } d := b.Duration() @@ -65,7 +70,7 @@ func (c *Client) connectionOnce(ctx context.Context) (connected, retry bool, err //already closed? select { case <-ctx.Done(): - return false, false, ctx.Err() + return false, false, errors.New("Cancelled") default: //still open } @@ -73,9 +78,11 @@ func (c *Client) connectionOnce(ctx context.Context) (connected, retry bool, err defer cancel() //prepare dialer d := websocket.Dialer{ - HandshakeTimeout: 45 * time.Second, + HandshakeTimeout: settings.EnvDuration("WS_TIMEOUT", 45*time.Second), Subprotocols: []string{chshare.ProtocolVersion}, TLSClientConfig: c.tlsConfig, + ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), + WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), } //optional proxy if p := c.proxyURL; p != nil { diff --git a/main.go b/main.go index 8b4595de..bba3e747 100644 --- a/main.go +++ b/main.go @@ -277,17 +277,18 @@ var clientHelp = ` s are remote connections tunneled through the server, each of which come in the form: - ::: + :::/ ■ local-host defaults to 0.0.0.0 (all interfaces). ■ local-port defaults to remote-port. ■ remote-port is required*. ■ remote-host defaults to 0.0.0.0 (server localhost). + ■ protocol defaults to tcp. which shares : from the server to the client as :, or: - R:::: + R::::/ which does reverse port forwarding, sharing : from the client to the server's :. @@ -304,6 +305,7 @@ var clientHelp = ` R:socks R:5000:socks stdio:example.com:22 + 1.1.1.1:53/udp When the chisel server has --socks5 enabled, remotes can specify "socks" in place of remote-host and remote-port. diff --git a/server/server.go b/server/server.go index 3581e8fa..5ecdda49 100644 --- a/server/server.go +++ b/server/server.go @@ -46,7 +46,9 @@ type Server struct { } var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: func(r *http.Request) bool { return true }, + ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), + WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), } // NewServer creates and returns a new chisel server diff --git a/server/server_handler.go b/server/server_handler.go index 968ad102..103b94f5 100644 --- a/server/server_handler.go +++ b/server/server_handler.go @@ -82,7 +82,7 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) { var r *ssh.Request select { case r = <-reqs: - case <-time.After(10 * time.Second): + case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)): l.Debugf("Timeout waiting for configuration") sshConn.Close() return diff --git a/server/server_listen.go b/server/server_listen.go index c22637f9..4fb1d570 100644 --- a/server/server_listen.go +++ b/server/server_listen.go @@ -10,6 +10,7 @@ import ( "os/user" "path/filepath" + "github.com/jpillora/chisel/share/settings" "golang.org/x/crypto/acme/autocert" ) @@ -63,11 +64,11 @@ func (s *Server) tlsLetsEncrypt(domains []string) *tls.Config { s.Infof("Accepting LetsEncrypt TOS and fetching certificate...") return true }, - Email: os.Getenv("CHISEL_LE_EMAIL"), + Email: settings.Env("LE_EMAIL"), HostPolicy: autocert.HostWhitelist(domains...), } //configure file cache - c := os.Getenv("CHISEL_LE_CACHE") + c := settings.Env("LE_CACHE") if c == "" { h := os.Getenv("HOME") if h == "" { diff --git a/share/cnet/http_server.go b/share/cnet/http_server.go index bf669df2..6f46a712 100644 --- a/share/cnet/http_server.go +++ b/share/cnet/http_server.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/http" + "sync" "golang.org/x/sync/errgroup" ) @@ -13,7 +14,7 @@ import ( //adds graceful shutdowns type HTTPServer struct { *http.Server - serving bool + waiterMux sync.Mutex waiter *errgroup.Group listenErr error } @@ -21,8 +22,7 @@ type HTTPServer struct { //NewHTTPServer creates a new HTTPServer func NewHTTPServer() *HTTPServer { return &HTTPServer{ - Server: &http.Server{}, - serving: false, + Server: &http.Server{}, } } @@ -46,8 +46,9 @@ func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.H if ctx == nil { return errors.New("ctx must be set") } + h.waiterMux.Lock() + defer h.waiterMux.Unlock() h.Handler = handler - h.serving = true h.waiter, ctx = errgroup.WithContext(ctx) h.waiter.Go(func() error { return h.Serve(l) @@ -60,17 +61,25 @@ func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.H } func (h *HTTPServer) Close() error { - if !h.serving { + h.waiterMux.Lock() + defer h.waiterMux.Unlock() + if h.waiter == nil { return errors.New("not started yet") } return h.Server.Close() } func (h *HTTPServer) Wait() error { - if !h.serving { + h.waiterMux.Lock() + unset := h.waiter == nil + h.waiterMux.Unlock() + if unset { return errors.New("not started yet") } - err := h.waiter.Wait() + h.waiterMux.Lock() + wait := h.waiter.Wait + h.waiterMux.Unlock() + err := wait() if err == http.ErrServerClosed { err = nil //success } diff --git a/share/cos/pprof.go b/share/cos/pprof.go new file mode 100644 index 00000000..6c95b7e7 --- /dev/null +++ b/share/cos/pprof.go @@ -0,0 +1,16 @@ +// +build pprof + +package cos + +import ( + "log" + "net/http" + _ "net/http/pprof" //import http profiler api +) + +func init() { + go func() { + log.Fatal(http.ListenAndServe("localhost:6060", nil)) + }() + log.Printf("[pprof] listening on 6060") +} diff --git a/share/settings/env.go b/share/settings/env.go new file mode 100644 index 00000000..17b002be --- /dev/null +++ b/share/settings/env.go @@ -0,0 +1,28 @@ +package settings + +import ( + "os" + "strconv" + "time" +) + +//Env returns a chisel environment variable +func Env(name string) string { + return os.Getenv("CHISEL_" + name) +} + +//EnvInt returns an integer using an environment variable, with a default fallback +func EnvInt(name string, def int) int { + if n, err := strconv.Atoi(Env(name)); err == nil { + return n + } + return def +} + +//EnvDuration returns a duration using an environment variable, with a default fallback +func EnvDuration(name string, def time.Duration) time.Duration { + if n, err := time.ParseDuration(Env(name)); err == nil { + return n + } + return def +} diff --git a/share/tunnel/tunnel.go b/share/tunnel/tunnel.go index 264f969d..0cfba6fa 100644 --- a/share/tunnel/tunnel.go +++ b/share/tunnel/tunnel.go @@ -121,8 +121,8 @@ func (t *Tunnel) getSSH(ctx context.Context) ssh.Conn { select { case <-ctx.Done(): //cancelled return nil - case <-time.After(35 * time.Second): //a bit longer than ssh timeout - return nil + case <-time.After(settings.EnvDuration("SSH_WAIT", 35*time.Second)): + return nil //a bit longer than ssh timeout case <-t.activatingConnWait(): t.activeConnMut.RLock() c := t.activeConn diff --git a/share/tunnel/tunnel_out_ssh_udp.go b/share/tunnel/tunnel_out_ssh_udp.go index 5cf91e06..20a10a63 100644 --- a/share/tunnel/tunnel_out_ssh_udp.go +++ b/share/tunnel/tunnel_out_ssh_udp.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jpillora/chisel/share/cio" + "github.com/jpillora/chisel/share/settings" ) func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error { @@ -80,8 +81,7 @@ func (h *udpHandler) handleRead(p *udpPacket, conn *udpConn) { buff := make([]byte, maxMTU) for { //response must arrive within 15 seconds - //TODO configurable - const deadline = 15 * time.Second + deadline := settings.EnvDuration("UDP_DEADLINE", 15*time.Second) conn.SetReadDeadline(time.Now().Add(deadline)) //read response n, err := conn.Read(buff)