diff --git a/client/client.go b/client/client.go index 3a93746..0a330e4 100644 --- a/client/client.go +++ b/client/client.go @@ -1,33 +1,34 @@ package chclient import ( - "errors" "fmt" + "io" "net" "net/url" "regexp" "strings" "time" - "github.com/hashicorp/yamux" "github.com/jpillora/backoff" "github.com/jpillora/chisel/share" - "github.com/jpillora/conncrypt" + "golang.org/x/crypto/ssh" "golang.org/x/net/websocket" ) type Client struct { *chshare.Logger config *chshare.Config - encconfig []byte - key, server string + sshConfig *ssh.ClientConfig proxies []*Proxy - session *yamux.Session + sshConn ssh.Conn + fingerprint string + server string + keyPrefix string running bool runningc chan error } -func NewClient(key, server string, remotes ...string) (*Client, error) { +func NewClient(keyPrefix, auth, server string, remotes ...string) (*Client, error) { //apply default scheme if !strings.HasPrefix(server, "http") { @@ -60,20 +61,27 @@ func NewClient(key, server string, remotes ...string) (*Client, error) { config.Remotes = append(config.Remotes, r) } - encconfig, err := chshare.EncodeConfig(config) - if err != nil { - return nil, fmt.Errorf("Failed to encode config: %s", err) - } - - return &Client{ + c := &Client{ Logger: chshare.NewLogger("client"), config: config, - encconfig: encconfig, - key: key, server: u.String(), + keyPrefix: keyPrefix, running: true, runningc: make(chan error, 1), - }, nil + } + + c.sshConfig = &ssh.ClientConfig{ + ClientVersion: chshare.ProtocolVersion + "-client", + HostKeyCallback: c.verifyServer, + } + + user, pass := chshare.ParseAuth(auth) + if user != "" { + c.sshConfig.User = user + c.sshConfig.Auth = []ssh.AuthMethod{ssh.Password(pass)} + } + + return c, nil } //Start then Wait @@ -82,6 +90,15 @@ func (c *Client) Run() error { return c.Wait() } +func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error { + f := chshare.FingerprintKey(key) + if c.keyPrefix != "" && !strings.HasPrefix(f, c.keyPrefix) { + return fmt.Errorf("Invalid fingerprint (Got %s)", f) + } + c.fingerprint = f + return nil +} + //Starts the client func (c *Client) Start() { go c.start() @@ -90,37 +107,24 @@ func (c *Client) Start() { func (c *Client) start() { c.Infof("Connecting to %s\n", c.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) } - var connerr error - b := &backoff.Backoff{Max: 15 * time.Second} - //connection loop! + var connerr error + b := &backoff.Backoff{Max: 5 * time.Minute} + for { if !c.running { break } if connerr != nil { d := b.Duration() - c.Infof("Connerr: %v", connerr) - c.Infof("Retrying in %s...", d) + c.Infof("Retrying in %s...\n", d) connerr = nil time.Sleep(d) } @@ -131,58 +135,43 @@ func (c *Client) start() { continue } - conn := net.Conn(ws) - - if c.key != "" { - conn = conncrypt.New(conn, &conncrypt.Config{Password: c.key}) + sshConn, chans, reqs, err := ssh.NewClientConn(ws, "", c.sshConfig) + //NOTE break -> dont retry on handshake failures + if err != nil { + if strings.Contains(err.Error(), "unable to authenticate") { + c.Infof("Authentication failed") + } else { + c.Infof(err.Error()) + } + break } - - //write config, read result - chshare.SizeWrite(conn, c.encconfig) - - resp := chshare.SizeRead(conn) - if string(resp) != "Handshake Success" { - //no point in retrying - c.runningc <- errors.New("Handshake failed") - conn.Close() + conf, _ := chshare.EncodeConfig(c.config) + _, conerr, err := sshConn.SendRequest("config", true, conf) + if err != nil { + c.Infof("Config verification failed", c.fingerprint) + break + } + if len(conerr) > 0 { + c.Infof(string(conerr)) break } - // Setup client side of yamux - c.session, err = yamux.Client(conn, nil) - if err != nil { - connerr = err - continue - } + c.Infof("Connected (%s)", c.fingerprint) + //connected b.Reset() - //check latency - go func() { - d, err := c.session.Ping() - if err == nil { - c.Infof("Connected (Latency: %s)\n", d) - } else { - c.Infof("Connected\n") - } - }() - - //signal is connected - connected := make(chan bool) - - //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 chshare.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) } @@ -195,8 +184,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/client/proxy.go b/client/proxy.go index 4683402..a6bc84e 100644 --- a/client/proxy.go +++ b/client/proxy.go @@ -1,7 +1,7 @@ package chclient import ( - "encoding/binary" + "io" "net" "github.com/jpillora/chisel/share" @@ -9,18 +9,18 @@ import ( type Proxy struct { *chshare.Logger - id int - count int - remote *chshare.Remote - openStream func() (net.Conn, error) + client *Client + id int + count int + remote *chshare.Remote } -func NewProxy(c *Client, id int, remote *chshare.Remote, openStream func() (net.Conn, error)) *Proxy { +func NewProxy(c *Client, id int, remote *chshare.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 +43,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 := chshare.OpenStream(p.client.sshConn, remoteAddr) + if err != nil { + l.Infof("Stream error: %s", err) + src.Close() + return + } //then pipe s, r := chshare.Pipe(src, dst) - - clog.Debugf("Close (sent %d received %d)", s, r) + l.Debugf("Close (sent %d received %d)", s, r) } diff --git a/example/users.json b/example/users.json new file mode 100644 index 0000000..e4bab74 --- /dev/null +++ b/example/users.json @@ -0,0 +1,9 @@ +{ + "foo:bar": [ + "0.0.0.0:3000" + ], + "ping:pong": [ + "0.0.0.0:[45]000", + "example.com:80" + ] +} \ No newline at end of file diff --git a/main.go b/main.go index 226eb83..90c5090 100644 --- a/main.go +++ b/main.go @@ -59,13 +59,13 @@ func main() { } var commonHelp = ` - --key, Enables AES256 encryption and specify the string to - use to derive the key (derivation is performed using PBKDF2 - with 2048 iterations of SHA256). - -v, Enable verbose logging --help, This help text + + Read more: + https://github.com/jpillora/chisel + ` var serverHelp = ` @@ -78,22 +78,32 @@ var serverHelp = ` --port, Defines the HTTP listening port (defaults to 8080). + --key, An optional string to seed the generation of a ECC public + and private key pair. All commications will be secured using this + key pair. Share the resulting fingerprint with clients to prevent + man-in-the-middle attacks. + + --authfile, An optional path to a users.json file. This file should + be an object with users defined like: + "": ["",""] + when connects, their will be verified and then + each of the remote addresses will be compared against the list + of address regular expressions for a match. Addresses will + always come in the form ":". + --proxy, Specifies the default proxy target to use when chisel receives a normal HTTP request. -` + commonHelp + ` - Read more: - https://github.com/jpillora/chisel - -` +` + commonHelp func server(args []string) { flags := flag.NewFlagSet("server", flag.ContinueOnError) - hostf := flags.String("host", "", "") - portf := flags.String("port", "", "") - authf := flags.String("key", "", "") - proxyf := flags.String("proxy", "", "") + host := flags.String("host", "", "") + port := flags.String("port", "", "") + key := flags.String("key", "", "") + authfile := flags.String("authfile", "", "") + proxy := flags.String("proxy", "", "") verbose := flags.Bool("v", false, "") flags.Usage = func() { @@ -102,28 +112,21 @@ func server(args []string) { } flags.Parse(args) - host := *hostf - if host == "" { - host = os.Getenv("HOST") + if *host == "" { + *host = os.Getenv("HOST") } - if host == "" { - host = "0.0.0.0" + if *host == "" { + *host = "0.0.0.0" } - port := *portf - if port == "" { - port = os.Getenv("PORT") + if *port == "" { + *port = os.Getenv("PORT") } - if port == "" { - port = "8080" + if *port == "" { + *port = "8080" } - key := *authf - if key == "" { - key = os.Getenv("key") - } - - s, err := chserver.NewServer(key, *proxyf) + s, err := chserver.NewServer(*key, *authfile, *proxy) if err != nil { log.Fatal(err) } @@ -131,7 +134,7 @@ func server(args []string) { s.Info = true s.Debug = *verbose - if err = s.Run(host, port); err != nil { + if err = s.Run(*host, *port); err != nil { log.Fatal(err) } } @@ -159,17 +162,22 @@ var clientHelp = ` 192.168.0.5:3000:google.com:80 Options: -` + commonHelp + ` - Read more: - https://github.com/jpillora/chisel -` + --key, An optional fingerprint (server authentication) string to + compare against the server's public key. You may provide just a + prefix of the key or the entire string. Fingerprint mismatches + will disallow the connection. + + --auth, An optional username and password (client authentication) + in the form: ":". +` + commonHelp func client(args []string) { flags := flag.NewFlagSet("client", flag.ContinueOnError) key := flags.String("key", "", "") + auth := flags.String("auth", "", "") verbose := flags.Bool("v", false, "") flags.Usage = func() { fmt.Fprintf(os.Stderr, clientHelp) @@ -185,7 +193,7 @@ func client(args []string) { server := args[0] remotes := args[1:] - c, err := chclient.NewClient(*key, server, remotes...) + c, err := chclient.NewClient(*key, *auth, server, remotes...) if err != nil { log.Fatal(err) } diff --git a/server/endpoint.go b/server/endpoint.go deleted file mode 100644 index 3d15dfc..0000000 --- a/server/endpoint.go +++ /dev/null @@ -1,47 +0,0 @@ -package chserver - -import ( - "net" - - "github.com/jpillora/chisel/share" -) - -type endpoint struct { - *chshare.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 := chshare.Pipe(src, dst) - e.Debugf("stream#%d: Close (sent %d received %d)", eid, s, r) -} diff --git a/server/server.go b/server/server.go index 6438fd7..48bbe58 100644 --- a/server/server.go +++ b/server/server.go @@ -1,34 +1,63 @@ package chserver import ( - "net" + "errors" + "log" "net/http" "net/http/httputil" "net/url" "github.com/jpillora/chisel/share" - "github.com/jpillora/conncrypt" + "golang.org/x/crypto/ssh" "golang.org/x/net/websocket" ) type Server struct { *chshare.Logger - key string - wsCount int - wsServer websocket.Server - httpServer *chshare.HTTPServer - proxy *httputil.ReverseProxy + Users chshare.Users + fingerprint string + wsCount int + wsServer websocket.Server + httpServer *chshare.HTTPServer + proxy *httputil.ReverseProxy + sshConfig *ssh.ServerConfig + sessions map[string]*chshare.User } -func NewServer(key, proxy string) (*Server, error) { +func NewServer(keySeed, authfile, proxy string) (*Server, error) { s := &Server{ Logger: chshare.NewLogger("server"), - key: key, wsServer: websocket.Server{}, httpServer: chshare.NewHTTPServer(), + sessions: map[string]*chshare.User{}, } s.wsServer.Handler = websocket.Handler(s.handleWS) + //parse users, if provided + if authfile != "" { + users, err := chshare.ParseUsers(authfile) + if err != nil { + return nil, err + } + s.Users = users + } + + //generate private key (optionally using seed) + key, _ := chshare.GenerateKey(keySeed) + //convert into ssh.PrivateKey + private, err := ssh.ParsePrivateKey(key) + if err != nil { + log.Fatal("Failed to parse key") + } + //fingerprint this key + s.fingerprint = chshare.FingerprintKey(private.PublicKey()) + //create ssh config + s.sshConfig = &ssh.ServerConfig{ + ServerVersion: chshare.ProtocolVersion + "-server", + PasswordCallback: s.authUser, + } + s.sshConfig.AddHostKey(private) + if proxy != "" { u, err := url.Parse(proxy) if err != nil { @@ -57,8 +86,9 @@ func (s *Server) Run(host, port string) error { } func (s *Server) Start(host, port string) error { - if s.key != "" { - s.Infof("Authenication enabled") + s.Infof("Fingerprint %s", s.fingerprint) + if len(s.Users) > 0 { + s.Infof("User authenication enabled") } if s.proxy != nil { s.Infof("Default proxy enabled") @@ -93,25 +123,80 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) } -func (s *Server) handleWS(ws *websocket.Conn) { - - conn := net.Conn(ws) - - if s.key != "" { - conn = conncrypt.New(conn, &conncrypt.Config{Password: s.key}) +// +func (s *Server) authUser(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { + // no auth + if len(s.Users) == 0 { + return nil, nil } + // authenticate user + u, ok := s.Users[c.User()] + if !ok || u.Pass != string(pass) { + return nil, errors.New("Invalid auth") + } + //insert session + s.sessions[string(c.SessionID())] = u + return nil, nil +} - configb := chshare.SizeRead(conn) - config, err := chshare.DecodeConfig(configb) - +func (s *Server) handleWS(ws *websocket.Conn) { + // Before use, a handshake must be performed on the incoming net.Conn. + sshConn, chans, reqs, err := ssh.NewServerConn(ws, s.sshConfig) if err != nil { - s.Infof("Handshake failed: %s", err) - chshare.SizeWrite(conn, []byte("Handshake failed")) + s.Debugf("Failed to handshake (%s)", err) return } - chshare.SizeWrite(conn, []byte("Handshake Success")) - // s.Infof("success %+v\n", config) - s.wsCount++ - newWebSocket(s, config, conn).handle() + //load user + sid := string(sshConn.SessionID()) + var user *chshare.User + if len(s.Users) > 0 { + user = s.sessions[sid] + } + + //verify configuration + r := <-reqs + reply := func(err error) { + r.Reply(err == nil, []byte(err.Error())) + if err != nil { + sshConn.Close() + } + } + if r.Type != "config" { + reply(s.Errorf("expecting config request")) + return + } + + c, err := chshare.DecodeConfig(r.Payload) + if err != nil { + reply(s.Errorf("invalid config")) + return + } + + //if user is provided, ensure they have + //access to the desired remote + if user != nil { + for _, r := range c.Remotes { + addr := r.RemoteHost + ":" + r.RemotePort + if !user.HasAccess(addr) { + reply(s.Errorf("access to '%s' denied", addr)) + return + } + } + } + + //prepare connection logger + s.wsCount++ + id := s.wsCount + l := s.Fork("session#%d", id) + + l.Debugf("Open") + go ssh.DiscardRequests(reqs) + go chshare.ConnectStreams(l, chans) + sshConn.Wait() + l.Debugf("Close") + + if user != nil { + delete(s.sessions, sid) + } } diff --git a/server/websocket.go b/server/websocket.go deleted file mode 100644 index b9f9fdb..0000000 --- a/server/websocket.go +++ /dev/null @@ -1,95 +0,0 @@ -package chserver - -import ( - "encoding/binary" - "net" - - "github.com/hashicorp/yamux" - "github.com/jpillora/chisel/share" -) - -type webSocket struct { - *chshare.Logger - id int - config *chshare.Config - conn net.Conn -} - -func newWebSocket(s *Server, config *chshare.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/share/determ_rand.go b/share/determ_rand.go new file mode 100644 index 0000000..8bd6695 --- /dev/null +++ b/share/determ_rand.go @@ -0,0 +1,44 @@ +package chshare + +// overview: only half the result is used as the output +// next -> sha512(next) -> [output|next] -> + +import ( + "crypto/sha512" + "io" +) + +const DetermRandIter = 1024 + +func NewDetermRand(seed []byte) io.Reader { + var out []byte + //strengthen seed + var next = seed + for i := 0; i < DetermRandIter; i++ { + next, out = hash(next) + } + return &DetermRand{ + next: next, + out: out, + } +} + +type DetermRand struct { + next, out []byte +} + +func (d *DetermRand) Read(b []byte) (int, error) { + n := 0 + l := len(b) + for n < l { + next, out := hash(d.next) + n += copy(b[n:], out) + d.next = next + } + return n, nil +} + +func hash(input []byte) ([]byte, []byte) { + nextout := sha512.Sum512(input) + return nextout[:sha512.Size/2], nextout[sha512.Size/2:] +} diff --git a/share/docs.go b/share/docs.go deleted file mode 100644 index baf3c99..0000000 --- a/share/docs.go +++ /dev/null @@ -1,6 +0,0 @@ -// Chisel tunnels TCP traffic over HTTP -// -// Read more here -// https://github.com/jpillora/chisel - -package chshare diff --git a/share/pipe.go b/share/pipe.go index 6ab0356..f396a38 100644 --- a/share/pipe.go +++ b/share/pipe.go @@ -2,11 +2,10 @@ package chshare 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/share/simple_protocol.go b/share/simple_protocol.go deleted file mode 100644 index 8894702..0000000 --- a/share/simple_protocol.go +++ /dev/null @@ -1,27 +0,0 @@ -package chshare - -import ( - "encoding/binary" - "net" -) - -//simple protocol [2 bytes of size, size bytes of data] - -//TODO add length and error checks -func SizeRead(c net.Conn) []byte { - sizeb := make([]byte, 2) - c.Read(sizeb) - size := binary.BigEndian.Uint16(sizeb) - datab := make([]byte, size) - c.Read(datab) - return datab -} - -func SizeWrite(c net.Conn, data []byte) { - size := len(data) - sizeb := make([]byte, 2) - binary.BigEndian.PutUint16(sizeb, uint16(size)) - c.Write(sizeb) - c.Write(data) - return -} diff --git a/share/ssh.go b/share/ssh.go new file mode 100644 index 0000000..f3ab023 --- /dev/null +++ b/share/ssh.go @@ -0,0 +1,96 @@ +package chshare + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/md5" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "net" + "strings" + + "golang.org/x/crypto/ssh" +) + +func GenerateKey(seed string) ([]byte, error) { + + var r io.Reader + if seed == "" { + r = rand.Reader + } else { + r = NewDetermRand([]byte(seed)) + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + 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 { + bytes := md5.Sum(k.Marshal()) + strbytes := make([]string, len(bytes)) + for i, b := range bytes { + strbytes[i] = fmt.Sprintf("%02x", b) + } + return strings.Join(strbytes, ":") +} + +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 { + + addr := string(ch.ExtraData()) + + 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, addr) + } +} + +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/share/user.go b/share/user.go new file mode 100644 index 0000000..a3e3a92 --- /dev/null +++ b/share/user.go @@ -0,0 +1,68 @@ +package chshare + +import ( + "encoding/json" + "errors" + "io/ioutil" + "regexp" + "strings" +) + +func ParseAuth(auth string) (string, string) { + if strings.Contains(auth, ":") { + pair := strings.SplitN(auth, ":", 2) + return pair[0], pair[1] + } + return "", "" +} + +type User struct { + Name string + Pass string + Addrs []*regexp.Regexp +} + +func (u *User) HasAccess(addr string) bool { + m := false + for _, r := range u.Addrs { + if r.MatchString(addr) { + m = true + break + } + } + return m +} + +type Users map[string]*User + +func ParseUsers(authfile string) (Users, error) { + + b, err := ioutil.ReadFile(authfile) + if err != nil { + return nil, errors.New("Failed to read auth file") + } + + var raw map[string][]string + err = json.Unmarshal(b, &raw) + if err != nil { + return nil, errors.New("Invalid JSON: " + err.Error()) + } + + users := Users{} + for auth, remotes := range raw { + u := &User{} + u.Name, u.Pass = ParseAuth(auth) + if u.Name == "" { + return nil, errors.New("Invalid user:pass string") + } + for _, r := range remotes { + re, err := regexp.Compile(r) + if err != nil { + return nil, errors.New("Invalid address regex") + } + u.Addrs = append(u.Addrs, re) + } + users[u.Name] = u + } + return users, nil +} diff --git a/test/chisel_test.go b/test/chisel_test.go new file mode 100644 index 0000000..df8fcec --- /dev/null +++ b/test/chisel_test.go @@ -0,0 +1,142 @@ +//chisel tests +//=================== +// (direct) +// .--------------->----------------. +// / chisel chisel \ +// request--->client:2001--->server:2002---->fileserver:3000 +// \ / +// '--> crowbar:4001--->crowbar:4002' +// client server +// +// benchmarks don't use testing.B, instead use +// go test -test.run=Bench +// +// tests use +// go test -test.run=Request +// +// crowbar and chisel binaries should be in your PATH + +package test + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path" + + "testing" + "time" +) + +const ( + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB +) + +//test +func TestRequestChisel(t *testing.T) { + testTunnel("2001", 500, t) + testTunnel("2001", 50000, t) +} + +//benchmark +func TestBenchDirect(t *testing.T) { + benchSizes("3000", t) +} +func TestBenchChisel(t *testing.T) { + benchSizes("2001", t) +} + +// func TestBenchrowbar(t *testing.T) { +// benchSizes("4001", t) +// } + +func benchSizes(port string, t *testing.T) { + for size := 1; size < 100*MB; size *= 10 { + testTunnel(port, size, t) + } +} + +func testTunnel(port string, size int, t *testing.T) { + t0 := time.Now() + resp, err := requestFile(port, size) + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + t1 := time.Now() + fmt.Printf(":%s => %d bytes in %s\n", port, size, t1.Sub(t0)) + if len(b) != size { + t.Fatalf("%d bytes expected, got %d", size, len(b)) + } +} + +//============================ + +//global setup +func TestMain(m *testing.M) { + + fs := makeFileServer() + go func() { + log.Fatal(fs.Wait()) + }() + + dir, _ := os.Getwd() + cd := exec.Command("crowbard", + `-listen`, "0.0.0.0:4002", + `-userfile`, path.Join(dir, "userfile")) + if err := cd.Start(); err != nil { + log.Fatal(err) + } + go func() { + log.Fatalf("crowbard: %s", cd.Wait()) + }() + + time.Sleep(100 * time.Millisecond) + + cf := exec.Command("crowbar-forward", + "-local=0.0.0.0:4001", + "-server=http://127.0.0.1:4002", + "-remote=127.0.0.1:3000", + "-username", "foo", + "-password", "bar") + if err := cf.Start(); err != nil { + log.Fatal(err) + } + + 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) + } + hf := exec.Command("chisel-forward", + "--auth", "foobar", + "127.0.0.1:2002", + "2001:3000") + if err := hf.Start(); err != nil { + log.Fatal(err) + } + + time.Sleep(300 * time.Millisecond) + + fmt.Println("Running!") + code := m.Run() + fmt.Println("Done") + + cd.Process.Kill() + cf.Process.Kill() + hd.Process.Kill() + hf.Process.Kill() + fs.Close() + + os.Exit(code) +}