add udp support, massive refactor and more (see below)

* split out share/ package into multiple subpackages
* added share/compat.go to keep backward compatibility(ish)
* moved shared tunnelling logic from client/server packages into share/tunnel/
* added remote protocol "<host>:<port>/<protocol>", currently supporting tcp and udp
* added an end-to-end test suite
* added TODO e2e tests as contribution targets
* added deep context integration for improved cleanup and cancellation
This commit is contained in:
Jaime Pillora
2020-07-18 11:05:53 +10:00
parent 591f95885f
commit 53e72fe2ba
52 changed files with 2272 additions and 974 deletions
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# TODO
gocompat compare \
--go1compat \
--log-level=debug \
--git-refs=origin/master..$(git rev-parse --abbrev-ref HEAD) \
./...
+3 -1
View File
@@ -1,5 +1,5 @@
# test this goreleaser config with:
# - cd chisel/
# - cd chisel
# - goreleaser --skip-publish --rm-dist --config .github/goreleaser.yml
builds:
- env:
@@ -36,6 +36,8 @@ archives:
- format: gz
files:
- none*
release:
prerelease: auto
changelog:
sort: asc
filters:
+46
View File
@@ -0,0 +1,46 @@
on: [push, pull_request]
name: CI
jobs:
# ================
# TEST JOB
# runs on every push and PR
# runs 2x3 times (see matrix)
# ================
test:
name: Test
strategy:
matrix:
go-version: [1.13.x, 1.14.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Build
run: go build -v .
- name: Test
run: go test -v ./...
# ================
# RELEASE JOB
# runs after a success test
# only runs on push "v*" tag
# ================
release:
name: Release
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: goreleaser
if: success()
uses: docker://goreleaser/goreleaser:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: release --config .github/goreleaser.yml
-19
View File
@@ -1,19 +0,0 @@
on:
push:
tags:
- "v*"
name: CI
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@master
- name: goreleaser
uses: docker://goreleaser/goreleaser:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: release --config .github/goreleaser.yml
if: success()
+53 -100
View File
@@ -2,14 +2,14 @@
[![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 very similar to [crowbar](https://github.com/q3k/crowbar) though achieves **much** higher [performance](#performance).
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.
![overview](https://docs.google.com/drawings/d/1p53VWxzGNfy8rjr-mW8pvisJmhkoLl82vAgctO_6f1w/pub?w=960&h=720)
### Features
- Easy to use
- [Performant](#performance)\*
- [Performant](./test/bench/perf.md)\*
- [Encrypted connections](#security) using the SSH protocol (via `crypto/ssh`)
- [Authenticated connections](#authentication); authenticated client connections with a users config file, authenticated server connections with fingerprint matching.
- Client auto-reconnects with [exponential backoff](https://github.com/jpillora/backoff)
@@ -64,22 +64,31 @@ and then visit [localhost:3000](http://localhost:3000/), we should see a directo
### Usage
```
<!-- render these help texts by hand,
or use https://github.com/jpillora/md-tmpl
with $ md-tmpl -w README.md -->
<!--tmpl,code=plain:echo "$ chisel --help" && go run main.go --help | cat -->
``` plain
$ chisel --help
Usage: chisel [command] [--help]
Usage: chisel [command] [--help]
Version: 1.X.X
Version: 0.0.0-src (go1.14.3)
Commands:
server - runs chisel in server mode
client - runs chisel in client mode
Commands:
server - runs chisel in server mode
client - runs chisel in client mode
Read more:
https://github.com/jpillora/chisel
```
Read more:
https://github.com/jpillora/chisel
```
<!--/tmpl-->
<!--tmpl,code=plain:echo "$ chisel server --help" && go run main.go server --help | cat -->
``` plain
$ chisel server --help
Usage: chisel server [options]
@@ -111,8 +120,15 @@ $ chisel server --help
remotes. This file will be automatically reloaded on change.
--auth, An optional string representing a single user with full
access, in the form of <user:pass>. This is equivalent to creating an
authfile with {"<user:pass>": [""]}.
access, in the form of <user:pass>. It is equivalent to creating an
authfile with {"<user:pass>": [""]}. If unset, it will use the
environment variable AUTH.
--keepalive, An optional keepalive interval. Since the underlying
transport is HTTP, in many instances we'll be traversing through
proxies, often these proxies will close idle connections. You must
specify a time with a unit, for example '5s' or '2m'. Defaults
to '25s' (set to 0s to disable).
--proxy, Specifies another HTTP server to proxy requests to when
chisel receives a normal HTTP request. Useful for hiding chisel in
@@ -136,14 +152,17 @@ $ chisel server --help
a SIGHUP to short-circuit the client reconnect timer
Version:
1.X.X (go1.14)
0.0.0-src (go1.14.3)
Read more:
https://github.com/jpillora/chisel
```
<!--/tmpl-->
```
<!--tmpl,code=plain:echo "$ chisel client --help" && go run main.go client --help | cat -->
``` plain
$ chisel client --help
Usage: chisel client [options] <server> <remote> [remote] [remote] ...
@@ -217,8 +236,8 @@ $ chisel client --help
--keepalive, An optional keepalive interval. Since the underlying
transport is HTTP, in many instances we'll be traversing through
proxies, often these proxies will close idle connections. You must
specify a time with a unit, for example '30s' or '2m'. Defaults
to '0s' (disabled).
specify a time with a unit, for example '5s' or '2m'. Defaults
to '25s' (set to 0s to disable).
--max-retry-count, Maximum number of times to retry before exiting.
Defaults to unlimited.
@@ -230,7 +249,7 @@ $ chisel client --help
used to reach the chisel server. Authentication can be specified
inside the URL.
For example, http://admin:password@my-server.com:8081
or: socks://admin:password@my-server.com:1080
or: socks://admin:password@my-server.com:1080
--header, Set a custom header in the form "HeaderName: HeaderContent".
Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World")
@@ -250,12 +269,13 @@ $ chisel client --help
a SIGHUP to short-circuit the client reconnect timer
Version:
1.X.X (go1.14)
0.0.0-src (go1.14.3)
Read more:
https://github.com/jpillora/chisel
```
<!--/tmpl-->
### Security
@@ -281,92 +301,27 @@ docker run \
2. Connect your chisel client (using server's fingerprint)
```sh
chisel client --fingerprint ab:12:34 server-address:9312 socks
chisel client --fingerprint ab:12:34 <server-address>:9312 socks
```
3. Point your SOCKS5 clients (e.g. OS/Browser) to:
```
localhost:1080
<client-address>:1080
```
4. Now you have an encrypted, authenticated SOCKS5 connection over HTTP
### Performance
With [crowbar](https://github.com/q3k/crowbar), a connection is tunneled by repeatedly querying the server with updates. This results in a large amount of HTTP and TCP connection overhead. Chisel overcomes this using WebSockets combined with [crypto/ssh](https://golang.org/x/crypto/ssh) to create hundreds of logical connections, resulting in **one** TCP connection per client.
### Caveats
In this simple benchmark, we have:
Since WebSockets support is required:
```
(direct)
.--------------->----------------.
/ chisel chisel \
request--->client:2001--->server:2002---->fileserver:3000
\ /
'--> crowbar:4001--->crowbar:4002'
client server
```
Note, we're using an in-memory "file" server on localhost for these tests
_direct_
```
:3000 => 1 bytes in 1.291417ms
:3000 => 10 bytes in 713.525µs
:3000 => 100 bytes in 562.48µs
:3000 => 1000 bytes in 595.445µs
:3000 => 10000 bytes in 1.053298ms
:3000 => 100000 bytes in 741.351µs
:3000 => 1000000 bytes in 1.367143ms
:3000 => 10000000 bytes in 8.601549ms
:3000 => 100000000 bytes in 76.3939ms
```
`chisel`
```
:2001 => 1 bytes in 1.351976ms
:2001 => 10 bytes in 1.106086ms
:2001 => 100 bytes in 1.005729ms
:2001 => 1000 bytes in 1.254396ms
:2001 => 10000 bytes in 1.139777ms
:2001 => 100000 bytes in 2.35437ms
:2001 => 1000000 bytes in 11.502673ms
:2001 => 10000000 bytes in 123.130246ms
:2001 => 100000000 bytes in 966.48636ms
```
~100MB in **~1 second**
`crowbar`
```
:4001 => 1 bytes in 3.335797ms
:4001 => 10 bytes in 1.453007ms
:4001 => 100 bytes in 1.811727ms
:4001 => 1000 bytes in 1.621525ms
:4001 => 10000 bytes in 5.20729ms
:4001 => 100000 bytes in 38.461926ms
:4001 => 1000000 bytes in 358.784864ms
:4001 => 10000000 bytes in 3.603206487s
:4001 => 100000000 bytes in 36.332395213s
```
~100MB in **36 seconds**
See more [test/](test/)
### Known Issues
- WebSockets support is required
_ IaaS providers all will support WebSockets
_ Unless an unsupporting HTTP proxy has been forced in front of you, in which case I'd argue that you've been downgraded to PaaS.
_ PaaS providers vary in their support for WebSockets
_ Heroku has full support
_ Openshift has full support though connections are only accepted on ports 8443 and 8080
_ Google App Engine has **no** support (Track this on [their repo](https://code.google.com/p/googleappengine/issues/detail?id=2535))
- IaaS providers all will support WebSockets (unless an unsupporting HTTP proxy has been forced in front of you, in which case I'd argue that you've been downgraded to PaaS)
- PaaS providers vary in their support for WebSockets
- Heroku has full support
- Openshift has full support though connections are only accepted on ports 8443 and 8080
- Google App Engine has **no** support (Track this on [their repo](https://code.google.com/p/googleappengine/issues/detail?id=2535))
### Contributing
@@ -379,12 +334,10 @@ See more [test/](test/)
### Changelog
- `1.0` - Initial release
- `1.1` - Swapped out simple symmetric encryption for ECDSA SSH
- `1.1` - Replaced simple symmetric encryption for ECDSA SSH
- `1.2` - Added SOCKS5 (server) and HTTP CONNECT (client) support
- `1.3` - Added reverse tunnelling support
### Todo
- Better, faster tests
- Expose a stats page for proxy throughput
- Treat client stdin/stdout as a socket
- `1.4` - Added arbitrary HTTP header support
- `1.5` - Added reverse SOCKS support (by @aus)
- `1.6` - Added client stdio support (by @BoleynSu)
- `1.7` - Added UDP support
+194 -199
View File
@@ -12,18 +12,23 @@ import (
"strings"
"time"
"github.com/armon/go-socks5"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/ccrypto"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/cos"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/chisel/share/tunnel"
"golang.org/x/crypto/ssh"
"golang.org/x/net/proxy"
"golang.org/x/sync/errgroup"
)
//Config represents a client configuration
type Config struct {
shared *chshare.Config
Fingerprint string
Auth string
KeepAlive time.Duration
@@ -38,28 +43,28 @@ type Config struct {
//Client represents a client instance
type Client struct {
*chshare.Logger
config *Config
sshConfig *ssh.ClientConfig
sshConn ssh.Conn
proxyURL *url.URL
server string
running bool
runningc chan error
connStats chshare.ConnStats
socksServer *socks5.Server
*cio.Logger
config *Config
computed settings.Config
sshConfig *ssh.ClientConfig
proxyURL *url.URL
server string
connStats cnet.ConnStats
stop func()
eg *errgroup.Group
tunnel *tunnel.Tunnel
}
//NewClient creates a new client instance
func NewClient(config *Config) (*Client, error) {
func NewClient(c *Config) (*Client, error) {
//apply default scheme
if !strings.HasPrefix(config.Server, "http") {
config.Server = "http://" + config.Server
if !strings.HasPrefix(c.Server, "http") {
c.Server = "http://" + c.Server
}
if config.MaxRetryInterval < time.Second {
config.MaxRetryInterval = 5 * time.Minute
if c.MaxRetryInterval < time.Second {
c.MaxRetryInterval = 5 * time.Minute
}
u, err := url.Parse(config.Server)
u, err := url.Parse(c.Server)
if err != nil {
return nil, err
}
@@ -73,16 +78,27 @@ func NewClient(config *Config) (*Client, error) {
}
//swap to websockets scheme
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
shared := &chshare.Config{}
createSocksServer := false
hasReverse := false
hasSocks := false
hasStdio := false
for _, s := range config.Remotes {
r, err := chshare.DecodeRemote(s)
client := &Client{
Logger: cio.NewLogger("client"),
config: c,
computed: settings.Config{
Version: chshare.BuildVersion,
},
server: u.String(),
}
for _, s := range c.Remotes {
r, err := settings.DecodeRemote(s)
if err != nil {
return nil, fmt.Errorf("Failed to decode remote '%s': %s", s, err)
}
if r.Socks && r.Reverse {
createSocksServer = true
if r.Socks {
hasSocks = true
}
if r.Reverse {
hasReverse = true
}
if r.Stdio {
if hasStdio {
@@ -90,27 +106,19 @@ func NewClient(config *Config) (*Client, error) {
}
hasStdio = true
}
shared.Remotes = append(shared.Remotes, r)
client.computed.Remotes = append(client.computed.Remotes, r)
}
config.shared = shared
client := &Client{
Logger: chshare.NewLogger("client"),
config: config,
server: u.String(),
running: true,
runningc: make(chan error, 1),
}
client.Info = true
if p := config.Proxy; p != "" {
//set default log level
client.Logger.Info = true
//outbound proxy
if p := c.Proxy; p != "" {
client.proxyURL, err = url.Parse(p)
if err != nil {
return nil, fmt.Errorf("Invalid proxy URL (%s)", err)
}
}
user, pass := chshare.ParseAuth(config.Auth)
//ssh auth and config
user, pass := settings.ParseAuth(c.Auth)
client.sshConfig = &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.Password(pass)},
@@ -118,15 +126,13 @@ func NewClient(config *Config) (*Client, error) {
HostKeyCallback: client.verifyServer,
Timeout: 30 * time.Second,
}
if createSocksServer {
socksConfig := &socks5.Config{}
client.socksServer, err = socks5.New(socksConfig)
if err != nil {
return nil, err
}
}
//prepare client tunnel
client.tunnel = tunnel.New(tunnel.Config{
Logger: client.Logger,
Inbound: true, //client always accepts inbound
Outbound: hasReverse,
Socks: hasReverse && hasSocks,
})
return client, nil
}
@@ -142,7 +148,7 @@ func (c *Client) Run() error {
func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error {
expect := c.config.Fingerprint
got := chshare.FingerprintKey(key)
got := ccrypto.FingerprintKey(key)
if expect != "" && !strings.HasPrefix(got, expect) {
return fmt.Errorf("Invalid fingerprint (%s)", got)
}
@@ -153,49 +159,38 @@ func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKe
//Start client and does not block
func (c *Client) Start(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
c.stop = cancel
eg, ctx := errgroup.WithContext(ctx)
c.eg = eg
via := ""
if c.proxyURL != nil {
via = " via " + c.proxyURL.String()
}
//prepare non-reverse proxies
for i, r := range c.config.shared.Remotes {
if !r.Reverse {
proxy := chshare.NewTCPProxy(c.Logger, func() ssh.Conn { return c.sshConn }, i, r)
if err := proxy.Start(ctx); err != nil {
return err
}
}
}
c.Infof("Connecting to %s%s\n", c.server, via)
//optional keepalive loop
if c.config.KeepAlive > 0 {
go c.keepAliveLoop()
}
//connection loop
go c.connectionLoop()
//connect chisel server
eg.Go(func() error {
return c.connectionLoop(ctx)
})
//listen sockets
eg.Go(func() error {
clientInbound := c.computed.Remotes.Reversed(false)
return c.tunnel.BindRemotes(ctx, clientInbound)
})
return nil
}
func (c *Client) keepAliveLoop() {
for c.running {
time.Sleep(c.config.KeepAlive)
if c.sshConn != nil {
c.sshConn.SendRequest("ping", true, nil)
}
}
}
func (c *Client) connectionLoop() {
func (c *Client) connectionLoop(ctx context.Context) error {
//connection loop!
var connerr error
b := &backoff.Backoff{Max: c.config.MaxRetryInterval}
for c.running {
if connerr != nil {
attempt := int(b.Attempt())
maxAttempt := c.config.MaxRetryCount
d := b.Duration()
for {
retry, err := c.connectionOnce(ctx)
//connection error
attempt := int(b.Attempt())
maxAttempt := c.config.MaxRetryCount
if err != nil {
//show error and attempt counts
msg := fmt.Sprintf("Connection error: %s", connerr)
msg := fmt.Sprintf("Connection error: %s", err)
if attempt > 0 {
msg += fmt.Sprintf(" (Attempt: %d", attempt)
if maxAttempt > 0 {
@@ -204,136 +199,136 @@ func (c *Client) connectionLoop() {
msg += ")"
}
c.Debugf(msg)
//give up?
if maxAttempt >= 0 && attempt >= maxAttempt {
break
}
c.Infof("Retrying in %s...", d)
connerr = nil
chshare.SleepSignal(d)
}
d := websocket.Dialer{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
HandshakeTimeout: 45 * time.Second,
Subprotocols: []string{chshare.ProtocolVersion},
NetDialContext: c.config.DialContext,
}
//optionally proxy
if c.proxyURL != nil {
if strings.HasPrefix(c.proxyURL.Scheme, "socks") {
// SOCKS5 proxy
if c.proxyURL.Scheme != "socks" && c.proxyURL.Scheme != "socks5h" {
c.Infof(
"unsupported socks proxy type: %s:// (only socks5h:// or socks:// is supported)",
c.proxyURL.Scheme)
break
}
var auth *proxy.Auth
if c.proxyURL.User != nil {
pass, _ := c.proxyURL.User.Password()
auth = &proxy.Auth{
User: c.proxyURL.User.Username(),
Password: pass,
}
}
socksDialer, err := proxy.SOCKS5("tcp", c.proxyURL.Host, auth, proxy.Direct)
if err != nil {
connerr = err
continue
}
d.NetDial = socksDialer.Dial
} else {
// CONNECT proxy
d.Proxy = func(*http.Request) (*url.URL, error) {
return c.proxyURL, nil
}
}
}
wsConn, _, err := d.Dial(c.server, c.config.Headers)
if err != nil {
connerr = err
continue
}
conn := chshare.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
c.Debugf("Handshaking...")
sshConn, chans, reqs, err := ssh.NewClientConn(conn, "", c.sshConfig)
if err != nil {
if strings.Contains(err.Error(), "unable to authenticate") {
c.Infof("Authentication failed")
c.Debugf(err.Error())
} else {
c.Infof(err.Error())
}
//give up?
if !retry || (maxAttempt >= 0 && attempt >= maxAttempt) {
break
}
c.config.shared.Version = chshare.BuildVersion
conf, _ := chshare.EncodeConfig(c.config.shared)
c.Debugf("Sending config")
t0 := time.Now()
_, configerr, err := sshConn.SendRequest("config", true, conf)
if err != nil {
c.Infof("Config verification failed")
break
d := b.Duration()
c.Infof("Retrying in %s...", d)
select {
case <-cos.AfterSignal(d):
continue //retry now
case <-ctx.Done():
c.Infof("Cancelled")
return nil
}
if len(configerr) > 0 {
c.Infof(string(configerr))
break
}
c.Infof("Connected (Latency %s)", time.Since(t0))
//connected
b.Reset()
c.sshConn = sshConn
go ssh.DiscardRequests(reqs)
go c.connectStreams(chans)
err = sshConn.Wait()
//disconnected
c.sshConn = nil
if err != nil && err != io.EOF {
connerr = err
continue
}
c.Infof("Disconnected\n")
}
close(c.runningc)
c.Close()
return nil
}
//connectionOnce returning nil error means retry
func (c *Client) connectionOnce(ctx context.Context) (retry bool, err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
select {
case <-ctx.Done():
return false, io.EOF
default:
//still open
}
//prepare dialer
d := websocket.Dialer{
HandshakeTimeout: 45 * time.Second,
Subprotocols: []string{chshare.ProtocolVersion},
}
//optional proxy
if p := c.proxyURL; p != nil {
if err := c.setProxy(p, &d); err != nil {
return false, err
}
}
wsConn, _, err := d.DialContext(ctx, c.server, c.config.Headers)
if err != nil {
return true, err
}
conn := cnet.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
c.Debugf("Handshaking...")
sshConn, chans, reqs, err := ssh.NewClientConn(conn, "", c.sshConfig)
if err != nil {
if strings.Contains(err.Error(), "unable to authenticate") {
c.Infof("Authentication failed")
c.Debugf(err.Error())
retry = false
} else if n, ok := err.(net.Error); ok && !n.Temporary() {
c.Infof(err.Error())
retry = false
} else {
c.Infof("retriable: %s", err.Error())
retry = true
}
return retry, err
}
defer sshConn.Close()
// chisel client handshake (reverse of server handshake)
// send configuration
c.Debugf("Sending config")
t0 := time.Now()
_, configerr, err := sshConn.SendRequest(
"config",
true,
settings.EncodeConfig(c.computed),
)
if err != nil {
c.Infof("Config verification failed")
return false, err
}
if len(configerr) > 0 {
return false, errors.New(string(configerr))
}
c.Infof("Connected (Latency %s)", time.Since(t0))
defer c.Infof("Disconnected")
//connected, handover ssh connection for tunnel to use, and block
retry = true
err = c.tunnel.BindSSH(ctx, sshConn, reqs, chans)
if n, ok := err.(net.Error); ok && !n.Temporary() {
retry = false
}
return retry, err
}
func (c *Client) setProxy(u *url.URL, d *websocket.Dialer) error {
// CONNECT proxy
if !strings.HasPrefix(u.Scheme, "socks") {
d.Proxy = func(*http.Request) (*url.URL, error) {
return u, nil
}
return nil
}
// SOCKS5 proxy
if u.Scheme != "socks" && u.Scheme != "socks5h" {
return fmt.Errorf(
"unsupported socks proxy type: %s:// (only socks5h:// or socks:// is supported)",
u.Scheme,
)
}
var auth *proxy.Auth
if u.User != nil {
pass, _ := u.User.Password()
auth = &proxy.Auth{
User: u.User.Username(),
Password: pass,
}
}
socksDialer, err := proxy.SOCKS5("tcp", u.Host, auth, proxy.Direct)
if err != nil {
return err
}
d.NetDial = socksDialer.Dial
return nil
}
//Wait blocks while the client is running.
//Can only be called once.
func (c *Client) Wait() error {
return <-c.runningc
return c.eg.Wait()
}
//Close manually stops the client
func (c *Client) Close() error {
c.running = false
if c.sshConn == nil {
return nil
}
return c.sshConn.Close()
}
func (c *Client) connectStreams(chans <-chan ssh.NewChannel) {
for ch := range chans {
remote := string(ch.ExtraData())
socks := remote == "socks"
if socks && c.socksServer == nil {
c.Debugf("Denied socks request, please enable client socks remote.")
ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled on the client")
continue
}
stream, reqs, err := ch.Accept()
if err != nil {
c.Debugf("Failed to accept stream: %s", err)
continue
}
go ssh.DiscardRequests(reqs)
l := c.Logger.Fork("conn#%d", c.connStats.New())
if socks {
go chshare.HandleSocksStream(l, c.socksServer, &c.connStats, stream)
} else {
go chshare.HandleTCPStream(l, &c.connStats, stream, remote)
}
if c.stop != nil {
c.stop()
}
return nil
}
+11 -8
View File
@@ -4,35 +4,38 @@ import (
"log"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
func TestCustomHeaders(t *testing.T) {
//fake server
wg := sync.WaitGroup{}
wg.Add(1)
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("Foo") != "Bar" {
t.Fatal("expected header Foo to be 'Bar'")
}
wg.Done()
}))
// Close the server when test finishes
defer server.Close()
//client
headers := http.Header{}
headers.Set("Foo", "Bar")
config := Config{
Fingerprint: "",
Auth: "",
KeepAlive: time.Second,
MaxRetryCount: 0,
MaxRetryInterval: time.Second,
Server: server.URL,
Remotes: []string{"socks"},
Remotes: []string{"9000"},
Headers: headers,
}
c, err := NewClient(&config)
if err != nil {
log.Fatal(err)
}
if err = c.Run(); err != nil {
log.Fatal(err)
}
go c.Run()
//wait for test to complete
wg.Wait()
c.Close()
}
+4 -3
View File
@@ -12,7 +12,8 @@ require (
github.com/jpillora/requestlog v1.0.0
github.com/jpillora/sizestr v1.0.0
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/net v0.0.0-20200707034311-ab3426394381
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
)
+9 -6
View File
@@ -17,15 +17,18 @@ github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mz
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+40 -27
View File
@@ -10,16 +10,18 @@ import (
"runtime"
"strconv"
"strings"
"time"
chclient "github.com/jpillora/chisel/client"
chserver "github.com/jpillora/chisel/server"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/cos"
)
var help = `
Usage: chisel [command] [--help]
Version: ` + chshare.BuildVersion + `
Version: ` + chshare.BuildVersion + ` (` + runtime.Version() + `)
Commands:
server - runs chisel in server mode
@@ -120,8 +122,15 @@ var serverHelp = `
remotes. This file will be automatically reloaded on change.
--auth, An optional string representing a single user with full
access, in the form of <user:pass>. This is equivalent to creating an
authfile with {"<user:pass>": [""]}.
access, in the form of <user:pass>. It is equivalent to creating an
authfile with {"<user:pass>": [""]}. If unset, it will use the
environment variable AUTH.
--keepalive, An optional keepalive interval. Since the underlying
transport is HTTP, in many instances we'll be traversing through
proxies, often these proxies will close idle connections. You must
specify a time with a unit, for example '5s' or '2m'. Defaults
to '25s' (set to 0s to disable).
--proxy, Specifies another HTTP server to proxy requests to when
chisel receives a normal HTTP request. Useful for hiding chisel in
@@ -138,15 +147,18 @@ func server(args []string) {
flags := flag.NewFlagSet("server", flag.ContinueOnError)
config := &chserver.Config{}
flags.StringVar(&config.KeySeed, "key", "", "")
flags.StringVar(&config.AuthFile, "authfile", "", "")
flags.StringVar(&config.Auth, "auth", "", "")
flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
flags.StringVar(&config.Proxy, "proxy", "", "")
flags.BoolVar(&config.Socks5, "socks5", false, "")
flags.BoolVar(&config.Reverse, "reverse", false, "")
host := flags.String("host", "", "")
p := flags.String("p", "", "")
port := flags.String("port", "", "")
key := flags.String("key", "", "")
authfile := flags.String("authfile", "", "")
auth := flags.String("auth", "", "")
proxy := flags.String("proxy", "", "")
socks5 := flags.Bool("socks5", false, "")
reverse := flags.Bool("reverse", false, "")
pid := flags.Bool("pid", false, "")
verbose := flags.Bool("v", false, "")
@@ -171,17 +183,10 @@ func server(args []string) {
if *port == "" {
*port = "8080"
}
if *key == "" {
*key = os.Getenv("CHISEL_KEY")
if config.KeySeed == "" {
config.KeySeed = os.Getenv("CHISEL_KEY")
}
s, err := chserver.NewServer(&chserver.Config{
KeySeed: *key,
AuthFile: *authfile,
Auth: *auth,
Proxy: *proxy,
Socks5: *socks5,
Reverse: *reverse,
})
s, err := chserver.NewServer(config)
if err != nil {
log.Fatal(err)
}
@@ -189,10 +194,14 @@ func server(args []string) {
if *pid {
generatePidFile()
}
go chshare.GoStats()
if err = s.Run(*host, *port); err != nil {
go cos.GoStats()
ctx := cos.InterruptContext()
if err := s.StartContext(ctx, *host, *port); err != nil {
log.Fatal(err)
}
if err := s.Wait(); err != nil {
log.Fatal()
}
}
type headerFlags struct {
@@ -293,8 +302,8 @@ var clientHelp = `
--keepalive, An optional keepalive interval. Since the underlying
transport is HTTP, in many instances we'll be traversing through
proxies, often these proxies will close idle connections. You must
specify a time with a unit, for example '30s' or '2m'. Defaults
to '0s' (disabled).
specify a time with a unit, for example '5s' or '2m'. Defaults
to '25s' (set to 0s to disable).
--max-retry-count, Maximum number of times to retry before exiting.
Defaults to unlimited.
@@ -306,7 +315,7 @@ var clientHelp = `
used to reach the chisel server. Authentication can be specified
inside the URL.
For example, http://admin:password@my-server.com:8081
or: socks://admin:password@my-server.com:1080
or: socks://admin:password@my-server.com:1080
--header, Set a custom header in the form "HeaderName: HeaderContent".
Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World")
@@ -320,7 +329,7 @@ func client(args []string) {
config := chclient.Config{Headers: http.Header{}}
flags.StringVar(&config.Fingerprint, "fingerprint", "", "")
flags.StringVar(&config.Auth, "auth", "", "")
flags.DurationVar(&config.KeepAlive, "keepalive", 0, "")
flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
flags.IntVar(&config.MaxRetryCount, "max-retry-count", -1, "")
flags.DurationVar(&config.MaxRetryInterval, "max-retry-interval", 0, "")
flags.StringVar(&config.Proxy, "proxy", "", "")
@@ -357,8 +366,12 @@ func client(args []string) {
if *pid {
generatePidFile()
}
go chshare.GoStats()
if err = c.Run(); err != nil {
go cos.GoStats()
ctx := cos.InterruptContext()
if err := c.Start(ctx); err != nil {
log.Fatal(err)
}
if err := c.Wait(); err != nil {
log.Fatal()
}
}
+66 -70
View File
@@ -1,139 +1,135 @@
package chserver
import (
"context"
"errors"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"time"
socks5 "github.com/armon/go-socks5"
"github.com/gorilla/websocket"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/ccrypto"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/requestlog"
"golang.org/x/crypto/ssh"
)
// Config is the configuration for the chisel service
type Config struct {
KeySeed string
AuthFile string
Auth string
Proxy string
Socks5 bool
Reverse bool
KeySeed string
AuthFile string
Auth string
Proxy string
Socks5 bool
Reverse bool
KeepAlive time.Duration
}
// Server respresent a chisel service
type Server struct {
*chshare.Logger
connStats chshare.ConnStats
*cio.Logger
config *Config
fingerprint string
httpServer *chshare.HTTPServer
httpServer *cnet.HTTPServer
reverseProxy *httputil.ReverseProxy
sessCount int32
sessions *chshare.Users
socksServer *socks5.Server
sessions *settings.Users
sshConfig *ssh.ServerConfig
users *chshare.UserIndex
reverseOk bool
users *settings.UserIndex
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
CheckOrigin: func(r *http.Request) bool { return true },
}
// NewServer creates and returns a new chisel server
func NewServer(config *Config) (*Server, error) {
s := &Server{
httpServer: chshare.NewHTTPServer(),
Logger: chshare.NewLogger("server"),
sessions: chshare.NewUsers(),
reverseOk: config.Reverse,
func NewServer(c *Config) (*Server, error) {
server := &Server{
config: c,
httpServer: cnet.NewHTTPServer(),
Logger: cio.NewLogger("server"),
sessions: settings.NewUsers(),
}
s.Info = true
s.users = chshare.NewUserIndex(s.Logger)
if config.AuthFile != "" {
if err := s.users.LoadUsers(config.AuthFile); err != nil {
server.Info = true
server.users = settings.NewUserIndex(server.Logger)
if c.AuthFile != "" {
if err := server.users.LoadUsers(c.AuthFile); err != nil {
return nil, err
}
}
if config.Auth != "" {
u := &chshare.User{Addrs: []*regexp.Regexp{chshare.UserAllowAll}}
u.Name, u.Pass = chshare.ParseAuth(config.Auth)
if c.Auth != "" {
u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}
u.Name, u.Pass = settings.ParseAuth(c.Auth)
if u.Name != "" {
s.users.AddUser(u)
server.users.AddUser(u)
}
}
//generate private key (optionally using seed)
key, _ := chshare.GenerateKey(config.KeySeed)
key, err := ccrypto.GenerateKey(c.KeySeed)
if err != nil {
log.Fatal("Failed to generate key")
}
//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())
server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
//create ssh config
s.sshConfig = &ssh.ServerConfig{
server.sshConfig = &ssh.ServerConfig{
ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server",
PasswordCallback: s.authUser,
PasswordCallback: server.authUser,
}
s.sshConfig.AddHostKey(private)
server.sshConfig.AddHostKey(private)
//setup reverse proxy
if config.Proxy != "" {
u, err := url.Parse(config.Proxy)
if c.Proxy != "" {
u, err := url.Parse(c.Proxy)
if err != nil {
return nil, err
}
if u.Host == "" {
return nil, s.Errorf("Missing protocol (%s)", u)
return nil, server.Errorf("Missing protocol (%s)", u)
}
s.reverseProxy = httputil.NewSingleHostReverseProxy(u)
server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
//always use proxy host
s.reverseProxy.Director = func(r *http.Request) {
server.reverseProxy.Director = func(r *http.Request) {
//enforce origin, keep path
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
r.Host = u.Host
}
}
//setup socks server (not listening on any port!)
if config.Socks5 {
socksConfig := &socks5.Config{}
if s.Debug {
socksConfig.Logger = log.New(os.Stdout, "[socks]", log.Ldate|log.Ltime)
} else {
socksConfig.Logger = log.New(ioutil.Discard, "", 0)
}
s.socksServer, err = socks5.New(socksConfig)
if err != nil {
return nil, err
}
s.Infof("SOCKS5 server enabled")
}
//print when reverse tunnelling is enabled
if config.Reverse {
s.Infof("Reverse tunnelling enabled")
if c.Reverse {
server.Infof("Reverse tunnelling enabled")
}
return s, nil
return server, nil
}
// Run is responsible for starting the chisel service
// Run is responsible for starting the chisel service.
// Internally this calls Start then Wait.
func (s *Server) Run(host, port string) error {
if err := s.Start(host, port); err != nil {
return err
}
return s.Wait()
}
// Start is responsible for kicking off the http server
func (s *Server) Start(host, port string) error {
return s.StartContext(nil, host, port)
}
// StartContext is responsible for kicking off the http server,
// and can be closed by cancelling the provided context
func (s *Server) StartContext(ctx context.Context, host, port string) error {
s.Infof("Fingerprint %s", s.fingerprint)
if s.users.Len() > 0 {
s.Infof("User authenication enabled")
@@ -148,7 +144,7 @@ func (s *Server) Start(host, port string) error {
o.TrustProxy = true
h = requestlog.WrapWith(h, o)
}
return s.httpServer.GoListenAndServe(host+":"+port, h)
return s.httpServer.GoListenAndServeContext(ctx, host+":"+port, h)
}
// Wait waits for the http server to close
@@ -180,26 +176,26 @@ func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions
return nil, errors.New("Invalid authentication for username: %s")
}
// insert the user session map
// @note: this should probably have a lock on it given the map isn't thread-safe??
// TODO this should probably have a lock on it given the map isn't thread-safe
s.sessions.Set(string(c.SessionID()), user)
return nil, nil
}
// AddUser adds a new user into the server user index
func (s *Server) AddUser(user, pass string, addrs ...string) error {
authorizedAddrs := make([]*regexp.Regexp, 0)
authorizedAddrs := []*regexp.Regexp{}
for _, addr := range addrs {
authorizedAddr, err := regexp.Compile(addr)
if err != nil {
return err
}
authorizedAddrs = append(authorizedAddrs, authorizedAddr)
}
u := &chshare.User{Name: user, Pass: pass, Addrs: authorizedAddrs}
s.users.AddUser(u)
s.users.AddUser(&settings.User{
Name: user,
Pass: pass,
Addrs: authorizedAddrs,
})
return nil
}
+49 -77
View File
@@ -1,14 +1,17 @@
package chserver
import (
"context"
"net/http"
"strings"
"sync/atomic"
"time"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/chisel/share/tunnel"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
)
// handleClientHandler is the main http websocket handler for the chisel server
@@ -47,46 +50,52 @@ func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {
// handleWebsocket is responsible for handling the websocket connection
func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
id := atomic.AddInt32(&s.sessCount, 1)
clog := s.Fork("session#%d", id)
l := s.Fork("session#%d", id)
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
clog.Debugf("Failed to upgrade (%s)", err)
l.Debugf("Failed to upgrade (%s)", err)
return
}
conn := chshare.NewWebSocketConn(wsConn)
conn := cnet.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
clog.Debugf("Handshaking...")
l.Debugf("Handshaking...")
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
if err != nil {
s.Debugf("Failed to handshake (%s)", err)
return
}
// pull the users from the session map
var user *chshare.User
var user *settings.User
if s.users.Len() > 0 {
sid := string(sshConn.SessionID())
user, _ = s.sessions.Get(sid)
u, ok := s.sessions.Get(sid)
if !ok {
panic("bug in ssh auth handler")
}
user = u
s.sessions.Del(sid)
}
//verify configuration
clog.Debugf("Verifying configuration")
//wait for request, with timeout
// chisel server handshake (reverse of client handshake)
// verify configuration
l.Debugf("Verifying configuration")
// wait for request, with timeout
var r *ssh.Request
select {
case r = <-reqs:
case <-time.After(10 * time.Second):
l.Debugf("Timeout waiting for configuration")
sshConn.Close()
return
}
failed := func(err error) {
clog.Debugf("Failed: %s", err)
l.Debugf("Failed: %s", err)
r.Reply(false, []byte(err.Error()))
}
if r.Type != "config" {
failed(s.Errorf("expecting config request"))
return
}
c, err := chshare.DecodeConfig(r.Payload)
c, err := settings.DecodeConfig(r.Payload)
if err != nil {
failed(s.Errorf("invalid config"))
return
@@ -97,13 +106,13 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
if v == "" {
v = "<unknown>"
}
clog.Infof("Client version (%s) differs from server version (%s)",
l.Infof("Client version (%s) differs from server version (%s)",
v, chshare.BuildVersion)
}
//confirm reverse tunnels are allowed
for _, r := range c.Remotes {
if r.Reverse && !s.reverseOk {
clog.Debugf("Denied reverse port forwarding request, please enable --reverse")
if r.Reverse && !s.config.Reverse {
l.Debugf("Denied reverse port forwarding request, please enable --reverse")
failed(s.Errorf("Reverse port forwaring not enabled on server"))
return
}
@@ -112,74 +121,37 @@ func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
//access to the desired remotes
if user != nil {
for _, r := range c.Remotes {
var addr string
if r.Reverse {
addr = "R:" + r.LocalHost + ":" + r.LocalPort
} else {
addr = r.RemoteHost + ":" + r.RemotePort
}
addr := r.UserAddr()
if !user.HasAccess(addr) {
failed(s.Errorf("access to '%s' denied", addr))
return
}
}
}
//set up reverse port forwarding
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for i, r := range c.Remotes {
if r.Reverse {
proxy := chshare.NewTCPProxy(s.Logger, func() ssh.Conn { return sshConn }, i, r)
if err := proxy.Start(ctx); err != nil {
failed(s.Errorf("%s", err))
return
}
}
}
//success!
//successfuly validated config!
r.Reply(true, nil)
//prepare connection logger
clog.Debugf("Open")
go s.handleSSHRequests(clog, reqs)
go s.handleSSHChannels(clog, chans)
sshConn.Wait()
clog.Debugf("Close")
}
func (s *Server) handleSSHRequests(clientLog *chshare.Logger, reqs <-chan *ssh.Request) {
for r := range reqs {
switch r.Type {
case "ping":
r.Reply(true, nil)
default:
clientLog.Debugf("Unknown request: %s", r.Type)
}
}
}
func (s *Server) handleSSHChannels(clientLog *chshare.Logger, chans <-chan ssh.NewChannel) {
for ch := range chans {
remote := string(ch.ExtraData())
socks := remote == "socks"
//dont accept socks when --socks5 isn't enabled
if socks && s.socksServer == nil {
clientLog.Debugf("Denied socks request, please enable --socks5")
ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled on the server")
continue
}
//accept rest
stream, reqs, err := ch.Accept()
if err != nil {
clientLog.Debugf("Failed to accept stream: %s", err)
continue
}
go ssh.DiscardRequests(reqs)
//handle stream type
connID := s.connStats.New()
if socks {
go chshare.HandleSocksStream(clientLog.Fork("socksconn#%d", connID), s.socksServer, &s.connStats, stream)
} else {
go chshare.HandleTCPStream(clientLog.Fork("conn#%d", connID), &s.connStats, stream, remote)
}
//tunnel per ssh connection
tunnel := tunnel.New(tunnel.Config{
Logger: l,
Inbound: s.config.Reverse,
Outbound: true, //server always accepts outbound
Socks: s.config.Socks5,
KeepAlive: s.config.KeepAlive,
})
//bind
eg, ctx := errgroup.WithContext(req.Context())
eg.Go(func() error {
//connected, handover ssh connection for tunnel to use, and block
return tunnel.BindSSH(ctx, sshConn, reqs, chans)
})
eg.Go(func() error {
serverInbound := c.Remotes.Reversed(true)
return tunnel.BindRemotes(ctx, serverInbound)
})
err = eg.Wait()
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
l.Debugf("Closed connection (%s)", err)
} else {
l.Debugf("Closed connection")
}
}
@@ -1,4 +1,4 @@
package chshare
package ccrypto
// Deterministic crypto.Reader
// overview: half the result is used as the output
@@ -18,17 +18,17 @@ func NewDetermRand(seed []byte) io.Reader {
for i := 0; i < DetermRandIter; i++ {
next, out = hash(next)
}
return &DetermRand{
return &determRand{
next: next,
out: out,
}
}
type DetermRand struct {
type determRand struct {
next, out []byte
}
func (d *DetermRand) Read(b []byte) (int, error) {
func (d *determRand) Read(b []byte) (int, error) {
n := 0
l := len(b)
for n < l {
+41
View File
@@ -0,0 +1,41 @@
package ccrypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"golang.org/x/crypto/ssh"
)
//GenerateKey for use as an SSH private key
func GenerateKey(seed string) ([]byte, error) {
r := rand.Reader
if seed != "" {
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
}
//FingerprintKey calculates the MD5 of an SSH public key
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, ":")
}
+1 -1
View File
@@ -1,4 +1,4 @@
package chshare
package cio
import (
"fmt"
+48
View File
@@ -0,0 +1,48 @@
package cio
import (
"io"
"log"
"sync"
)
func Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) {
var sent, received int64
var wg sync.WaitGroup
var o sync.Once
close := func() {
src.Close()
dst.Close()
}
wg.Add(2)
go func() {
received, _ = io.Copy(src, pipeVis("send", dst))
o.Do(close)
wg.Done()
}()
go func() {
sent, _ = io.Copy(dst, pipeVis("recv", src))
o.Do(close)
wg.Done()
}()
wg.Wait()
return sent, received
}
const vis = false
type pipeVisPrinter struct {
name string
}
func (p pipeVisPrinter) Write(b []byte) (int, error) {
log.Printf(">>> %s: %x", p.name, b)
return len(b), nil
}
func pipeVis(name string, r io.Reader) io.Reader {
if vis {
return io.TeeReader(r, pipeVisPrinter{name})
}
return r
}
+1 -1
View File
@@ -1,4 +1,4 @@
package chshare
package cio
import (
"io"
+1 -1
View File
@@ -1,4 +1,4 @@
package chshare
package cnet
import (
"io"
+5 -10
View File
@@ -1,7 +1,6 @@
package chshare
package cnet
import (
"log"
"net"
"time"
@@ -26,17 +25,13 @@ func (c *wsConn) Read(dst []byte) (int, error) {
ldst := len(dst)
//use buffer or read new message
var src []byte
if l := len(c.buff); l > 0 {
if len(c.buff) > 0 {
src = c.buff
c.buff = nil
} else {
t, msg, err := c.Conn.ReadMessage()
if err != nil {
return 0, err
} else if t != websocket.BinaryMessage {
log.Printf("<WARNING> non-binary msg")
}
} else if _, msg, err := c.Conn.ReadMessage(); err == nil {
src = msg
} else {
return 0, err
}
//copy src->dest
var n int
@@ -1,4 +1,4 @@
package chshare
package cnet
import (
"fmt"
+71
View File
@@ -0,0 +1,71 @@
package cnet
import (
"context"
"errors"
"net"
"net/http"
"golang.org/x/sync/errgroup"
)
//HTTPServer extends net/http Server and
//adds graceful shutdowns
type HTTPServer struct {
*http.Server
serving bool
waiter *errgroup.Group
listenErr error
}
//NewHTTPServer creates a new HTTPServer
func NewHTTPServer() *HTTPServer {
return &HTTPServer{
Server: &http.Server{},
serving: false,
}
}
func (h *HTTPServer) GoListenAndServe(addr string, handler http.Handler) error {
return h.GoListenAndServeContext(context.Background(), addr, handler)
}
func (h *HTTPServer) GoListenAndServeContext(ctx context.Context, addr string, handler http.Handler) error {
if ctx == nil {
return errors.New("ctx must be set")
}
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
h.Handler = handler
h.serving = true
h.waiter, ctx = errgroup.WithContext(ctx)
h.waiter.Go(func() error {
return h.Serve(l)
})
go func() {
<-ctx.Done()
h.Close()
}()
return nil
}
func (h *HTTPServer) Close() error {
if !h.serving {
return errors.New("not started yet")
}
return h.Server.Close()
}
func (h *HTTPServer) Wait() error {
if !h.serving {
return errors.New("not started yet")
}
err := h.waiter.Wait()
if err == http.ErrServerClosed {
err = nil //success
}
return err
}
+59
View File
@@ -0,0 +1,59 @@
package chshare
//this file exists to maintain backwards compatibility
import (
"github.com/jpillora/chisel/share/ccrypto"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/cos"
"github.com/jpillora/chisel/share/settings"
)
const (
DetermRandIter = ccrypto.DetermRandIter
)
type (
Config = settings.Config
Remote = settings.Remote
Remotes = settings.Remotes
User = settings.User
Users = settings.Users
UserIndex = settings.UserIndex
HTTPServer = cnet.HTTPServer
ConnStats = cnet.ConnStats
Logger = cio.Logger
)
var (
NewDetermRand = ccrypto.NewDetermRand
GenerateKey = ccrypto.GenerateKey
FingerprintKey = ccrypto.FingerprintKey
Pipe = cio.Pipe
NewLoggerFlag = cio.NewLoggerFlag
NewLogger = cio.NewLogger
Stdio = cio.Stdio
DecodeConfig = settings.DecodeConfig
DecodeRemote = settings.DecodeRemote
NewUsers = settings.NewUsers
NewUserIndex = settings.NewUserIndex
UserAllowAll = settings.UserAllowAll
ParseAuth = settings.ParseAuth
NewRWCConn = cnet.NewRWCConn
NewWebSocketConn = cnet.NewWebSocketConn
NewHTTPServer = cnet.NewHTTPServer
GoStats = cos.GoStats
SleepSignal = cos.SleepSignal
)
//EncodeConfig old version
func EncodeConfig(c *settings.Config) ([]byte, error) {
return settings.EncodeConfig(*c), nil
}
// //TCPProxy makes this package backward compatible
// type TCPProxy = Proxy
// //NewTCPProxy makes this package backward compatible
// var NewTCPProxy = NewProxy
+1 -1
View File
@@ -1,4 +1,4 @@
package chshare
package cos
import (
"log"
+21
View File
@@ -0,0 +1,21 @@
package cos
import (
"context"
"os"
"os/signal"
)
//InterruptContext returns a context which is
//cancelled on OS Interrupt
func InterruptContext() context.Context {
ctx, cancel := context.WithCancel(context.Background())
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
signal.Stop(sig)
cancel()
}()
return ctx
}
+33
View File
@@ -0,0 +1,33 @@
//+build !windows
package cos
import (
"os"
"os/signal"
"syscall"
"time"
)
//SleepSignal sleeps for the given duration,
//or until a SIGHUP is received
func SleepSignal(d time.Duration) {
<-AfterSignal(d)
}
//AfterSignal returns a channel which will be closed
//after the given duration or until a SIGHUP is received
func AfterSignal(d time.Duration) <-chan struct{} {
ch := make(chan struct{})
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
select {
case <-time.After(d):
case <-sig:
}
signal.Stop(sig)
close(ch)
}()
return ch
}
@@ -1,6 +1,6 @@
//+build windows
package chshare
package cos
import "time"
-61
View File
@@ -1,61 +0,0 @@
package chshare
import (
"errors"
"net"
"net/http"
"sync"
)
//HTTPServer extends net/http Server and
//adds graceful shutdowns
type HTTPServer struct {
*http.Server
listener net.Listener
running chan error
isRunning bool
closer sync.Once
}
//NewHTTPServer creates a new HTTPServer
func NewHTTPServer() *HTTPServer {
return &HTTPServer{
Server: &http.Server{},
listener: nil,
running: make(chan error, 1),
}
}
func (h *HTTPServer) GoListenAndServe(addr string, handler http.Handler) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
h.isRunning = true
h.Handler = handler
h.listener = l
go func() {
h.closeWith(h.Serve(l))
}()
return nil
}
func (h *HTTPServer) closeWith(err error) {
if !h.isRunning {
return
}
h.isRunning = false
h.running <- err
}
func (h *HTTPServer) Close() error {
h.closeWith(nil)
return h.listener.Close()
}
func (h *HTTPServer) Wait() error {
if !h.isRunning {
return errors.New("Already closed")
}
return <-h.running
}
-29
View File
@@ -1,29 +0,0 @@
package chshare
import (
"io"
"sync"
)
func Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) {
var sent, received int64
var wg sync.WaitGroup
var o sync.Once
close := func() {
src.Close()
dst.Close()
}
wg.Add(2)
go func() {
received, _ = io.Copy(src, dst)
o.Do(close)
wg.Done()
}()
go func() {
sent, _ = io.Copy(dst, src)
o.Do(close)
wg.Done()
}()
wg.Wait()
return sent, received
}
-106
View File
@@ -1,106 +0,0 @@
package chshare
import (
"context"
"fmt"
"io"
"net"
"github.com/jpillora/sizestr"
"golang.org/x/crypto/ssh"
)
type GetSSHConn func() ssh.Conn
type TCPProxy struct {
*Logger
ssh GetSSHConn
id int
count int
remote *Remote
}
func NewTCPProxy(logger *Logger, ssh GetSSHConn, index int, remote *Remote) *TCPProxy {
id := index + 1
return &TCPProxy{
Logger: logger.Fork("proxy#%d:%s", id, remote),
ssh: ssh,
id: id,
remote: remote,
}
}
func (p *TCPProxy) Start(ctx context.Context) error {
if p.remote.Stdio {
go p.listenStdio(ctx)
return nil
}
l, err := net.Listen("tcp4", p.remote.LocalHost+":"+p.remote.LocalPort)
if err != nil {
return fmt.Errorf("%s: %s", p.Logger.Prefix(), err)
}
go p.listenNet(ctx, l)
return nil
}
func (p *TCPProxy) listenStdio(ctx context.Context) {
for {
p.accept(Stdio)
select {
case <-ctx.Done():
return
default:
// the connection is not ready yet, keep waiting
}
}
}
func (p *TCPProxy) listenNet(ctx context.Context, l net.Listener) {
p.Infof("Listening")
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
l.Close()
p.Infof("Closed")
case <-done:
}
}()
for {
src, err := l.Accept()
if err != nil {
select {
case <-ctx.Done():
//listener closed
default:
p.Infof("Accept error: %s", err)
}
close(done)
return
}
go p.accept(src)
}
}
func (p *TCPProxy) accept(src io.ReadWriteCloser) {
defer src.Close()
p.count++
cid := p.count
l := p.Fork("conn#%d", cid)
l.Debugf("Open")
sshConn := p.ssh()
if sshConn == nil {
l.Debugf("No remote connection")
return
}
//ssh request for tcp connection for this proxy's remote
dst, reqs, err := sshConn.OpenChannel("chisel", []byte(p.remote.Remote()))
if err != nil {
l.Infof("Stream error: %s", err)
return
}
go ssh.DiscardRequests(reqs)
//then pipe
s, r := Pipe(src, dst)
l.Debugf("Close (sent %s received %s)", sizestr.ToString(s), sizestr.ToString(r))
}
-138
View File
@@ -1,138 +0,0 @@
package chshare
import (
"errors"
"net/url"
"regexp"
"strings"
)
// short-hand conversions
// 3000 ->
// local 127.0.0.1:3000
// remote 127.0.0.1:3000
// foobar.com:3000 ->
// local 127.0.0.1:3000
// remote foobar.com:3000
// 3000:google.com:80 ->
// local 127.0.0.1:3000
// remote google.com:80
// 192.168.0.1:3000:google.com:80 ->
// local 192.168.0.1:3000
// remote google.com:80
// 127.0.0.1:1080:socks
// local 127.0.0.1:1080
// remote socks
// stdio:example.com:22
// local stdio
// remote example.com:22
type Remote struct {
LocalHost, LocalPort, RemoteHost, RemotePort string
Socks, Reverse, Stdio bool
}
const revPrefix = "R:"
func DecodeRemote(s string) (*Remote, error) {
reverse := false
if strings.HasPrefix(s, revPrefix) {
s = strings.TrimPrefix(s, revPrefix)
reverse = true
}
parts := strings.Split(s, ":")
if len(parts) <= 0 || len(parts) >= 5 {
return nil, errors.New("Invalid remote")
}
r := &Remote{Reverse: reverse}
for i := len(parts) - 1; i >= 0; i-- {
p := parts[i]
//remote portion is socks?
if i == len(parts)-1 && p == "socks" {
r.Socks = true
continue
}
//local portion is stdio?
if i == 0 && p == "stdio" {
r.Stdio = true
continue
}
if isPort(p) {
if !r.Socks && r.RemotePort == "" {
r.RemotePort = p
r.LocalPort = p
} else {
r.LocalPort = p
}
continue
}
if !r.Socks && (r.RemotePort == "" && r.LocalPort == "") {
return nil, errors.New("Missing ports")
}
if !isHost(p) {
return nil, errors.New("Invalid host")
}
if !r.Socks && r.RemoteHost == "" {
r.RemoteHost = p
} else {
r.LocalHost = p
}
}
if r.LocalHost == "" {
if r.Socks {
r.LocalHost = "127.0.0.1"
} else {
r.LocalHost = "0.0.0.0"
}
}
if r.LocalPort == "" && r.Socks {
r.LocalPort = "1080"
}
if !r.Socks && r.RemoteHost == "" {
r.RemoteHost = "0.0.0.0"
}
if r.Stdio && r.Reverse {
return nil, errors.New("stdio cannot be reversed")
}
return r, nil
}
var isPortRegExp = regexp.MustCompile(`^\d+$`)
func isPort(s string) bool {
if !isPortRegExp.MatchString(s) {
return false
}
return true
}
func isHost(s string) bool {
_, err := url.Parse(s)
if err != nil {
return false
}
return true
}
//implement Stringer
func (r *Remote) String() string {
tag := ""
if r.Reverse {
tag = revPrefix
}
return tag + r.Local() + "=>" + r.Remote()
}
func (r *Remote) Local() string {
if r.Stdio {
return "stdio"
}
return r.LocalHost + ":" + r.LocalPort
}
func (r *Remote) Remote() string {
if r.Socks {
return "socks"
}
return r.RemoteHost + ":" + r.RemotePort
}
+6 -4
View File
@@ -1,4 +1,4 @@
package chshare
package settings
import (
"encoding/json"
@@ -7,7 +7,7 @@ import (
type Config struct {
Version string
Remotes []*Remote
Remotes
}
func DecodeConfig(b []byte) (*Config, error) {
@@ -19,6 +19,8 @@ func DecodeConfig(b []byte) (*Config, error) {
return c, nil
}
func EncodeConfig(c *Config) ([]byte, error) {
return json.Marshal(c)
func EncodeConfig(c Config) []byte {
//Config doesn't have types that can fail to marshal
b, _ := json.Marshal(c)
return b
}
+244
View File
@@ -0,0 +1,244 @@
package settings
import (
"errors"
"net/url"
"regexp"
"strconv"
"strings"
)
// short-hand conversions (see remote_test)
// 3000 ->
// local 127.0.0.1:3000
// remote 127.0.0.1:3000
// foobar.com:3000 ->
// local 127.0.0.1:3000
// remote foobar.com:3000
// 3000:google.com:80 ->
// local 127.0.0.1:3000
// remote google.com:80
// 192.168.0.1:3000:google.com:80 ->
// local 192.168.0.1:3000
// remote google.com:80
// 127.0.0.1:1080:socks
// local 127.0.0.1:1080
// remote socks
// stdio:example.com:22
// local stdio
// remote example.com:22
// 1.1.1.1:53/udp
// local 127.0.0.1:53/udp
// remote 1.1.1.1:53/udp
type Remote struct {
LocalHost, LocalPort, LocalProto string
RemoteHost, RemotePort, RemoteProto string
Socks, Reverse, Stdio bool
}
const revPrefix = "R:"
func DecodeRemote(s string) (*Remote, error) {
reverse := false
if strings.HasPrefix(s, revPrefix) {
s = strings.TrimPrefix(s, revPrefix)
reverse = true
}
parts := strings.Split(s, ":")
if len(parts) <= 0 || len(parts) >= 5 {
return nil, errors.New("Invalid remote")
}
r := &Remote{Reverse: reverse}
//parse from back to front, to set 'remote' fields first,
//then to set 'local' fields second (allows the 'remote' side
//to provide the defaults)
for i := len(parts) - 1; i >= 0; i-- {
p := parts[i]
//remote portion is socks?
if i == len(parts)-1 && p == "socks" {
r.Socks = true
continue
}
//local portion is stdio?
if i == 0 && p == "stdio" {
r.Stdio = true
continue
}
p, proto := L4Proto(p)
if proto != "" {
if r.RemotePort == "" {
r.RemoteProto = proto
} else if r.LocalProto == "" {
r.LocalProto = proto
}
}
if isPort(p) {
if !r.Socks && r.RemotePort == "" {
r.RemotePort = p
}
r.LocalPort = p
continue
}
if !r.Socks && (r.RemotePort == "" && r.LocalPort == "") {
return nil, errors.New("Missing ports")
}
if !isHost(p) {
return nil, errors.New("Invalid host")
}
if !r.Socks && r.RemoteHost == "" {
r.RemoteHost = p
} else {
r.LocalHost = p
}
}
//remote string parsed, apply defaults...
if r.Socks {
//socks defaults
if r.LocalHost == "" {
r.LocalHost = "127.0.0.1"
}
if r.LocalPort == "" {
r.LocalPort = "1080"
}
} else {
//non-socks defaults
if r.LocalHost == "" {
r.LocalHost = "0.0.0.0"
}
if r.RemoteHost == "" {
r.RemoteHost = "127.0.0.1"
}
}
if r.RemoteProto == "" {
r.RemoteProto = "tcp"
}
if r.LocalProto == "" {
r.LocalProto = r.RemoteProto
}
if r.LocalProto != r.RemoteProto {
//TODO support cross protocol
//tcp <-> udp, is faily straight forward
//udp <-> tcp, is trickier since udp is stateless and tcp is not
return nil, errors.New("currently, local and remote protocols must match")
}
if r.Stdio && r.Reverse {
return nil, errors.New("stdio cannot be reversed")
}
return r, nil
}
func isPort(s string) bool {
n, err := strconv.Atoi(s)
if err != nil {
return false
}
if n <= 0 || n > 65535 {
return false
}
return true
}
func isHost(s string) bool {
_, err := url.Parse(s)
if err != nil {
return false
}
return true
}
var l4Proto = regexp.MustCompile(`(?i)\/(tcp|udp)$`)
//L4Proto extacts the layer-4 protocol from the given string
func L4Proto(s string) (head, proto string) {
if l4Proto.MatchString(s) {
l := len(s)
return strings.ToLower(s[:l-4]), s[l-3:]
}
return s, ""
}
//implement Stringer
func (r Remote) String() string {
sb := strings.Builder{}
if r.Reverse {
sb.WriteString(revPrefix)
}
sb.WriteString(strings.TrimPrefix(r.Local(), "0.0.0.0:"))
sb.WriteString("=>")
sb.WriteString(strings.TrimPrefix(r.Remote(), "127.0.0.1:"))
if r.RemoteProto == "udp" {
sb.WriteString("/udp")
}
return sb.String()
}
//Encode remote to a string
func (r Remote) Encode() string {
if r.LocalPort == "" {
r.LocalPort = r.RemotePort
}
local := r.Local()
remote := r.Remote()
if r.RemoteProto == "udp" {
remote += "/udp"
}
if r.Reverse {
return "R:" + local + ":" + remote
}
return local + ":" + remote
}
//Local is the decodable local portion
func (r Remote) Local() string {
if r.Stdio {
return "stdio"
}
if r.LocalHost == "" {
r.LocalHost = "0.0.0.0"
}
return r.LocalHost + ":" + r.LocalPort
}
//Remote is the decodable remote portion
func (r Remote) Remote() string {
if r.Socks {
return "socks"
}
if r.RemoteHost == "" {
r.RemoteHost = "127.0.0.1"
}
return r.RemoteHost + ":" + r.RemotePort
}
//UserAddr is checked when checking if a
//user has access to a given remote
func (r Remote) UserAddr() string {
if r.Reverse {
return "R:" + r.LocalHost + ":" + r.LocalPort
}
return r.RemoteHost + ":" + r.RemotePort
}
type Remotes []*Remote
//Filter out forward reversed/non-reversed remotes
func (rs Remotes) Reversed(reverse bool) Remotes {
subset := Remotes{}
for _, r := range rs {
match := r.Reverse == reverse
if match {
subset = append(subset, r)
}
}
return subset
}
//Encode back into strings
func (rs Remotes) Encode() []string {
s := make([]string, len(rs))
for i, r := range rs {
s[i] = r.Encode()
}
return s
}
+108
View File
@@ -0,0 +1,108 @@
package settings
import (
"reflect"
"testing"
)
func TestRemoteDecode(t *testing.T) {
//test table
for i, test := range []struct {
Input string
Output Remote
Encoded string
}{
{
"3000",
Remote{
LocalPort: "3000",
RemoteHost: "127.0.0.1",
RemotePort: "3000",
},
"0.0.0.0:3000:127.0.0.1:3000",
},
{
"google.com:80",
Remote{
LocalPort: "80",
RemoteHost: "google.com",
RemotePort: "80",
},
"0.0.0.0:80:google.com:80",
},
{
"R:google.com:80",
Remote{
LocalPort: "80",
RemoteHost: "google.com",
RemotePort: "80",
Reverse: true,
},
"R:0.0.0.0:80:google.com:80",
},
{
"socks",
Remote{
LocalHost: "127.0.0.1",
LocalPort: "1080",
Socks: true,
},
"127.0.0.1:1080:socks",
},
{
"127.0.0.1:1081:socks",
Remote{
LocalHost: "127.0.0.1",
LocalPort: "1081",
Socks: true,
},
"127.0.0.1:1081:socks",
},
{
"1.1.1.1:53/udp",
Remote{
LocalPort: "53",
LocalProto: "udp",
RemoteHost: "1.1.1.1",
RemotePort: "53",
RemoteProto: "udp",
},
"0.0.0.0:53:1.1.1.1:53/udp",
},
{
"localhost:5353:1.1.1.1:53/udp",
Remote{
LocalHost: "localhost",
LocalPort: "5353",
LocalProto: "udp",
RemoteHost: "1.1.1.1",
RemotePort: "53",
RemoteProto: "udp",
},
"localhost:5353:1.1.1.1:53/udp",
},
} {
//expected defaults
expected := test.Output
if expected.LocalHost == "" {
expected.LocalHost = "0.0.0.0"
}
if expected.RemoteProto == "" {
expected.RemoteProto = "tcp"
}
if expected.LocalProto == "" {
expected.LocalProto = "tcp"
}
//compare
got, err := DecodeRemote(test.Input)
if err != nil {
t.Fatalf("decode #%d '%s' failed: %s", i+1, test.Input, err)
}
if !reflect.DeepEqual(got, &expected) {
t.Fatalf("decode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, expected, got)
}
if e := got.Encode(); test.Encoded != e {
t.Fatalf("encode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, test.Encoded, e)
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
package chshare
package settings
import (
"regexp"
+13 -5
View File
@@ -1,4 +1,4 @@
package chshare
package settings
import (
"encoding/json"
@@ -10,6 +10,7 @@ import (
"sync"
"github.com/fsnotify/fsnotify"
"github.com/jpillora/chisel/share/cio"
)
type Users struct {
@@ -58,13 +59,13 @@ func (u *Users) AddUser(user *User) {
// UserIndex is a reloadable user source
type UserIndex struct {
*Logger
*cio.Logger
*Users
configFile string
}
// NewUserIndex creates a source for users
func NewUserIndex(logger *Logger) *UserIndex {
func NewUserIndex(logger *cio.Logger) *UserIndex {
return &UserIndex{
Logger: logger.Fork("users"),
Users: NewUsers(),
@@ -125,6 +126,7 @@ func (u *UserIndex) loadUserIndex() error {
if err := json.Unmarshal(b, &raw); err != nil {
return errors.New("Invalid JSON: " + err.Error())
}
users := []*User{}
for auth, remotes := range raw {
user := &User{}
user.Name, user.Pass = ParseAuth(auth)
@@ -141,9 +143,15 @@ func (u *UserIndex) loadUserIndex() error {
}
user.Addrs = append(user.Addrs, re)
}
}
u.Users.AddUser(user)
users = append(users, user)
}
//swap
u.RWMutex.Lock()
u.inner = map[string]*User{}
u.RWMutex.Unlock()
for _, user := range users {
u.AddUser(user)
}
return nil
}
-24
View File
@@ -1,24 +0,0 @@
//+build !windows
package chshare
import (
"os"
"os/signal"
"syscall"
"time"
)
//SleepSignal sleeps for the given duration,
//or until a SIGHUP is received
func SleepSignal(d time.Duration) {
//during this time, also listen for SIGHUP
//(this uses 0xc to allow windows to compile)
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGHUP)
select {
case <-time.After(d):
case <-sig:
}
signal.Stop(sig)
}
-72
View File
@@ -1,72 +0,0 @@
package chshare
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"strings"
"github.com/armon/go-socks5"
"github.com/jpillora/sizestr"
"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 HandleTCPStream(l *Logger, connStats *ConnStats, src io.ReadWriteCloser, remote string) {
dst, err := net.Dial("tcp", remote)
if err != nil {
l.Debugf("Remote failed (%s)", err)
src.Close()
return
}
connStats.Open()
l.Debugf("%s: Open", connStats)
s, r := Pipe(src, dst)
connStats.Close()
l.Debugf("%s: Close (sent %s received %s)", connStats, sizestr.ToString(s), sizestr.ToString(r))
}
func HandleSocksStream(l *Logger, server *socks5.Server, connStats *ConnStats, src io.ReadWriteCloser) {
conn := NewRWCConn(src)
connStats.Open()
l.Debugf("%s Opening", connStats)
err := server.ServeConn(conn)
connStats.Close()
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
l.Debugf("%s: Closed (error: %s)", connStats, err)
} else {
l.Debugf("%s: Closed", connStats)
}
}
+162
View File
@@ -0,0 +1,162 @@
package tunnel
import (
"bytes"
"context"
"errors"
"io/ioutil"
"log"
"os"
"sync"
"sync/atomic"
"time"
"github.com/armon/go-socks5"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
)
//Config a Tunnel
type Config struct {
*cio.Logger
Inbound bool
Outbound bool
Socks bool
KeepAlive time.Duration
}
//Tunnel represents an SSH tunnel with proxy capabilities.
//Both chisel client and server are Tunnels.
//chisel client has a single set of remotes, whereas
//chisel server has multiple sets of remotes (one set per client).
//Each remote has a 1:1 mapping to a proxy.
//Proxies listen, send data over ssh, and the other end of the ssh connection
//communicates with the endpoint and returns the response.
type Tunnel struct {
Config
//ssh connection
activeConnMut sync.RWMutex
activeConn ssh.Conn
wgConn sync.WaitGroup
//proxies
proxyCount int
//internals
connStats cnet.ConnStats
socksServer *socks5.Server
}
var tid = uint64(0)
//New Tunnel from the given Config
func New(c Config) *Tunnel {
if c.Logger.Debug {
c.Logger = c.Logger.Fork("tun%d", atomic.AddUint64(&tid, 1))
}
t := &Tunnel{Config: c}
//block getters
t.wgConn.Add(1)
//setup socks server (not listening on any port!)
extra := ""
if c.Socks {
sl := log.New(ioutil.Discard, "", 0)
if t.Logger.Debug {
sl = log.New(os.Stdout, "[socks]", log.Ldate|log.Ltime)
}
t.socksServer, _ = socks5.New(&socks5.Config{Logger: sl})
extra += " (SOCKS enabled)"
}
t.Debugf("Created%s", extra)
return t
}
//BindSSH provides an active SSH for use for tunnelling
func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error {
//link ctx to ssh-conn
go func() {
<-ctx.Done()
c.Close()
}()
//mark active
t.activeConnMut.Lock()
if t.activeConn != nil {
panic("double bind ssh")
}
t.activeConn = c
t.activeConnMut.Unlock()
//unblock getters
t.wgConn.Done()
//optional keepalive loop against this connection
if t.Config.KeepAlive > 0 {
go t.keepAliveLoop(c)
}
//block until closed
go t.handleSSHRequests(reqs)
go t.handleSSHChannels(chans)
t.Debugf("Bound SSH")
err := c.Wait()
t.Debugf("Unbound SSH")
//reblock getters
t.wgConn.Add(1)
//mark inactive
t.activeConnMut.Lock()
t.activeConn = nil
t.activeConnMut.Unlock()
return err
}
func (t *Tunnel) getSSH() ssh.Conn {
t.wgConn.Wait() //TODO timeout?
t.activeConnMut.RLock()
c := t.activeConn
t.activeConnMut.RUnlock()
return c
}
//BindRemotes converts the given remotes into proxies, and blocks
//until the caller cancels the context or there is a proxy error.
func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error {
if len(remotes) == 0 {
return nil
}
if !t.Inbound {
return errors.New("inbound connections blocked")
}
proxies := make([]*Proxy, len(remotes))
for i, remote := range remotes {
p, err := NewProxy(t.Logger, t.getSSH, t.proxyCount, remote)
if err != nil {
return err
}
proxies[i] = p
t.proxyCount++
}
//TODO: handle tunnel close
eg, ctx := errgroup.WithContext(ctx)
for _, proxy := range proxies {
p := proxy
eg.Go(func() error {
return p.Run(ctx)
})
}
t.Debugf("Bound proxies")
err := eg.Wait()
t.Debugf("Unbound proxies")
return err
}
func (t *Tunnel) keepAliveLoop(sshConn ssh.Conn) {
for {
time.Sleep(t.Config.KeepAlive)
_, b, err := sshConn.SendRequest("ping", true, nil)
if err != nil {
break
}
if len(b) > 0 && !bytes.Equal(b, []byte("pong")) {
t.Debugf("strange ping response")
break
}
}
}
+142
View File
@@ -0,0 +1,142 @@
package tunnel
import (
"context"
"io"
"net"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/sizestr"
"golang.org/x/crypto/ssh"
)
//GetSSHConn blocks then returns once
//an active SSH connection is ready
type GetSSHConn func() ssh.Conn
//Proxy is the inbound portion of a Tunnel
type Proxy struct {
*cio.Logger
ssh GetSSHConn
id int
count int
remote *settings.Remote
dialer net.Dialer
tcp *net.TCPListener
udp *udpListener
}
//NewProxy creates a Proxy
func NewProxy(logger *cio.Logger, ssh GetSSHConn, index int, remote *settings.Remote) (*Proxy, error) {
id := index + 1
p := &Proxy{
Logger: logger.Fork("proxy#%s", remote.String()),
ssh: ssh,
id: id,
remote: remote,
}
return p, p.listen()
}
func (p *Proxy) listen() error {
if p.remote.Stdio {
//TODO check if pipes active?
} else if p.remote.LocalProto == "tcp" {
addr, err := net.ResolveTCPAddr("tcp", p.remote.LocalHost+":"+p.remote.LocalPort)
if err != nil {
return p.Errorf("resolve: %s", err)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return p.Errorf("tcp: %s", err)
}
p.Debugf("Listening")
p.tcp = l
} else if p.remote.LocalProto == "udp" {
l, err := listenUDP(p.Logger, p.ssh, p.remote)
if err != nil {
return err
}
p.Debugf("Listening")
p.udp = l
} else {
return p.Errorf("unknown local proto")
}
return nil
}
//Run enables the proxy and blocks while its active,
//close the proxy by cancelling the context.
func (p *Proxy) Run(ctx context.Context) error {
defer p.Debugf("Closed")
if p.remote.Stdio {
return p.runStdio(ctx)
} else if p.remote.LocalProto == "tcp" {
return p.runTCP(ctx)
} else if p.remote.LocalProto == "udp" {
return p.udp.run(ctx)
}
panic("should not get here")
}
func (p *Proxy) runStdio(ctx context.Context) error {
for {
p.pipeRemote(cio.Stdio)
select {
case <-ctx.Done():
return nil
default:
// the connection is not ready yet, keep waiting
}
}
}
func (p *Proxy) runTCP(ctx context.Context) error {
done := make(chan struct{})
//implements missing net.ListenContext
go func() {
select {
case <-ctx.Done():
p.tcp.Close()
case <-done:
}
}()
for {
src, err := p.tcp.Accept()
if err != nil {
select {
case <-ctx.Done():
//listener closed
default:
p.Infof("Accept error: %s", err)
}
close(done)
return err
}
go p.pipeRemote(src)
}
}
func (p *Proxy) pipeRemote(src io.ReadWriteCloser) {
defer src.Close()
p.count++
cid := p.count
l := p.Fork("conn#%d", cid)
l.Debugf("Open")
sshConn := p.ssh()
if sshConn == nil {
l.Debugf("No remote connection")
return
}
//ssh request for tcp connection for this proxy's remote
dst, reqs, err := sshConn.OpenChannel("chisel", []byte(p.remote.Remote()))
if err != nil {
l.Infof("Stream error: %s", err)
return
}
go ssh.DiscardRequests(reqs)
//then pipe
s, r := cio.Pipe(src, dst)
l.Debugf("Close (sent %s received %s)", sizestr.ToString(s), sizestr.ToString(r))
}
+157
View File
@@ -0,0 +1,157 @@
package tunnel
import (
"context"
"encoding/gob"
"net"
"sync"
"sync/atomic"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/settings"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
)
//listenUDP is a special listener which forwards packets via
//the bound ssh connection. tricky part is multiplexing lots of
//udp clients through the entry node. each will listen on its
//own source-port for a response:
// (random)
// src-1 1111->... dst-1 6345->7777
// src-2 2222->... <---> udp <---> udp <-> dst-1 7543->7777
// src-3 3333->... listener handler dst-1 1444->7777
//
//we must store these mappings (1111-6345, etc) in memory for a length
//of time, so that when the exit node receives a response on 6345, it
//knows to return it to 1111.
func listenUDP(l *cio.Logger, ssh GetSSHConn, remote *settings.Remote) (*udpListener, error) {
l = l.Fork("udp")
a, err := net.ResolveUDPAddr("udp", remote.Local())
if err != nil {
return nil, l.Errorf("resolve: %s", err)
}
conn, err := net.ListenUDP("udp", a)
if err != nil {
return nil, l.Errorf("listen: %s", err)
}
//ready
u := &udpListener{
Logger: l,
ssh: ssh,
remote: remote,
inbound: conn,
}
return u, nil
}
type udpListener struct {
*cio.Logger
ssh GetSSHConn
remote *settings.Remote
inbound *net.UDPConn
outboundMut sync.Mutex
outbound *udpOutbound
sent, recv int64
}
func (u *udpListener) run(ctx context.Context) error {
//udp doesnt accept connections,
//udp simply forwards all packets
//and therefore only needs to listen
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
return u.runInbound(ctx)
})
eg.Go(func() error {
return u.runOutbound(ctx)
})
if err := eg.Wait(); err != nil {
u.Debugf("listen: %s", err)
return err
}
u.Debugf("sent %d, received %d", u.sent, u.recv)
return nil
}
func (u *udpListener) runInbound(ctx context.Context) error {
dstAddr := u.remote.Remote()
const maxMTU = 9012
buff := make([]byte, maxMTU)
for !isDone(ctx) {
//read from inbound udp
n, addr, err := u.inbound.ReadFromUDP(buff)
if err != nil {
return u.Errorf("read error: %w", err)
}
//upsert ssh channel
o, err := u.getOubound()
if err != nil {
return u.Errorf("ssh-chan error: %w", err)
}
//send over channel, including source address
b := buff[:n]
if err := o.encode(addr.String(), dstAddr, b); err != nil {
return u.Errorf("encode error: %w", err)
}
//stats
atomic.AddInt64(&u.sent, int64(n))
}
return nil
}
func (u *udpListener) runOutbound(ctx context.Context) error {
for !isDone(ctx) {
//upsert ssh channel
o, err := u.getOubound()
if err != nil {
return u.Errorf("ssh-chan error: %w", err)
}
//receive from channel, including source address
p := udpPacket{}
if err := o.decode(&p); err != nil {
return u.Errorf("decode error: %w", err)
}
//write back to inbound udp
addr, err := net.ResolveUDPAddr("udp", p.Src)
if err != nil {
return u.Errorf("resolve error: %w", err)
}
n, err := u.inbound.WriteToUDP(p.Payload, addr)
if err != nil {
return u.Errorf("write error: %w", err)
}
//stats
atomic.AddInt64(&u.recv, int64(n))
}
return nil
}
func (u *udpListener) getOubound() (*udpOutbound, error) {
u.outboundMut.Lock()
defer u.outboundMut.Unlock()
//cached
if u.outbound != nil {
return u.outbound, nil
}
//not cached, bind
sshConn := u.ssh()
if sshConn == nil {
return nil, u.Errorf("ssh-conn nil")
}
//ssh request for udp packets for this proxy's remote,
//just "udp" since the remote address is sent with each packet
rwc, reqs, err := sshConn.OpenChannel("chisel", []byte("udp"))
if err != nil {
return nil, u.Errorf("ssh-chan error: %s", err)
}
go ssh.DiscardRequests(reqs)
//ready
o := &udpOutbound{
r: gob.NewDecoder(rwc),
w: gob.NewEncoder(rwc),
c: rwc,
}
u.outbound = o
return o, nil
}
+84
View File
@@ -0,0 +1,84 @@
package tunnel
import (
"fmt"
"io"
"net"
"strings"
"github.com/jpillora/chisel/share/cio"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/sizestr"
"golang.org/x/crypto/ssh"
)
func (t *Tunnel) handleSSHRequests(reqs <-chan *ssh.Request) {
for r := range reqs {
switch r.Type {
case "ping":
r.Reply(true, []byte("pong"))
default:
t.Debugf("Unknown request: %s", r.Type)
}
}
}
func (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) {
for ch := range chans {
go t.handleSSHChannel(ch)
}
}
func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) {
if !t.Config.Outbound {
t.Debugf("Denied outbound connection")
ch.Reject(ssh.Prohibited, "Denied outbound connection")
return
}
remote := string(ch.ExtraData())
udp := remote == "udp"
socks := remote == "socks"
if socks && t.socksServer == nil {
t.Debugf("Denied socks request, please enable socks")
ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled")
return
}
stream, reqs, err := ch.Accept()
if err != nil {
t.Debugf("Failed to accept stream: %s", err)
return
}
defer stream.Close()
go ssh.DiscardRequests(reqs)
l := t.Logger.Fork("conn#%d", t.connStats.New())
//ready to handle
t.connStats.Open()
l.Debugf("Open %s", t.connStats.String())
if socks {
err = t.handleSocks(stream)
} else if udp {
err = t.handleUDP(l, stream)
} else {
err = t.handleTCP(l, stream, remote)
}
t.connStats.Close()
errmsg := ""
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
errmsg = fmt.Sprintf(" (error %s)", err)
}
l.Debugf("Close %s%s", t.connStats.String(), errmsg)
}
func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error {
return t.socksServer.ServeConn(cnet.NewRWCConn(src))
}
func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, remote string) error {
dst, err := net.Dial("tcp", remote)
if err != nil {
return err
}
s, r := cio.Pipe(src, dst)
l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r))
return nil
}
+145
View File
@@ -0,0 +1,145 @@
package tunnel
import (
"encoding/gob"
"io"
"net"
"os"
"sync"
"sync/atomic"
"time"
"github.com/jpillora/chisel/share/cio"
)
func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser) error {
h := &udpHandler{
Logger: l,
udpOutbound: &udpOutbound{
r: gob.NewDecoder(rwc),
w: gob.NewEncoder(rwc),
c: rwc,
},
udpConns: &udpConns{
m: map[string]*udpConn{},
},
}
for {
p := udpPacket{}
if err := h.handleWrite(&p); err != nil {
return err
}
}
}
type udpHandler struct {
*cio.Logger
*udpOutbound
*udpConns
sent, recv int64
}
func (h *udpHandler) handleWrite(p *udpPacket) error {
if err := h.r.Decode(&p); err != nil {
return err
}
//dial now, we know we must write
conn, exists, err := h.udpConns.dial(p.Src, p.Dst)
if err != nil {
return err
}
//however, we dont know if we must read...
//spawn up to <max-conns> go-routines to wait
//for a reply.
//TODO configurable
//TODO++ dont use go-routines, switch to pollable
// array of listeners where all listeners are
// sweeped periodically, removing the idle ones
const maxConns = 100
if !exists {
if h.udpConns.len() <= maxConns {
go h.handleRead(p, conn)
} else {
h.Debugf("exceeded max udp connections (%d)", maxConns)
}
}
n, err := conn.Write(p.Payload)
if err != nil {
return err
}
//stats
atomic.AddInt64(&h.sent, int64(n))
return nil
}
func (h *udpHandler) handleRead(p *udpPacket, conn *udpConn) {
//ensure connection is cleaned up
defer h.udpConns.remove(conn.id)
const maxMTU = 9012
buff := make([]byte, maxMTU)
for {
//response must arrive within 15 seconds
//TODO configurable
const deadline = 15 * time.Second
conn.SetReadDeadline(time.Now().Add(deadline))
//read response
n, err := conn.Read(buff)
if err != nil {
if !os.IsTimeout(err) && err != io.EOF {
h.Debugf("read error: %s", err)
}
break
}
b := buff[:n]
//encode back over ssh connection
err = h.udpOutbound.encode(p.Src, p.Dst, b)
if err != nil {
h.Debugf("encode error: %s", err)
return
}
//stats
atomic.AddInt64(&h.recv, int64(n))
}
}
type udpConns struct {
sync.Mutex
m map[string]*udpConn
}
func (cs *udpConns) dial(id, addr string) (*udpConn, bool, error) {
cs.Lock()
defer cs.Unlock()
conn, ok := cs.m[id]
if !ok {
c, err := net.Dial("udp", addr)
if err != nil {
return nil, false, err
}
conn = &udpConn{
id: id,
Conn: c,
}
conn.Conn = c
cs.m[id] = conn
}
return conn, ok, nil
}
func (cs *udpConns) len() int {
cs.Lock()
l := len(cs.m)
cs.Unlock()
return l
}
func (cs *udpConns) remove(id string) {
cs.Lock()
delete(cs.m, id)
cs.Unlock()
}
type udpConn struct {
id string
net.Conn
}
+44
View File
@@ -0,0 +1,44 @@
package tunnel
import (
"context"
"encoding/gob"
"io"
)
type udpPacket struct {
Src string
Dst string
Payload []byte
}
func init() {
gob.Register(&udpPacket{})
}
type udpOutbound struct {
r *gob.Decoder
w *gob.Encoder
c io.Closer
}
func (o *udpOutbound) encode(src, dst string, b []byte) error {
return o.w.Encode(udpPacket{
Src: src,
Dst: dst,
Payload: b,
})
}
func (o *udpOutbound) decode(p *udpPacket) error {
return o.r.Decode(p)
}
func isDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
+4 -4
View File
@@ -25,7 +25,7 @@ import (
"path"
"strconv"
"github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/cnet"
"time"
)
@@ -105,7 +105,7 @@ func requestFile(port string, size int) (*http.Response, error) {
return http.Get(url)
}
func makeFileServer() *chshare.HTTPServer {
func makeFileServer() *cnet.HTTPServer {
bsize := 3 * MB
bytes := make([]byte, bsize)
//filling huge buffer
@@ -113,8 +113,8 @@ func makeFileServer() *chshare.HTTPServer {
bytes[i] = byte(i)
}
s := chshare.NewHTTPServer()
s.SetKeepAlivesEnabled(false)
s := cnet.NewHTTPServer()
s.Server.SetKeepAlivesEnabled(false)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rsize, _ := strconv.Atoi(r.URL.Path[1:])
for rsize >= bsize {
+66
View File
@@ -0,0 +1,66 @@
### Performance
With [crowbar](https://github.com/q3k/crowbar), a connection is tunneled by repeatedly querying the server with updates. This results in a large amount of HTTP and TCP connection overhead. Chisel overcomes this using WebSockets combined with [crypto/ssh](https://golang.org/x/crypto/ssh) to create hundreds of logical connections, resulting in **one** TCP connection per client.
In this simple benchmark, we have:
```
(direct)
.--------------->----------------.
/ chisel chisel \
request--->client:2001--->server:2002---->fileserver:3000
\ /
'--> crowbar:4001--->crowbar:4002'
client server
```
Note, we're using an in-memory "file" server on localhost for these tests
_direct_
```
:3000 => 1 bytes in 1.291417ms
:3000 => 10 bytes in 713.525µs
:3000 => 100 bytes in 562.48µs
:3000 => 1000 bytes in 595.445µs
:3000 => 10000 bytes in 1.053298ms
:3000 => 100000 bytes in 741.351µs
:3000 => 1000000 bytes in 1.367143ms
:3000 => 10000000 bytes in 8.601549ms
:3000 => 100000000 bytes in 76.3939ms
```
`chisel`
```
:2001 => 1 bytes in 1.351976ms
:2001 => 10 bytes in 1.106086ms
:2001 => 100 bytes in 1.005729ms
:2001 => 1000 bytes in 1.254396ms
:2001 => 10000 bytes in 1.139777ms
:2001 => 100000 bytes in 2.35437ms
:2001 => 1000000 bytes in 11.502673ms
:2001 => 10000000 bytes in 123.130246ms
:2001 => 100000000 bytes in 966.48636ms
```
~100MB in **~1 second**
`crowbar`
```
:4001 => 1 bytes in 3.335797ms
:4001 => 10 bytes in 1.453007ms
:4001 => 100 bytes in 1.811727ms
:4001 => 1000 bytes in 1.621525ms
:4001 => 10000 bytes in 5.20729ms
:4001 => 100000 bytes in 38.461926ms
:4001 => 1000000 bytes in 358.784864ms
:4001 => 10000000 bytes in 3.603206487s
:4001 => 100000000 bytes in 36.332395213s
```
~100MB in **36 seconds**
See `test/bench/main.go`
+48
View File
@@ -0,0 +1,48 @@
package e2e_test
import (
"testing"
chclient "github.com/jpillora/chisel/client"
chserver "github.com/jpillora/chisel/server"
)
//TODO tests for:
// - failed auth
// - dynamic auth (server add/remove user)
// - watch auth file
func TestAuth(t *testing.T) {
tmpPort1 := availablePort()
tmpPort2 := availablePort()
//setup server, client, fileserver
teardown := simpleSetup(t,
&chserver.Config{
KeySeed: "foobar",
Auth: "../bench/userfile",
},
&chclient.Config{
Remotes: []string{
"0.0.0.0:" + tmpPort1 + ":127.0.0.1:$FILEPORT",
"0.0.0.0:" + tmpPort2 + ":localhost:$FILEPORT",
},
Auth: "foo:bar",
})
defer teardown()
//test first remote
result, err := post("http://localhost:"+tmpPort1, "foo")
if err != nil {
t.Fatal(err)
}
if result != "foo!" {
t.Fatalf("expected exclamation mark added")
}
//test second remote
result, err = post("http://localhost:"+tmpPort2, "bar")
if err != nil {
t.Fatal(err)
}
if result != "bar!" {
t.Fatalf("expected exclamation mark added again")
}
}
+48
View File
@@ -0,0 +1,48 @@
package e2e_test
import (
"testing"
chclient "github.com/jpillora/chisel/client"
chserver "github.com/jpillora/chisel/server"
)
func TestBase(t *testing.T) {
tmpPort := availablePort()
//setup server, client, fileserver
teardown := simpleSetup(t,
&chserver.Config{},
&chclient.Config{
Remotes: []string{tmpPort + ":$FILEPORT"},
})
defer teardown()
//test remote
result, err := post("http://localhost:"+tmpPort, "foo")
if err != nil {
t.Fatal(err)
}
if result != "foo!" {
t.Fatalf("expected exclamation mark added")
}
}
func TestReverse(t *testing.T) {
tmpPort := availablePort()
//setup server, client, fileserver
teardown := simpleSetup(t,
&chserver.Config{
Reverse: true,
},
&chclient.Config{
Remotes: []string{"R:" + tmpPort + ":$FILEPORT"},
})
defer teardown()
//test remote (this goes through the server and out the client)
result, err := post("http://localhost:"+tmpPort, "foo")
if err != nil {
t.Fatal(err)
}
if result != "foo!" {
t.Fatalf("expected exclamation mark added")
}
}
+5
View File
@@ -0,0 +1,5 @@
package e2e_test
//TODO tests for:
// client -> CONNECT proxy -> server -> endpoint
// client -> SOCKS proxy -> server -> endpoint
+132
View File
@@ -0,0 +1,132 @@
package e2e_test
import (
"context"
"io/ioutil"
"log"
"net"
"net/http"
"strings"
"testing"
"time"
chclient "github.com/jpillora/chisel/client"
chserver "github.com/jpillora/chisel/server"
)
const debug = true
//test layout configuration
type testLayout struct {
server *chserver.Config
client *chclient.Config
fileServer bool
udpEcho bool
udpServer bool
}
func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chclient.Client, teardown context.CancelFunc) {
ctx, teardown := context.WithCancel(context.Background())
//fileserver (fake endpoint)
filePort := availablePort()
if tl.fileServer {
fileAddr := "127.0.0.1:" + filePort
f := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
w.Write(append(b, '!'))
}),
}
fl, err := net.Listen("tcp", fileAddr)
if err != nil {
t.Fatal(err)
}
log.Printf("fileserver: listening on %s", fileAddr)
go func() {
f.Serve(fl)
teardown()
}()
go func() {
<-ctx.Done()
f.Close()
}()
}
//server
server, err := chserver.NewServer(tl.server)
if err != nil {
t.Fatal(err)
}
server.Debug = debug
port := availablePort()
if err := server.StartContext(ctx, "127.0.0.1", port); err != nil {
t.Fatal(err)
}
go func() {
server.Wait()
server.Infof("Closed")
teardown()
}()
//client (with defaults)
tl.client.Fingerprint = server.GetFingerprint()
tl.client.Server = "http://127.0.0.1:" + port
for i, r := range tl.client.Remotes {
//convert $FILEPORT into the allocated port for this test case
if tl.fileServer {
tl.client.Remotes[i] = strings.Replace(r, "$FILEPORT", filePort, 1)
}
}
client, err = chclient.NewClient(tl.client)
if err != nil {
t.Fatal(err)
}
client.Debug = debug
if err := client.Start(ctx); err != nil {
t.Fatal(err)
}
go func() {
client.Wait()
client.Infof("Closed")
teardown()
}()
//wait a bit...
//TODO: client signal API, similar to os.Notify(signal)
// wait for client setup
time.Sleep(50 * time.Millisecond)
//ready
return server, client, teardown
}
func simpleSetup(t *testing.T, s *chserver.Config, c *chclient.Config) context.CancelFunc {
conf := testLayout{
server: s,
client: c,
fileServer: true,
}
_, _, teardown := conf.setup(t)
return teardown
}
func post(url, body string) (string, error) {
resp, err := http.Post(url, "text/plain", strings.NewReader(body))
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(b), nil
}
func availablePort() string {
l, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
log.Panic(err)
}
l.Close()
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
log.Panic(err)
}
return port
}
+5
View File
@@ -0,0 +1,5 @@
package e2e_test
//TODO tests for:
// - SOCKS-client -> [client -> server SOCKS] -> endpoint
// - SOCKS-client -> [server -> client SOCKS] -> endpoint
+88
View File
@@ -0,0 +1,88 @@
package e2e_test
import (
"log"
"net"
"testing"
"time"
chclient "github.com/jpillora/chisel/client"
chserver "github.com/jpillora/chisel/server"
"golang.org/x/sync/errgroup"
)
func TestUDP(t *testing.T) {
//listen on random udp port
echoPort := availableUDPPort()
a, _ := net.ResolveUDPAddr("udp", ":"+echoPort)
l, err := net.ListenUDP("udp", a)
if err != nil {
t.Fatal(err)
}
//chisel client+server
inboundPort := availableUDPPort()
teardown := simpleSetup(t,
&chserver.Config{},
&chclient.Config{
Remotes: []string{
inboundPort + ":" + echoPort + "/udp",
},
},
)
defer teardown()
//fake udp server, read and echo back twice, close
eg := errgroup.Group{}
eg.Go(func() error {
defer l.Close()
b := make([]byte, 128)
n, a, err := l.ReadFrom(b)
if err != nil {
return err
}
if _, err = l.WriteTo(append(b[:n], b[:n]...), a); err != nil {
return err
}
return nil
})
//fake udp client
conn, err := net.Dial("udp4", "localhost:"+inboundPort)
if err != nil {
t.Fatal(err)
}
//write bazz through the tunnel
if _, err := conn.Write([]byte("bazz")); err != nil {
t.Fatal(err)
}
//receive bazzbazz back
b := make([]byte, 128)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(b)
if err != nil {
t.Fatal(err)
return
}
//udp server should close correctly
if err := eg.Wait(); err != nil {
t.Fatal(err)
return
}
//ensure expected
s := string(b[:n])
if s != "bazzbazz" {
t.Fatalf("expected double bazz")
}
}
func availableUDPPort() string {
a, _ := net.ResolveUDPAddr("udp", ":0")
l, err := net.ListenUDP("udp", a)
if err != nil {
log.Panicf("availability listen: %s", err)
}
l.Close()
_, port, err := net.SplitHostPort(l.LocalAddr().String())
if err != nil {
log.Panic(err)
}
return port
}