Skip to content

Commit

Permalink
document udp, log client connection failures, expose more settings vi…
Browse files Browse the repository at this point in the history
…a env-vars
  • Loading branch information
jpillora committed Nov 16, 2020
1 parent f04afd2 commit 6ddc09d
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 25 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -193,17 +193,18 @@ $ chisel client --help
<remote>s are remote connections tunneled through the server, each of
which come in the form:
<local-host>:<local-port>:<remote-host>:<remote-port>
<local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>
■ 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 <remote-host>:<remote-port> from the server to the client
as <local-host>:<local-port>, or:
R:<local-interface>:<local-port>:<remote-host>:<remote-port>
R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>
which does reverse port forwarding, sharing <remote-host>:<remote-port>
from the client to the server's <local-interface>:<local-port>.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
15 changes: 11 additions & 4 deletions client/client_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -65,17 +70,19 @@ 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
}
ctx, cancel := context.WithCancel(ctx)
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 {
Expand Down
6 changes: 4 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,18 @@ var clientHelp = `
<remote>s are remote connections tunneled through the server, each of
which come in the form:
<local-host>:<local-port>:<remote-host>:<remote-port>
<local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>
■ 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 <remote-host>:<remote-port> from the server to the client
as <local-host>:<local-port>, or:
R:<local-interface>:<local-port>:<remote-host>:<remote-port>
R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>
which does reverse port forwarding, sharing <remote-host>:<remote-port>
from the client to the server's <local-interface>:<local-port>.
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/server_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions server/server_listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os/user"
"path/filepath"

"github.com/jpillora/chisel/share/settings"
"golang.org/x/crypto/acme/autocert"
)

Expand Down Expand Up @@ -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 == "" {
Expand Down
23 changes: 16 additions & 7 deletions share/cnet/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"net"
"net/http"
"sync"

"golang.org/x/sync/errgroup"
)
Expand All @@ -13,16 +14,15 @@ import (
//adds graceful shutdowns
type HTTPServer struct {
*http.Server
serving bool
waiterMux sync.Mutex
waiter *errgroup.Group
listenErr error
}

//NewHTTPServer creates a new HTTPServer
func NewHTTPServer() *HTTPServer {
return &HTTPServer{
Server: &http.Server{},
serving: false,
Server: &http.Server{},
}

}
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
16 changes: 16 additions & 0 deletions share/cos/pprof.go
Original file line number Diff line number Diff line change
@@ -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")
}
28 changes: 28 additions & 0 deletions share/settings/env.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions share/tunnel/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions share/tunnel/tunnel_out_ssh_udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 6ddc09d

Please sign in to comment.