mirror of
https://github.com/jpillora/chisel
synced 2026-06-08 15:07:02 +00:00
progressing to users and remote address whitelists
This commit is contained in:
+71
-82
@@ -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()
|
||||
}
|
||||
|
||||
+23
-22
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"foo:bar": [
|
||||
"0.0.0.0:3000"
|
||||
],
|
||||
"ping:pong": [
|
||||
"0.0.0.0:[45]000",
|
||||
"example.com:80"
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
"<user:pass>": ["<addr-regex>","<addr-regex>"]
|
||||
when <user> connects, their <pass> 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 "<host/ip>:<port>".
|
||||
|
||||
--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: "<user>:<pass>".
|
||||
` + 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+111
-26
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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:]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Chisel tunnels TCP traffic over HTTP
|
||||
//
|
||||
// Read more here
|
||||
// https://github.com/jpillora/chisel
|
||||
|
||||
package chshare
|
||||
+1
-2
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user