mirror of
https://github.com/jpillora/chisel
synced 2026-06-08 15:07:02 +00:00
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:
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# TODO
|
||||
gocompat compare \
|
||||
--go1compat \
|
||||
--log-level=debug \
|
||||
--git-refs=origin/master..$(git rev-parse --abbrev-ref HEAD) \
|
||||
./...
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
[](https://godoc.org/github.com/jpillora/chisel) [](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.
|
||||
|
||||

|
||||
|
||||
### 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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,4 +1,4 @@
|
||||
package chshare
|
||||
package cio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -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,4 +1,4 @@
|
||||
package chshare
|
||||
package cio
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -1,4 +1,4 @@
|
||||
package chshare
|
||||
package cnet
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
package chshare
|
||||
package cos
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
package chshare
|
||||
package settings
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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`
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package e2e_test
|
||||
|
||||
//TODO tests for:
|
||||
// client -> CONNECT proxy -> server -> endpoint
|
||||
// client -> SOCKS proxy -> server -> endpoint
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package e2e_test
|
||||
|
||||
//TODO tests for:
|
||||
// - SOCKS-client -> [client -> server SOCKS] -> endpoint
|
||||
// - SOCKS-client -> [server -> client SOCKS] -> endpoint
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user