Codebase list chisel / e055f8e
New upstream version 1.7.4 Sophie Brun 3 years ago
62 changed file(s) with 5297 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/bin/bash
1 # TODO
2 gocompat compare \
3 --go1compat \
4 --log-level=debug \
5 --git-refs=origin/master..$(git rev-parse --abbrev-ref HEAD) \
6 ./...
0 # test this goreleaser config with:
1 # - cd chisel
2 # - goreleaser --skip-publish --rm-dist --config .github/goreleaser.yml
3 builds:
4 - env:
5 - CGO_ENABLED=0
6 ldflags:
7 - -s -w -X github.com/jpillora/chisel/share.BuildVersion={{.Version}}
8 flags:
9 - -trimpath
10 goos:
11 - linux
12 - darwin
13 - windows
14 goarch:
15 - 386
16 - amd64
17 - arm
18 - arm64
19 - ppc64
20 - ppc64le
21 - mips
22 - mipsle
23 - mips64
24 - mips64le
25 - s390x
26 goarm:
27 - 6
28 - 7
29 gomips:
30 - hardfloat
31 - softfloat
32 archives:
33 - format: gz
34 files:
35 - none*
36 release:
37 prerelease: auto
38 changelog:
39 sort: asc
40 filters:
41 exclude:
42 - "^docs:"
43 - "^test:"
0 on: [push, pull_request]
1 name: CI
2 jobs:
3 # ================
4 # TEST JOB
5 # runs on every push and PR
6 # runs 2x3 times (see matrix)
7 # ================
8 test:
9 name: Test
10 strategy:
11 matrix:
12 go-version: [1.13.x, 1.14.x, 1.15.x]
13 platform: [ubuntu-latest, macos-latest, windows-latest]
14 runs-on: ${{ matrix.platform }}
15 steps:
16 - name: Install Go
17 uses: actions/setup-go@v1
18 with:
19 go-version: ${{ matrix.go-version }}
20 - name: Checkout code
21 uses: actions/checkout@v2
22 - name: Build
23 run: go build -v .
24 - name: Test
25 run: go test -v ./...
26 env:
27 GODEBUG: x509ignoreCN=0
28 # ================
29 # RELEASE JOB
30 # runs after a success test
31 # only runs on push "v*" tag
32 # ================
33 release:
34 name: Release
35 needs: test
36 if: startsWith(github.ref, 'refs/tags/v')
37 runs-on: ubuntu-latest
38 steps:
39 - name: Check out code
40 uses: actions/checkout@v2
41 - name: goreleaser
42 if: success()
43 uses: docker://goreleaser/goreleaser:latest
44 env:
45 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 with:
47 args: release --config .github/goreleaser.yml
0
1 *.swp
2 .idea/
3 chisel
4 bin/
5 release/
6 tmp/
7 *.orig
8 debug
9
10 # Compiled Object files, Static and Dynamic libs (Shared Objects)
11 *.o
12 *.a
13 *.so
14
15 # Folders
16 _obj
17 _test
18
19 # Architecture specific extensions/prefixes
20 *.[568vq]
21 [568vq].out
22
23 *.cgo1.go
24 *.cgo2.c
25 _cgo_defun.c
26 _cgo_gotypes.go
27 _cgo_export.*
28
29 _testmain.go
30
31 *.exe
32 *.test
33 *.prof
0 # build stage
1 FROM golang:alpine AS build-env
2 LABEL maintainer="[email protected]"
3 RUN apk update
4 RUN apk add git
5 ENV CGO_ENABLED 0
6 ADD . /src
7 WORKDIR /src
8 RUN go build \
9 -ldflags "-X github.com/jpillora/chisel/share.BuildVersion=$(git describe --abbrev=0 --tags)" \
10 -o chisel
11 # container stage
12 FROM alpine
13 RUN apk update && apk add --no-cache ca-certificates
14 WORKDIR /app
15 COPY --from=build-env /src/chisel /app/chisel
16 ENTRYPOINT ["/app/chisel"]
0 MIT License
1
2 Copyright (c) 2020 Jaime Pillora <[email protected]>
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
0 # chisel
1
2 [![GoDoc](https://godoc.org/github.com/jpillora/chisel?status.svg)](https://godoc.org/github.com/jpillora/chisel) [![CI](https://github.com/jpillora/chisel/workflows/CI/badge.svg)](https://github.com/jpillora/chisel/actions?workflow=CI)
3
4 Chisel is a fast TCP/UDP tunnel, transported over HTTP, secured via SSH. Single executable including both client and server. Written in Go (golang). Chisel is mainly useful for passing through firewalls, though it can also be used to provide a secure endpoint into your network.
5
6 ![overview](https://docs.google.com/drawings/d/1p53VWxzGNfy8rjr-mW8pvisJmhkoLl82vAgctO_6f1w/pub?w=960&h=720)
7
8 ### Features
9
10 - Easy to use
11 - [Performant](./test/bench/perf.md)\*
12 - [Encrypted connections](#security) using the SSH protocol (via `crypto/ssh`)
13 - [Authenticated connections](#authentication); authenticated client connections with a users config file, authenticated server connections with fingerprint matching.
14 - Client auto-reconnects with [exponential backoff](https://github.com/jpillora/backoff)
15 - Clients can create multiple tunnel endpoints over one TCP connection
16 - Clients can optionally pass through SOCKS or HTTP CONNECT proxies
17 - Reverse port forwarding (Connections go through the server and out the client)
18 - Server optionally doubles as a [reverse proxy](http://golang.org/pkg/net/http/httputil/#NewSingleHostReverseProxy)
19 - Server optionally allows [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections (See [guide below](#socks5-guide))
20 - Clients optionally allow [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections from a reversed port forward
21 - Client connections over stdio which supports `ssh -o ProxyCommand` providing SSH over HTTP
22
23 ### Install
24
25 **Binaries**
26
27 [![Releases](https://img.shields.io/github/release/jpillora/chisel.svg)](https://github.com/jpillora/chisel/releases) [![Releases](https://img.shields.io/github/downloads/jpillora/chisel/total.svg)](https://github.com/jpillora/chisel/releases)
28
29 See [the latest release](https://github.com/jpillora/chisel/releases/latest) or download and install it now with `curl https://i.jpillora.com/chisel! | bash`
30
31 **Docker**
32
33 [![Docker Pulls](https://img.shields.io/docker/pulls/jpillora/chisel.svg)](https://hub.docker.com/r/jpillora/chisel/) [![Image Size](https://images.microbadger.com/badges/image/jpillora/chisel.svg)](https://microbadger.com/images/jpillora/chisel)
34
35 ```sh
36 docker run --rm -it jpillora/chisel --help
37 ```
38
39 **Source**
40
41 ```sh
42 $ go get -v github.com/jpillora/chisel
43 ```
44
45 ### Demo
46
47 A [demo app](https://chisel-demo.herokuapp.com) on Heroku is running this `chisel server`:
48
49 ```sh
50 $ chisel server --port $PORT --proxy http://example.com
51 # listens on $PORT, proxy web requests to http://example.com
52 ```
53
54 This demo app is also running a [simple file server](https://www.npmjs.com/package/serve) on `:3000`, which is normally inaccessible due to Heroku's firewall. However, if we tunnel in with:
55
56 ```sh
57 $ chisel client https://chisel-demo.herokuapp.com 3000
58 # connects to chisel server at https://chisel-demo.herokuapp.com,
59 # tunnels your localhost:3000 to the server's localhost:3000
60 ```
61
62 and then visit [localhost:3000](http://localhost:3000/), we should see a directory listing. Also, if we visit the [demo app](https://chisel-demo.herokuapp.com) in the browser we should hit the server's default proxy and see a copy of [example.com](http://example.com).
63
64 ### Usage
65
66 <!-- render these help texts by hand,
67 or use https://github.com/jpillora/md-tmpl
68 with $ md-tmpl -w README.md -->
69
70 <!--tmpl,code=plain:echo "$ chisel --help" && go run main.go --help | sed 's#0.0.0-src (go1\..*)#X.Y.Z#' -->
71 ``` plain
72 $ chisel --help
73
74 Usage: chisel [command] [--help]
75
76 Version: X.Y.Z
77
78 Commands:
79 server - runs chisel in server mode
80 client - runs chisel in client mode
81
82 Read more:
83 https://github.com/jpillora/chisel
84
85 ```
86 <!--/tmpl-->
87
88
89 <!--tmpl,code=plain:echo "$ chisel server --help" && go run main.go server --help | cat | sed 's#0.0.0-src (go1\..*)#X.Y.Z#' -->
90 ``` plain
91 $ chisel server --help
92
93 Usage: chisel server [options]
94
95 Options:
96
97 --host, Defines the HTTP listening host – the network interface
98 (defaults the environment variable HOST and falls back to 0.0.0.0).
99
100 --port, -p, Defines the HTTP listening port (defaults to the environment
101 variable PORT and fallsback to port 8080).
102
103 --key, An optional string to seed the generation of a ECDSA public
104 and private key pair. All communications will be secured using this
105 key pair. Share the subsequent fingerprint with clients to enable detection
106 of man-in-the-middle attacks (defaults to the CHISEL_KEY environment
107 variable, otherwise a new key is generate each run).
108
109 --authfile, An optional path to a users.json file. This file should
110 be an object with users defined like:
111 {
112 "<user:pass>": ["<addr-regex>","<addr-regex>"]
113 }
114 when <user> connects, their <pass> will be verified and then
115 each of the remote addresses will be compared against the list
116 of address regular expressions for a match. Addresses will
117 always come in the form "<remote-host>:<remote-port>" for normal remotes
118 and "R:<local-interface>:<local-port>" for reverse port forwarding
119 remotes. This file will be automatically reloaded on change.
120
121 --auth, An optional string representing a single user with full
122 access, in the form of <user:pass>. It is equivalent to creating an
123 authfile with {"<user:pass>": [""]}. If unset, it will use the
124 environment variable AUTH.
125
126 --keepalive, An optional keepalive interval. Since the underlying
127 transport is HTTP, in many instances we'll be traversing through
128 proxies, often these proxies will close idle connections. You must
129 specify a time with a unit, for example '5s' or '2m'. Defaults
130 to '25s' (set to 0s to disable).
131
132 --backend, Specifies another HTTP server to proxy requests to when
133 chisel receives a normal HTTP request. Useful for hiding chisel in
134 plain sight.
135
136 --socks5, Allow clients to access the internal SOCKS5 proxy. See
137 chisel client --help for more information.
138
139 --reverse, Allow clients to specify reverse port forwarding remotes
140 in addition to normal remotes.
141
142 --tls-key, Enables TLS and provides optional path to a PEM-encoded
143 TLS private key. When this flag is set, you must also set --tls-cert,
144 and you cannot set --tls-domain.
145
146 --tls-cert, Enables TLS and provides optional path to a PEM-encoded
147 TLS certificate. When this flag is set, you must also set --tls-key,
148 and you cannot set --tls-domain.
149
150 --tls-domain, Enables TLS and automatically acquires a TLS key and
151 certificate using LetsEncypt. Setting --tls-domain requires port 443.
152 You may specify multiple --tls-domain flags to serve multiple domains.
153 The resulting files are cached in the "$HOME/.cache/chisel" directory.
154 You can modify this path by setting the CHISEL_LE_CACHE variable,
155 or disable caching by setting this variable to "-". You can optionally
156 provide a certificate notification email by setting CHISEL_LE_EMAIL.
157
158 --tls-ca, a path to a PEM encoded CA certificate bundle or a directory
159 holding multiple PEM encode CA certificate bundle files, which is used to
160 validate client connections. The provided CA certificates will be used
161 instead of the system roots. This is commonly used to implement mutual-TLS.
162
163 --pid Generate pid file in current working directory
164
165 -v, Enable verbose logging
166
167 --help, This help text
168
169 Signals:
170 The chisel process is listening for:
171 a SIGUSR2 to print process stats, and
172 a SIGHUP to short-circuit the client reconnect timer
173
174 Version:
175 X.Y.Z
176
177 Read more:
178 https://github.com/jpillora/chisel
179
180 ```
181 <!--/tmpl-->
182
183
184 <!--tmpl,code=plain:echo "$ chisel client --help" && go run main.go client --help | sed 's#0.0.0-src (go1\..*)#X.Y.Z#' -->
185 ``` plain
186 $ chisel client --help
187
188 Usage: chisel client [options] <server> <remote> [remote] [remote] ...
189
190 <server> is the URL to the chisel server.
191
192 <remote>s are remote connections tunneled through the server, each of
193 which come in the form:
194
195 <local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>
196
197 ■ local-host defaults to 0.0.0.0 (all interfaces).
198 ■ local-port defaults to remote-port.
199 ■ remote-port is required*.
200 ■ remote-host defaults to 0.0.0.0 (server localhost).
201 ■ protocol defaults to tcp.
202
203 which shares <remote-host>:<remote-port> from the server to the client
204 as <local-host>:<local-port>, or:
205
206 R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>
207
208 which does reverse port forwarding, sharing <remote-host>:<remote-port>
209 from the client to the server's <local-interface>:<local-port>.
210
211 example remotes
212
213 3000
214 example.com:3000
215 3000:google.com:80
216 192.168.0.5:3000:google.com:80
217 socks
218 5000:socks
219 R:2222:localhost:22
220 R:socks
221 R:5000:socks
222 stdio:example.com:22
223 1.1.1.1:53/udp
224
225 When the chisel server has --socks5 enabled, remotes can
226 specify "socks" in place of remote-host and remote-port.
227 The default local host and port for a "socks" remote is
228 127.0.0.1:1080. Connections to this remote will terminate
229 at the server's internal SOCKS5 proxy.
230
231 When the chisel server has --reverse enabled, remotes can
232 be prefixed with R to denote that they are reversed. That
233 is, the server will listen and accept connections, and they
234 will be proxied through the client which specified the remote.
235 Reverse remotes specifying "R:socks" will listen on the server's
236 default socks port (1080) and terminate the connection at the
237 client's internal SOCKS5 proxy.
238
239 When stdio is used as local-host, the tunnel will connect standard
240 input/output of this program with the remote. This is useful when
241 combined with ssh ProxyCommand. You can use
242 ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \
243 [email protected]
244 to connect to an SSH server through the tunnel.
245
246 Options:
247
248 --fingerprint, A *strongly recommended* fingerprint string
249 to perform host-key validation against the server's public key.
250 Fingerprint mismatches will close the connection.
251 Fingerprints are generated by hashing the ECDSA public key using
252 SHA256 and encoding the result in base64.
253 Fingerprints must be 44 characters containing a trailing equals (=).
254
255 --auth, An optional username and password (client authentication)
256 in the form: "<user>:<pass>". These credentials are compared to
257 the credentials inside the server's --authfile. defaults to the
258 AUTH environment variable.
259
260 --keepalive, An optional keepalive interval. Since the underlying
261 transport is HTTP, in many instances we'll be traversing through
262 proxies, often these proxies will close idle connections. You must
263 specify a time with a unit, for example '5s' or '2m'. Defaults
264 to '25s' (set to 0s to disable).
265
266 --max-retry-count, Maximum number of times to retry before exiting.
267 Defaults to unlimited.
268
269 --max-retry-interval, Maximum wait time before retrying after a
270 disconnection. Defaults to 5 minutes.
271
272 --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be
273 used to reach the chisel server. Authentication can be specified
274 inside the URL.
275 For example, http://admin:[email protected]:8081
276 or: socks://admin:[email protected]:1080
277
278 --header, Set a custom header in the form "HeaderName: HeaderContent".
279 Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World")
280
281 --hostname, Optionally set the 'Host' header (defaults to the host
282 found in the server url).
283
284 --tls-ca, An optional root certificate bundle used to verify the
285 chisel server. Only valid when connecting to the server with
286 "https" or "wss". By default, the operating system CAs will be used.
287
288 --tls-skip-verify, Skip server TLS certificate verification of
289 chain and host name (if TLS is used for transport connections to
290 server). If set, client accepts any TLS certificate presented by
291 the server and any host name in that certificate. This only affects
292 transport https (wss) connection. Chisel server's public key
293 may be still verified (see --fingerprint) after inner connection
294 is established.
295
296 --tls-key, a path to a PEM encoded private key used for client
297 authentication (mutual-TLS).
298
299 --tls-cert, a path to a PEM encoded certificate matching the provided
300 private key. The certificate must have client authentication
301 enabled (mutual-TLS).
302
303 --pid Generate pid file in current working directory
304
305 -v, Enable verbose logging
306
307 --help, This help text
308
309 Signals:
310 The chisel process is listening for:
311 a SIGUSR2 to print process stats, and
312 a SIGHUP to short-circuit the client reconnect timer
313
314 Version:
315 X.Y.Z
316
317 Read more:
318 https://github.com/jpillora/chisel
319
320 ```
321 <!--/tmpl-->
322
323 ### Security
324
325 Encryption is always enabled. When you start up a chisel server, it will generate an in-memory ECDSA public/private key pair. The public key fingerprint (base64 encoded SHA256) will be displayed as the server starts. Instead of generating a random key, the server may optionally specify a key seed, using the `--key` option, which will be used to seed the key generation. When clients connect, they will also display the server's public key fingerprint. The client can force a particular fingerprint using the `--fingerprint` option. See the `--help` above for more information.
326
327 ### Authentication
328
329 Using the `--authfile` option, the server may optionally provide a `user.json` configuration file to create a list of accepted users. The client then authenticates using the `--auth` option. See [users.json](example/users.json) for an example authentication configuration file. See the `--help` above for more information.
330
331 Internally, this is done using the _Password_ authentication method provided by SSH. Learn more about `crypto/ssh` here http://blog.gopheracademy.com/go-and-ssh/.
332
333 ### SOCKS5 Guide
334
335 1. Start your chisel server
336
337 ```sh
338 docker run \
339 --name chisel -p 9312:9312 \
340 -d --restart always \
341 jpillora/chisel server -p 9312 --socks5 --key supersecret
342 ```
343
344 2. Connect your chisel client (using server's fingerprint)
345
346 ```sh
347 chisel client --fingerprint 'rHb55mcxf6vSckL2AezFV09rLs7pfPpavVu++MF7AhQ=' <server-address>:9312 socks
348 ```
349
350 3. Point your SOCKS5 clients (e.g. OS/Browser) to:
351
352 ```
353 <client-address>:1080
354 ```
355
356 4. Now you have an encrypted, authenticated SOCKS5 connection over HTTP
357
358
359 ### Caveats
360
361 Since WebSockets support is required:
362
363 - 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)
364 - PaaS providers vary in their support for WebSockets
365 - Heroku has full support
366 - Openshift has full support though connections are only accepted on ports 8443 and 8080
367 - Google App Engine has **no** support (Track this on [their repo](https://code.google.com/p/googleappengine/issues/detail?id=2535))
368
369 ### Contributing
370
371 - http://golang.org/doc/code.html
372 - http://golang.org/doc/effective_go.html
373 - `github.com/jpillora/chisel/share` contains the shared package
374 - `github.com/jpillora/chisel/server` contains the server package
375 - `github.com/jpillora/chisel/client` contains the client package
376
377 ### Changelog
378
379 - `1.0` - Initial release
380 - `1.1` - Replaced simple symmetric encryption for ECDSA SSH
381 - `1.2` - Added SOCKS5 (server) and HTTP CONNECT (client) support
382 - `1.3` - Added reverse tunnelling support
383 - `1.4` - Added arbitrary HTTP header support
384 - `1.5` - Added reverse SOCKS support (by @aus)
385 - `1.6` - Added client stdio support (by @BoleynSu)
386 - `1.7` - Added UDP support
0 package chclient
1
2 import (
3 "context"
4 "crypto/md5"
5 "crypto/tls"
6 "crypto/x509"
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "io/ioutil"
11 "net"
12 "net/http"
13 "net/url"
14 "regexp"
15 "strings"
16 "time"
17
18 "github.com/gorilla/websocket"
19 chshare "github.com/jpillora/chisel/share"
20 "github.com/jpillora/chisel/share/ccrypto"
21 "github.com/jpillora/chisel/share/cio"
22 "github.com/jpillora/chisel/share/cnet"
23 "github.com/jpillora/chisel/share/settings"
24 "github.com/jpillora/chisel/share/tunnel"
25
26 "golang.org/x/crypto/ssh"
27 "golang.org/x/net/proxy"
28 "golang.org/x/sync/errgroup"
29 )
30
31 //Config represents a client configuration
32 type Config struct {
33 Fingerprint string
34 Auth string
35 KeepAlive time.Duration
36 MaxRetryCount int
37 MaxRetryInterval time.Duration
38 Server string
39 Proxy string
40 Remotes []string
41 Headers http.Header
42 TLS TLSConfig
43 DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
44 }
45
46 //TLSConfig for a Client
47 type TLSConfig struct {
48 SkipVerify bool
49 CA string
50 Cert string
51 Key string
52 }
53
54 //Client represents a client instance
55 type Client struct {
56 *cio.Logger
57 config *Config
58 computed settings.Config
59 sshConfig *ssh.ClientConfig
60 tlsConfig *tls.Config
61 proxyURL *url.URL
62 server string
63 connCount cnet.ConnCount
64 stop func()
65 eg *errgroup.Group
66 tunnel *tunnel.Tunnel
67 }
68
69 //NewClient creates a new client instance
70 func NewClient(c *Config) (*Client, error) {
71 //apply default scheme
72 if !strings.HasPrefix(c.Server, "http") {
73 c.Server = "http://" + c.Server
74 }
75 if c.MaxRetryInterval < time.Second {
76 c.MaxRetryInterval = 5 * time.Minute
77 }
78 u, err := url.Parse(c.Server)
79 if err != nil {
80 return nil, err
81 }
82 //swap to websockets scheme
83 u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
84 //apply default port
85 if !regexp.MustCompile(`:\d+$`).MatchString(u.Host) {
86 if u.Scheme == "wss" {
87 u.Host = u.Host + ":443"
88 } else {
89 u.Host = u.Host + ":80"
90 }
91 }
92 hasReverse := false
93 hasSocks := false
94 hasStdio := false
95 client := &Client{
96 Logger: cio.NewLogger("client"),
97 config: c,
98 computed: settings.Config{
99 Version: chshare.BuildVersion,
100 },
101 server: u.String(),
102 tlsConfig: nil,
103 }
104 //set default log level
105 client.Logger.Info = true
106 //configure tls
107 if u.Scheme == "wss" {
108 tc := &tls.Config{}
109 //certificate verification config
110 if c.TLS.SkipVerify {
111 client.Infof("TLS verification disabled")
112 tc.InsecureSkipVerify = true
113 } else if c.TLS.CA != "" {
114 rootCAs := x509.NewCertPool()
115 if b, err := ioutil.ReadFile(c.TLS.CA); err != nil {
116 return nil, fmt.Errorf("Failed to load file: %s", c.TLS.CA)
117 } else if ok := rootCAs.AppendCertsFromPEM(b); !ok {
118 return nil, fmt.Errorf("Failed to decode PEM: %s", c.TLS.CA)
119 } else {
120 client.Infof("TLS verification using CA %s", c.TLS.CA)
121 tc.RootCAs = rootCAs
122 }
123 }
124 //provide client cert and key pair for mtls
125 if c.TLS.Cert != "" && c.TLS.Key != "" {
126 c, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
127 if err != nil {
128 return nil, fmt.Errorf("Error loading client cert and key pair: %v", err)
129 }
130 tc.Certificates = []tls.Certificate{c}
131 } else if c.TLS.Cert != "" || c.TLS.Key != "" {
132 return nil, fmt.Errorf("Please specify client BOTH cert and key")
133 }
134 client.tlsConfig = tc
135 }
136 //validate remotes
137 for _, s := range c.Remotes {
138 r, err := settings.DecodeRemote(s)
139 if err != nil {
140 return nil, fmt.Errorf("Failed to decode remote '%s': %s", s, err)
141 }
142 if r.Socks {
143 hasSocks = true
144 }
145 if r.Reverse {
146 hasReverse = true
147 }
148 if r.Stdio {
149 if hasStdio {
150 return nil, errors.New("Only one stdio is allowed")
151 }
152 hasStdio = true
153 }
154 //confirm non-reverse tunnel is available
155 if !r.Reverse && !r.Stdio && !r.CanListen() {
156 return nil, fmt.Errorf("Client cannot listen on %s", r.String())
157 }
158 client.computed.Remotes = append(client.computed.Remotes, r)
159 }
160 //outbound proxy
161 if p := c.Proxy; p != "" {
162 client.proxyURL, err = url.Parse(p)
163 if err != nil {
164 return nil, fmt.Errorf("Invalid proxy URL (%s)", err)
165 }
166 }
167 //ssh auth and config
168 user, pass := settings.ParseAuth(c.Auth)
169 client.sshConfig = &ssh.ClientConfig{
170 User: user,
171 Auth: []ssh.AuthMethod{ssh.Password(pass)},
172 ClientVersion: "SSH-" + chshare.ProtocolVersion + "-client",
173 HostKeyCallback: client.verifyServer,
174 Timeout: settings.EnvDuration("SSH_TIMEOUT", 30*time.Second),
175 }
176 //prepare client tunnel
177 client.tunnel = tunnel.New(tunnel.Config{
178 Logger: client.Logger,
179 Inbound: true, //client always accepts inbound
180 Outbound: hasReverse,
181 Socks: hasReverse && hasSocks,
182 })
183 return client, nil
184 }
185
186 //Run starts client and blocks while connected
187 func (c *Client) Run() error {
188 ctx, cancel := context.WithCancel(context.Background())
189 defer cancel()
190 if err := c.Start(ctx); err != nil {
191 return err
192 }
193 return c.Wait()
194 }
195
196 func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error {
197 expect := c.config.Fingerprint
198 if expect == "" {
199 return nil
200 }
201 got := ccrypto.FingerprintKey(key)
202 _, err := base64.StdEncoding.DecodeString(expect)
203 if _, ok := err.(base64.CorruptInputError); ok {
204 c.Logger.Infof("Specified deprecated MD5 fingerprint (%s), please update to the new SHA256 fingerprint: %s", expect, got)
205 return c.verifyLegacyFingerprint(key)
206 } else if err != nil {
207 return fmt.Errorf("Error decoding fingerprint: %w", err)
208 }
209 if got != expect {
210 return fmt.Errorf("Invalid fingerprint (%s)", got)
211 }
212 //overwrite with complete fingerprint
213 c.Infof("Fingerprint %s", got)
214 return nil
215 }
216
217 //verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints
218 func (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error {
219 bytes := md5.Sum(key.Marshal())
220 strbytes := make([]string, len(bytes))
221 for i, b := range bytes {
222 strbytes[i] = fmt.Sprintf("%02x", b)
223 }
224 got := strings.Join(strbytes, ":")
225 expect := c.config.Fingerprint
226 if !strings.HasPrefix(got, expect) {
227 return fmt.Errorf("Invalid fingerprint (%s)", got)
228 }
229 return nil
230 }
231
232 //Start client and does not block
233 func (c *Client) Start(ctx context.Context) error {
234 ctx, cancel := context.WithCancel(ctx)
235 c.stop = cancel
236 eg, ctx := errgroup.WithContext(ctx)
237 c.eg = eg
238 via := ""
239 if c.proxyURL != nil {
240 via = " via " + c.proxyURL.String()
241 }
242 c.Infof("Connecting to %s%s\n", c.server, via)
243 //connect to chisel server
244 eg.Go(func() error {
245 return c.connectionLoop(ctx)
246 })
247 //listen sockets
248 eg.Go(func() error {
249 clientInbound := c.computed.Remotes.Reversed(false)
250 if len(clientInbound) == 0 {
251 return nil
252 }
253 return c.tunnel.BindRemotes(ctx, clientInbound)
254 })
255 return nil
256 }
257
258 func (c *Client) setProxy(u *url.URL, d *websocket.Dialer) error {
259 // CONNECT proxy
260 if !strings.HasPrefix(u.Scheme, "socks") {
261 d.Proxy = func(*http.Request) (*url.URL, error) {
262 return u, nil
263 }
264 return nil
265 }
266 // SOCKS5 proxy
267 if u.Scheme != "socks" && u.Scheme != "socks5h" {
268 return fmt.Errorf(
269 "unsupported socks proxy type: %s:// (only socks5h:// or socks:// is supported)",
270 u.Scheme,
271 )
272 }
273 var auth *proxy.Auth
274 if u.User != nil {
275 pass, _ := u.User.Password()
276 auth = &proxy.Auth{
277 User: u.User.Username(),
278 Password: pass,
279 }
280 }
281 socksDialer, err := proxy.SOCKS5("tcp", u.Host, auth, proxy.Direct)
282 if err != nil {
283 return err
284 }
285 d.NetDial = socksDialer.Dial
286 return nil
287 }
288
289 //Wait blocks while the client is running.
290 func (c *Client) Wait() error {
291 return c.eg.Wait()
292 }
293
294 //Close manually stops the client
295 func (c *Client) Close() error {
296 if c.stop != nil {
297 c.stop()
298 }
299 return nil
300 }
0 package chclient
1
2 import (
3 "context"
4 "errors"
5 "fmt"
6 "io"
7 "net"
8 "strings"
9 "time"
10
11 "github.com/gorilla/websocket"
12 "github.com/jpillora/backoff"
13 chshare "github.com/jpillora/chisel/share"
14 "github.com/jpillora/chisel/share/cnet"
15 "github.com/jpillora/chisel/share/cos"
16 "github.com/jpillora/chisel/share/settings"
17 "golang.org/x/crypto/ssh"
18 )
19
20 func (c *Client) connectionLoop(ctx context.Context) error {
21 //connection loop!
22 b := &backoff.Backoff{Max: c.config.MaxRetryInterval}
23 for {
24 connected, retry, err := c.connectionOnce(ctx)
25 //reset backoff after successful connections
26 if connected {
27 b.Reset()
28 }
29 //connection error
30 attempt := int(b.Attempt())
31 maxAttempt := c.config.MaxRetryCount
32 //dont print closed-connection errors
33 if strings.HasSuffix(err.Error(), "use of closed network connection") {
34 err = io.EOF
35 }
36 //show error message and attempt counts (excluding disconnects)
37 if err != nil && err != io.EOF {
38 msg := fmt.Sprintf("Connection error: %s", err)
39 if attempt > 0 {
40 msg += fmt.Sprintf(" (Attempt: %d", attempt)
41 if maxAttempt > 0 {
42 msg += fmt.Sprintf("/%d", maxAttempt)
43 }
44 msg += ")"
45 }
46 c.Infof(msg)
47 }
48 //give up?
49 if !retry || (maxAttempt >= 0 && attempt >= maxAttempt) {
50 c.Infof("Give up")
51 break
52 }
53 d := b.Duration()
54 c.Infof("Retrying in %s...", d)
55 select {
56 case <-cos.AfterSignal(d):
57 continue //retry now
58 case <-ctx.Done():
59 c.Infof("Cancelled")
60 return nil
61 }
62 }
63 c.Close()
64 return nil
65 }
66
67 //connectionOnce connects to the chisel server and blocks
68 func (c *Client) connectionOnce(ctx context.Context) (connected, retry bool, err error) {
69 //already closed?
70 select {
71 case <-ctx.Done():
72 return false, false, errors.New("Cancelled")
73 default:
74 //still open
75 }
76 ctx, cancel := context.WithCancel(ctx)
77 defer cancel()
78 //prepare dialer
79 d := websocket.Dialer{
80 HandshakeTimeout: settings.EnvDuration("WS_TIMEOUT", 45*time.Second),
81 Subprotocols: []string{chshare.ProtocolVersion},
82 TLSClientConfig: c.tlsConfig,
83 ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
84 WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
85 }
86 //optional proxy
87 if p := c.proxyURL; p != nil {
88 if err := c.setProxy(p, &d); err != nil {
89 return false, false, err
90 }
91 }
92 wsConn, _, err := d.DialContext(ctx, c.server, c.config.Headers)
93 if err != nil {
94 return false, true, err
95 }
96 conn := cnet.NewWebSocketConn(wsConn)
97 // perform SSH handshake on net.Conn
98 c.Debugf("Handshaking...")
99 sshConn, chans, reqs, err := ssh.NewClientConn(conn, "", c.sshConfig)
100 if err != nil {
101 e := err.Error()
102 if strings.Contains(e, "unable to authenticate") {
103 c.Infof("Authentication failed")
104 c.Debugf(e)
105 retry = false
106 } else if strings.Contains(e, "connection abort") {
107 c.Infof("retriable: %s", e)
108 retry = true
109 } else if n, ok := err.(net.Error); ok && !n.Temporary() {
110 c.Infof(e)
111 retry = false
112 } else {
113 c.Infof("retriable: %s", e)
114 retry = true
115 }
116 return false, retry, err
117 }
118 defer sshConn.Close()
119 // chisel client handshake (reverse of server handshake)
120 // send configuration
121 c.Debugf("Sending config")
122 t0 := time.Now()
123 _, configerr, err := sshConn.SendRequest(
124 "config",
125 true,
126 settings.EncodeConfig(c.computed),
127 )
128 if err != nil {
129 c.Infof("Config verification failed")
130 return false, false, err
131 }
132 if len(configerr) > 0 {
133 return false, false, errors.New(string(configerr))
134 }
135 c.Infof("Connected (Latency %s)", time.Since(t0))
136 //connected, handover ssh connection for tunnel to use, and block
137 retry = true
138 err = c.tunnel.BindSSH(ctx, sshConn, reqs, chans)
139 if n, ok := err.(net.Error); ok && !n.Temporary() {
140 retry = false
141 }
142 c.Infof("Disconnected")
143 connected = time.Since(t0) > 5*time.Second
144 return connected, retry, err
145 }
0 package chclient
1
2 import (
3 "crypto/ecdsa"
4 "crypto/elliptic"
5 "log"
6 "net/http"
7 "net/http/httptest"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/jpillora/chisel/share/ccrypto"
13 "golang.org/x/crypto/ssh"
14 )
15
16 func TestCustomHeaders(t *testing.T) {
17 //fake server
18 wg := sync.WaitGroup{}
19 wg.Add(1)
20 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
21 if req.Header.Get("Foo") != "Bar" {
22 t.Fatal("expected header Foo to be 'Bar'")
23 }
24 wg.Done()
25 }))
26 defer server.Close()
27 //client
28 headers := http.Header{}
29 headers.Set("Foo", "Bar")
30 config := Config{
31 KeepAlive: time.Second,
32 MaxRetryInterval: time.Second,
33 Server: server.URL,
34 Remotes: []string{"9000"},
35 Headers: headers,
36 }
37 c, err := NewClient(&config)
38 if err != nil {
39 log.Fatal(err)
40 }
41 go c.Run()
42 //wait for test to complete
43 wg.Wait()
44 c.Close()
45 }
46
47 func TestFallbackLegacyFingerprint(t *testing.T) {
48 config := Config{
49 Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44",
50 }
51 c, err := NewClient(&config)
52 if err != nil {
53 t.Fatal(err)
54 }
55 r := ccrypto.NewDetermRand([]byte("test123"))
56 priv, err := ecdsa.GenerateKey(elliptic.P256(), r)
57 if err != nil {
58 t.Fatal(err)
59 }
60 pub, err := ssh.NewPublicKey(&priv.PublicKey)
61 if err != nil {
62 t.Fatal(err)
63 }
64 err = c.verifyServer("", nil, pub)
65 if err != nil {
66 t.Fatal(err)
67 }
68 }
69
70 func TestVerifyLegacyFingerprint(t *testing.T) {
71 config := Config{
72 Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44",
73 }
74 c, err := NewClient(&config)
75 if err != nil {
76 t.Fatal(err)
77 }
78 r := ccrypto.NewDetermRand([]byte("test123"))
79 priv, err := ecdsa.GenerateKey(elliptic.P256(), r)
80 if err != nil {
81 t.Fatal(err)
82 }
83 pub, err := ssh.NewPublicKey(&priv.PublicKey)
84 if err != nil {
85 t.Fatal(err)
86 }
87 err = c.verifyLegacyFingerprint(pub)
88 if err != nil {
89 t.Fatal(err)
90 }
91 }
92
93 func TestVerifyFingerprint(t *testing.T) {
94 config := Config{
95 Fingerprint: "qmrRoo8MIqePv3jC8+wv49gU6uaFgD3FASQx9V8KdmY=",
96 }
97 c, err := NewClient(&config)
98 if err != nil {
99 t.Fatal(err)
100 }
101 r := ccrypto.NewDetermRand([]byte("test123"))
102 priv, err := ecdsa.GenerateKey(elliptic.P256(), r)
103 if err != nil {
104 t.Fatal(err)
105 }
106 pub, err := ssh.NewPublicKey(&priv.PublicKey)
107 if err != nil {
108 t.Fatal(err)
109 }
110 err = c.verifyServer("", nil, pub)
111 if err != nil {
112 t.Fatal(err)
113 }
114 }
0 {
1 "root:toor": [
2 ""
3 ],
4 "foo:bar": [
5 "^0.0.0.0:3000$"
6 ],
7 "ping:pong": [
8 "^0.0.0.0:[45]000$",
9 "^example.com:80$",
10 "^R:0.0.0.0:7000$"
11 ]
12 }
0 module github.com/jpillora/chisel
1
2 go 1.13
3
4 require (
5 github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect
6 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
7 github.com/fsnotify/fsnotify v1.4.9
8 github.com/gorilla/websocket v1.4.2
9 github.com/jpillora/ansi v1.0.2 // indirect
10 github.com/jpillora/backoff v1.0.0
11 github.com/jpillora/requestlog v1.0.0
12 github.com/jpillora/sizestr v1.0.0
13 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
14 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
15 golang.org/x/net v0.0.0-20200707034311-ab3426394381
16 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
17 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
18 )
0 github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
1 github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
2 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
3 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
4 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
5 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
6 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
7 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
8 github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s=
9 github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=
10 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
11 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
12 github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=
13 github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=
14 github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=
15 github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=
16 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
17 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
18 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
20 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
21 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
22 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
23 golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
24 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
25 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
26 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
27 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
28 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
30 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
32 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
33 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
34 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
0 package main
1
2 import (
3 "flag"
4 "fmt"
5 "io/ioutil"
6 "log"
7 "net/http"
8 "os"
9 "runtime"
10 "strconv"
11 "strings"
12 "time"
13
14 chclient "github.com/jpillora/chisel/client"
15 chserver "github.com/jpillora/chisel/server"
16 chshare "github.com/jpillora/chisel/share"
17 "github.com/jpillora/chisel/share/cos"
18 )
19
20 var help = `
21 Usage: chisel [command] [--help]
22
23 Version: ` + chshare.BuildVersion + ` (` + runtime.Version() + `)
24
25 Commands:
26 server - runs chisel in server mode
27 client - runs chisel in client mode
28
29 Read more:
30 https://github.com/jpillora/chisel
31
32 `
33
34 func main() {
35
36 version := flag.Bool("version", false, "")
37 v := flag.Bool("v", false, "")
38 flag.Bool("help", false, "")
39 flag.Bool("h", false, "")
40 flag.Usage = func() {}
41 flag.Parse()
42
43 if *version || *v {
44 fmt.Println(chshare.BuildVersion)
45 os.Exit(0)
46 }
47
48 args := flag.Args()
49
50 subcmd := ""
51 if len(args) > 0 {
52 subcmd = args[0]
53 args = args[1:]
54 }
55
56 switch subcmd {
57 case "server":
58 server(args)
59 case "client":
60 client(args)
61 default:
62 fmt.Print(help)
63 os.Exit(0)
64 }
65 }
66
67 var commonHelp = `
68 --pid Generate pid file in current working directory
69
70 -v, Enable verbose logging
71
72 --help, This help text
73
74 Signals:
75 The chisel process is listening for:
76 a SIGUSR2 to print process stats, and
77 a SIGHUP to short-circuit the client reconnect timer
78
79 Version:
80 ` + chshare.BuildVersion + ` (` + runtime.Version() + `)
81
82 Read more:
83 https://github.com/jpillora/chisel
84
85 `
86
87 func generatePidFile() {
88 pid := []byte(strconv.Itoa(os.Getpid()))
89 if err := ioutil.WriteFile("chisel.pid", pid, 0644); err != nil {
90 log.Fatal(err)
91 }
92 }
93
94 var serverHelp = `
95 Usage: chisel server [options]
96
97 Options:
98
99 --host, Defines the HTTP listening host – the network interface
100 (defaults the environment variable HOST and falls back to 0.0.0.0).
101
102 --port, -p, Defines the HTTP listening port (defaults to the environment
103 variable PORT and fallsback to port 8080).
104
105 --key, An optional string to seed the generation of a ECDSA public
106 and private key pair. All communications will be secured using this
107 key pair. Share the subsequent fingerprint with clients to enable detection
108 of man-in-the-middle attacks (defaults to the CHISEL_KEY environment
109 variable, otherwise a new key is generate each run).
110
111 --authfile, An optional path to a users.json file. This file should
112 be an object with users defined like:
113 {
114 "<user:pass>": ["<addr-regex>","<addr-regex>"]
115 }
116 when <user> connects, their <pass> will be verified and then
117 each of the remote addresses will be compared against the list
118 of address regular expressions for a match. Addresses will
119 always come in the form "<remote-host>:<remote-port>" for normal remotes
120 and "R:<local-interface>:<local-port>" for reverse port forwarding
121 remotes. This file will be automatically reloaded on change.
122
123 --auth, An optional string representing a single user with full
124 access, in the form of <user:pass>. It is equivalent to creating an
125 authfile with {"<user:pass>": [""]}. If unset, it will use the
126 environment variable AUTH.
127
128 --keepalive, An optional keepalive interval. Since the underlying
129 transport is HTTP, in many instances we'll be traversing through
130 proxies, often these proxies will close idle connections. You must
131 specify a time with a unit, for example '5s' or '2m'. Defaults
132 to '25s' (set to 0s to disable).
133
134 --backend, Specifies another HTTP server to proxy requests to when
135 chisel receives a normal HTTP request. Useful for hiding chisel in
136 plain sight.
137
138 --socks5, Allow clients to access the internal SOCKS5 proxy. See
139 chisel client --help for more information.
140
141 --reverse, Allow clients to specify reverse port forwarding remotes
142 in addition to normal remotes.
143
144 --tls-key, Enables TLS and provides optional path to a PEM-encoded
145 TLS private key. When this flag is set, you must also set --tls-cert,
146 and you cannot set --tls-domain.
147
148 --tls-cert, Enables TLS and provides optional path to a PEM-encoded
149 TLS certificate. When this flag is set, you must also set --tls-key,
150 and you cannot set --tls-domain.
151
152 --tls-domain, Enables TLS and automatically acquires a TLS key and
153 certificate using LetsEncypt. Setting --tls-domain requires port 443.
154 You may specify multiple --tls-domain flags to serve multiple domains.
155 The resulting files are cached in the "$HOME/.cache/chisel" directory.
156 You can modify this path by setting the CHISEL_LE_CACHE variable,
157 or disable caching by setting this variable to "-". You can optionally
158 provide a certificate notification email by setting CHISEL_LE_EMAIL.
159
160 --tls-ca, a path to a PEM encoded CA certificate bundle or a directory
161 holding multiple PEM encode CA certificate bundle files, which is used to
162 validate client connections. The provided CA certificates will be used
163 instead of the system roots. This is commonly used to implement mutual-TLS.
164 ` + commonHelp
165
166 func server(args []string) {
167
168 flags := flag.NewFlagSet("server", flag.ContinueOnError)
169
170 config := &chserver.Config{}
171 flags.StringVar(&config.KeySeed, "key", "", "")
172 flags.StringVar(&config.AuthFile, "authfile", "", "")
173 flags.StringVar(&config.Auth, "auth", "", "")
174 flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
175 flags.StringVar(&config.Proxy, "proxy", "", "")
176 flags.StringVar(&config.Proxy, "backend", "", "")
177 flags.BoolVar(&config.Socks5, "socks5", false, "")
178 flags.BoolVar(&config.Reverse, "reverse", false, "")
179 flags.StringVar(&config.TLS.Key, "tls-key", "", "")
180 flags.StringVar(&config.TLS.Cert, "tls-cert", "", "")
181 flags.Var(multiFlag{&config.TLS.Domains}, "tls-domain", "")
182 flags.StringVar(&config.TLS.CA, "tls-ca", "", "")
183
184 host := flags.String("host", "", "")
185 p := flags.String("p", "", "")
186 port := flags.String("port", "", "")
187 pid := flags.Bool("pid", false, "")
188 verbose := flags.Bool("v", false, "")
189
190 flags.Usage = func() {
191 fmt.Print(serverHelp)
192 os.Exit(0)
193 }
194 flags.Parse(args)
195
196 if *host == "" {
197 *host = os.Getenv("HOST")
198 }
199 if *host == "" {
200 *host = "0.0.0.0"
201 }
202 if *port == "" {
203 *port = *p
204 }
205 if *port == "" {
206 *port = os.Getenv("PORT")
207 }
208 if *port == "" {
209 *port = "8080"
210 }
211 if config.KeySeed == "" {
212 config.KeySeed = os.Getenv("CHISEL_KEY")
213 }
214 s, err := chserver.NewServer(config)
215 if err != nil {
216 log.Fatal(err)
217 }
218 s.Debug = *verbose
219 if *pid {
220 generatePidFile()
221 }
222 go cos.GoStats()
223 ctx := cos.InterruptContext()
224 if err := s.StartContext(ctx, *host, *port); err != nil {
225 log.Fatal(err)
226 }
227 if err := s.Wait(); err != nil {
228 log.Fatal(err)
229 }
230 }
231
232 type multiFlag struct {
233 values *[]string
234 }
235
236 func (flag multiFlag) String() string {
237 return strings.Join(*flag.values, ", ")
238 }
239
240 func (flag multiFlag) Set(arg string) error {
241 *flag.values = append(*flag.values, arg)
242 return nil
243 }
244
245 type headerFlags struct {
246 http.Header
247 }
248
249 func (flag *headerFlags) String() string {
250 out := ""
251 for k, v := range flag.Header {
252 out += fmt.Sprintf("%s: %s\n", k, v)
253 }
254 return out
255 }
256
257 func (flag *headerFlags) Set(arg string) error {
258 index := strings.Index(arg, ":")
259 if index < 0 {
260 return fmt.Errorf(`Invalid header (%s). Should be in the format "HeaderName: HeaderContent"`, arg)
261 }
262 if flag.Header == nil {
263 flag.Header = http.Header{}
264 }
265 key := arg[0:index]
266 value := arg[index+1:]
267 flag.Header.Set(key, strings.TrimSpace(value))
268 return nil
269 }
270
271 var clientHelp = `
272 Usage: chisel client [options] <server> <remote> [remote] [remote] ...
273
274 <server> is the URL to the chisel server.
275
276 <remote>s are remote connections tunneled through the server, each of
277 which come in the form:
278
279 <local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>
280
281 ■ local-host defaults to 0.0.0.0 (all interfaces).
282 ■ local-port defaults to remote-port.
283 ■ remote-port is required*.
284 ■ remote-host defaults to 0.0.0.0 (server localhost).
285 ■ protocol defaults to tcp.
286
287 which shares <remote-host>:<remote-port> from the server to the client
288 as <local-host>:<local-port>, or:
289
290 R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>
291
292 which does reverse port forwarding, sharing <remote-host>:<remote-port>
293 from the client to the server's <local-interface>:<local-port>.
294
295 example remotes
296
297 3000
298 example.com:3000
299 3000:google.com:80
300 192.168.0.5:3000:google.com:80
301 socks
302 5000:socks
303 R:2222:localhost:22
304 R:socks
305 R:5000:socks
306 stdio:example.com:22
307 1.1.1.1:53/udp
308
309 When the chisel server has --socks5 enabled, remotes can
310 specify "socks" in place of remote-host and remote-port.
311 The default local host and port for a "socks" remote is
312 127.0.0.1:1080. Connections to this remote will terminate
313 at the server's internal SOCKS5 proxy.
314
315 When the chisel server has --reverse enabled, remotes can
316 be prefixed with R to denote that they are reversed. That
317 is, the server will listen and accept connections, and they
318 will be proxied through the client which specified the remote.
319 Reverse remotes specifying "R:socks" will listen on the server's
320 default socks port (1080) and terminate the connection at the
321 client's internal SOCKS5 proxy.
322
323 When stdio is used as local-host, the tunnel will connect standard
324 input/output of this program with the remote. This is useful when
325 combined with ssh ProxyCommand. You can use
326 ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \
327 [email protected]
328 to connect to an SSH server through the tunnel.
329
330 Options:
331
332 --fingerprint, A *strongly recommended* fingerprint string
333 to perform host-key validation against the server's public key.
334 Fingerprint mismatches will close the connection.
335 Fingerprints are generated by hashing the ECDSA public key using
336 SHA256 and encoding the result in base64.
337 Fingerprints must be 44 characters containing a trailing equals (=).
338
339 --auth, An optional username and password (client authentication)
340 in the form: "<user>:<pass>". These credentials are compared to
341 the credentials inside the server's --authfile. defaults to the
342 AUTH environment variable.
343
344 --keepalive, An optional keepalive interval. Since the underlying
345 transport is HTTP, in many instances we'll be traversing through
346 proxies, often these proxies will close idle connections. You must
347 specify a time with a unit, for example '5s' or '2m'. Defaults
348 to '25s' (set to 0s to disable).
349
350 --max-retry-count, Maximum number of times to retry before exiting.
351 Defaults to unlimited.
352
353 --max-retry-interval, Maximum wait time before retrying after a
354 disconnection. Defaults to 5 minutes.
355
356 --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be
357 used to reach the chisel server. Authentication can be specified
358 inside the URL.
359 For example, http://admin:[email protected]:8081
360 or: socks://admin:[email protected]:1080
361
362 --header, Set a custom header in the form "HeaderName: HeaderContent".
363 Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World")
364
365 --hostname, Optionally set the 'Host' header (defaults to the host
366 found in the server url).
367
368 --tls-ca, An optional root certificate bundle used to verify the
369 chisel server. Only valid when connecting to the server with
370 "https" or "wss". By default, the operating system CAs will be used.
371
372 --tls-skip-verify, Skip server TLS certificate verification of
373 chain and host name (if TLS is used for transport connections to
374 server). If set, client accepts any TLS certificate presented by
375 the server and any host name in that certificate. This only affects
376 transport https (wss) connection. Chisel server's public key
377 may be still verified (see --fingerprint) after inner connection
378 is established.
379
380 --tls-key, a path to a PEM encoded private key used for client
381 authentication (mutual-TLS).
382
383 --tls-cert, a path to a PEM encoded certificate matching the provided
384 private key. The certificate must have client authentication
385 enabled (mutual-TLS).
386 ` + commonHelp
387
388 func client(args []string) {
389 flags := flag.NewFlagSet("client", flag.ContinueOnError)
390 config := chclient.Config{Headers: http.Header{}}
391 flags.StringVar(&config.Fingerprint, "fingerprint", "", "")
392 flags.StringVar(&config.Auth, "auth", "", "")
393 flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
394 flags.IntVar(&config.MaxRetryCount, "max-retry-count", -1, "")
395 flags.DurationVar(&config.MaxRetryInterval, "max-retry-interval", 0, "")
396 flags.StringVar(&config.Proxy, "proxy", "", "")
397 flags.StringVar(&config.TLS.CA, "tls-ca", "", "")
398 flags.BoolVar(&config.TLS.SkipVerify, "tls-skip-verify", false, "")
399 flags.StringVar(&config.TLS.Cert, "tls-cert", "", "")
400 flags.StringVar(&config.TLS.Key, "tls-key", "", "")
401 flags.Var(&headerFlags{config.Headers}, "header", "")
402 hostname := flags.String("hostname", "", "")
403 pid := flags.Bool("pid", false, "")
404 verbose := flags.Bool("v", false, "")
405 flags.Usage = func() {
406 fmt.Print(clientHelp)
407 os.Exit(0)
408 }
409 flags.Parse(args)
410 //pull out options, put back remaining args
411 args = flags.Args()
412 if len(args) < 2 {
413 log.Fatalf("A server and least one remote is required")
414 }
415 config.Server = args[0]
416 config.Remotes = args[1:]
417 //default auth
418 if config.Auth == "" {
419 config.Auth = os.Getenv("AUTH")
420 }
421 //move hostname onto headers
422 if *hostname != "" {
423 config.Headers.Set("Host", *hostname)
424 }
425 //ready
426 c, err := chclient.NewClient(&config)
427 if err != nil {
428 log.Fatal(err)
429 }
430 c.Debug = *verbose
431 if *pid {
432 generatePidFile()
433 }
434 go cos.GoStats()
435 ctx := cos.InterruptContext()
436 if err := c.Start(ctx); err != nil {
437 log.Fatal(err)
438 }
439 if err := c.Wait(); err != nil {
440 log.Fatal(err)
441 }
442 }
0 package chserver
1
2 import (
3 "context"
4 "errors"
5 "log"
6 "net/http"
7 "net/http/httputil"
8 "net/url"
9 "regexp"
10 "time"
11
12 "github.com/gorilla/websocket"
13 chshare "github.com/jpillora/chisel/share"
14 "github.com/jpillora/chisel/share/ccrypto"
15 "github.com/jpillora/chisel/share/cio"
16 "github.com/jpillora/chisel/share/cnet"
17 "github.com/jpillora/chisel/share/settings"
18 "github.com/jpillora/requestlog"
19 "golang.org/x/crypto/ssh"
20 )
21
22 // Config is the configuration for the chisel service
23 type Config struct {
24 KeySeed string
25 AuthFile string
26 Auth string
27 Proxy string
28 Socks5 bool
29 Reverse bool
30 KeepAlive time.Duration
31 TLS TLSConfig
32 }
33
34 // Server respresent a chisel service
35 type Server struct {
36 *cio.Logger
37 config *Config
38 fingerprint string
39 httpServer *cnet.HTTPServer
40 reverseProxy *httputil.ReverseProxy
41 sessCount int32
42 sessions *settings.Users
43 sshConfig *ssh.ServerConfig
44 users *settings.UserIndex
45 }
46
47 var upgrader = websocket.Upgrader{
48 CheckOrigin: func(r *http.Request) bool { return true },
49 ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
50 WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
51 }
52
53 // NewServer creates and returns a new chisel server
54 func NewServer(c *Config) (*Server, error) {
55 server := &Server{
56 config: c,
57 httpServer: cnet.NewHTTPServer(),
58 Logger: cio.NewLogger("server"),
59 sessions: settings.NewUsers(),
60 }
61 server.Info = true
62 server.users = settings.NewUserIndex(server.Logger)
63 if c.AuthFile != "" {
64 if err := server.users.LoadUsers(c.AuthFile); err != nil {
65 return nil, err
66 }
67 }
68 if c.Auth != "" {
69 u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}
70 u.Name, u.Pass = settings.ParseAuth(c.Auth)
71 if u.Name != "" {
72 server.users.AddUser(u)
73 }
74 }
75 //generate private key (optionally using seed)
76 key, err := ccrypto.GenerateKey(c.KeySeed)
77 if err != nil {
78 log.Fatal("Failed to generate key")
79 }
80 //convert into ssh.PrivateKey
81 private, err := ssh.ParsePrivateKey(key)
82 if err != nil {
83 log.Fatal("Failed to parse key")
84 }
85 //fingerprint this key
86 server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
87 //create ssh config
88 server.sshConfig = &ssh.ServerConfig{
89 ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server",
90 PasswordCallback: server.authUser,
91 }
92 server.sshConfig.AddHostKey(private)
93 //setup reverse proxy
94 if c.Proxy != "" {
95 u, err := url.Parse(c.Proxy)
96 if err != nil {
97 return nil, err
98 }
99 if u.Host == "" {
100 return nil, server.Errorf("Missing protocol (%s)", u)
101 }
102 server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
103 //always use proxy host
104 server.reverseProxy.Director = func(r *http.Request) {
105 //enforce origin, keep path
106 r.URL.Scheme = u.Scheme
107 r.URL.Host = u.Host
108 r.Host = u.Host
109 }
110 }
111 //print when reverse tunnelling is enabled
112 if c.Reverse {
113 server.Infof("Reverse tunnelling enabled")
114 }
115 return server, nil
116 }
117
118 // Run is responsible for starting the chisel service.
119 // Internally this calls Start then Wait.
120 func (s *Server) Run(host, port string) error {
121 if err := s.Start(host, port); err != nil {
122 return err
123 }
124 return s.Wait()
125 }
126
127 // Start is responsible for kicking off the http server
128 func (s *Server) Start(host, port string) error {
129 return s.StartContext(context.Background(), host, port)
130 }
131
132 // StartContext is responsible for kicking off the http server,
133 // and can be closed by cancelling the provided context
134 func (s *Server) StartContext(ctx context.Context, host, port string) error {
135 s.Infof("Fingerprint %s", s.fingerprint)
136 if s.users.Len() > 0 {
137 s.Infof("User authenication enabled")
138 }
139 if s.reverseProxy != nil {
140 s.Infof("Reverse proxy enabled")
141 }
142 l, err := s.listener(host, port)
143 if err != nil {
144 return err
145 }
146 h := http.Handler(http.HandlerFunc(s.handleClientHandler))
147 if s.Debug {
148 o := requestlog.DefaultOptions
149 o.TrustProxy = true
150 h = requestlog.WrapWith(h, o)
151 }
152 return s.httpServer.GoServe(ctx, l, h)
153 }
154
155 // Wait waits for the http server to close
156 func (s *Server) Wait() error {
157 return s.httpServer.Wait()
158 }
159
160 // Close forcibly closes the http server
161 func (s *Server) Close() error {
162 return s.httpServer.Close()
163 }
164
165 // GetFingerprint is used to access the server fingerprint
166 func (s *Server) GetFingerprint() string {
167 return s.fingerprint
168 }
169
170 // authUser is responsible for validating the ssh user / password combination
171 func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
172 // check if user authenication is enable and it not allow all
173 if s.users.Len() == 0 {
174 return nil, nil
175 }
176 // check the user exists and has matching password
177 n := c.User()
178 user, found := s.users.Get(n)
179 if !found || user.Pass != string(password) {
180 s.Debugf("Login failed for user: %s", n)
181 return nil, errors.New("Invalid authentication for username: %s")
182 }
183 // insert the user session map
184 // TODO this should probably have a lock on it given the map isn't thread-safe
185 s.sessions.Set(string(c.SessionID()), user)
186 return nil, nil
187 }
188
189 // AddUser adds a new user into the server user index
190 func (s *Server) AddUser(user, pass string, addrs ...string) error {
191 authorizedAddrs := []*regexp.Regexp{}
192 for _, addr := range addrs {
193 authorizedAddr, err := regexp.Compile(addr)
194 if err != nil {
195 return err
196 }
197 authorizedAddrs = append(authorizedAddrs, authorizedAddr)
198 }
199 s.users.AddUser(&settings.User{
200 Name: user,
201 Pass: pass,
202 Addrs: authorizedAddrs,
203 })
204 return nil
205 }
206
207 // DeleteUser removes a user from the server user index
208 func (s *Server) DeleteUser(user string) {
209 s.users.Del(user)
210 }
211
212 // ResetUsers in the server user index.
213 // Use nil to remove all.
214 func (s *Server) ResetUsers(users []*settings.User) {
215 s.users.Reset(users)
216 }
0 package chserver
1
2 import (
3 "net/http"
4 "strings"
5 "sync/atomic"
6 "time"
7
8 chshare "github.com/jpillora/chisel/share"
9 "github.com/jpillora/chisel/share/cnet"
10 "github.com/jpillora/chisel/share/settings"
11 "github.com/jpillora/chisel/share/tunnel"
12 "golang.org/x/crypto/ssh"
13 "golang.org/x/sync/errgroup"
14 )
15
16 // handleClientHandler is the main http websocket handler for the chisel server
17 func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {
18 //websockets upgrade AND has chisel prefix
19 upgrade := strings.ToLower(r.Header.Get("Upgrade"))
20 protocol := r.Header.Get("Sec-WebSocket-Protocol")
21 if upgrade == "websocket" && strings.HasPrefix(protocol, "chisel-") {
22 if protocol == chshare.ProtocolVersion {
23 s.handleWebsocket(w, r)
24 return
25 }
26 //print into server logs and silently fall-through
27 s.Infof("ignored client connection using protocol '%s', expected '%s'",
28 protocol, chshare.ProtocolVersion)
29 }
30 //proxy target was provided
31 if s.reverseProxy != nil {
32 s.reverseProxy.ServeHTTP(w, r)
33 return
34 }
35 //no proxy defined, provide access to health/version checks
36 switch r.URL.String() {
37 case "/health":
38 w.Write([]byte("OK\n"))
39 return
40 case "/version":
41 w.Write([]byte(chshare.BuildVersion))
42 return
43 }
44 //missing :O
45 w.WriteHeader(404)
46 w.Write([]byte("Not found"))
47 }
48
49 // handleWebsocket is responsible for handling the websocket connection
50 func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
51 id := atomic.AddInt32(&s.sessCount, 1)
52 l := s.Fork("session#%d", id)
53 wsConn, err := upgrader.Upgrade(w, req, nil)
54 if err != nil {
55 l.Debugf("Failed to upgrade (%s)", err)
56 return
57 }
58 conn := cnet.NewWebSocketConn(wsConn)
59 // perform SSH handshake on net.Conn
60 l.Debugf("Handshaking with %s...", req.RemoteAddr)
61 sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
62 if err != nil {
63 s.Debugf("Failed to handshake (%s)", err)
64 return
65 }
66 // pull the users from the session map
67 var user *settings.User
68 if s.users.Len() > 0 {
69 sid := string(sshConn.SessionID())
70 u, ok := s.sessions.Get(sid)
71 if !ok {
72 panic("bug in ssh auth handler")
73 }
74 user = u
75 s.sessions.Del(sid)
76 }
77 // chisel server handshake (reverse of client handshake)
78 // verify configuration
79 l.Debugf("Verifying configuration")
80 // wait for request, with timeout
81 var r *ssh.Request
82 select {
83 case r = <-reqs:
84 case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)):
85 l.Debugf("Timeout waiting for configuration")
86 sshConn.Close()
87 return
88 }
89 failed := func(err error) {
90 l.Debugf("Failed: %s", err)
91 r.Reply(false, []byte(err.Error()))
92 }
93 if r.Type != "config" {
94 failed(s.Errorf("expecting config request"))
95 return
96 }
97 c, err := settings.DecodeConfig(r.Payload)
98 if err != nil {
99 failed(s.Errorf("invalid config"))
100 return
101 }
102 //print if client and server versions dont match
103 if c.Version != chshare.BuildVersion {
104 v := c.Version
105 if v == "" {
106 v = "<unknown>"
107 }
108 l.Infof("Client version (%s) differs from server version (%s)",
109 v, chshare.BuildVersion)
110 }
111 //validate remotes
112 for _, r := range c.Remotes {
113 //if user is provided, ensure they have
114 //access to the desired remotes
115 if user != nil {
116 addr := r.UserAddr()
117 if !user.HasAccess(addr) {
118 failed(s.Errorf("access to '%s' denied", addr))
119 return
120 }
121 }
122 //confirm reverse tunnels are allowed
123 if r.Reverse && !s.config.Reverse {
124 l.Debugf("Denied reverse port forwarding request, please enable --reverse")
125 failed(s.Errorf("Reverse port forwaring not enabled on server"))
126 return
127 }
128 //confirm reverse tunnel is available
129 if r.Reverse && !r.CanListen() {
130 failed(s.Errorf("Server cannot listen on %s", r.String()))
131 return
132 }
133 }
134 //successfuly validated config!
135 r.Reply(true, nil)
136 //tunnel per ssh connection
137 tunnel := tunnel.New(tunnel.Config{
138 Logger: l,
139 Inbound: s.config.Reverse,
140 Outbound: true, //server always accepts outbound
141 Socks: s.config.Socks5,
142 KeepAlive: s.config.KeepAlive,
143 })
144 //bind
145 eg, ctx := errgroup.WithContext(req.Context())
146 eg.Go(func() error {
147 //connected, handover ssh connection for tunnel to use, and block
148 return tunnel.BindSSH(ctx, sshConn, reqs, chans)
149 })
150 eg.Go(func() error {
151 //connected, setup reversed-remotes?
152 serverInbound := c.Remotes.Reversed(true)
153 if len(serverInbound) == 0 {
154 return nil
155 }
156 //block
157 return tunnel.BindRemotes(ctx, serverInbound)
158 })
159 err = eg.Wait()
160 if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
161 l.Debugf("Closed connection (%s)", err)
162 } else {
163 l.Debugf("Closed connection")
164 }
165 }
0 package chserver
1
2 import (
3 "crypto/tls"
4 "crypto/x509"
5 "errors"
6 "io/ioutil"
7 "net"
8 "os"
9 "os/user"
10 "path/filepath"
11
12 "github.com/jpillora/chisel/share/settings"
13 "golang.org/x/crypto/acme/autocert"
14 )
15
16 //TLSConfig enables configures TLS
17 type TLSConfig struct {
18 Key string
19 Cert string
20 Domains []string
21 CA string
22 }
23
24 func (s *Server) listener(host, port string) (net.Listener, error) {
25 hasDomains := len(s.config.TLS.Domains) > 0
26 hasKeyCert := s.config.TLS.Key != "" && s.config.TLS.Cert != ""
27 if hasDomains && hasKeyCert {
28 return nil, errors.New("cannot use key/cert and domains")
29 }
30 var tlsConf *tls.Config
31 if hasDomains {
32 tlsConf = s.tlsLetsEncrypt(s.config.TLS.Domains)
33 }
34 extra := ""
35 if hasKeyCert {
36 c, err := s.tlsKeyCert(s.config.TLS.Key, s.config.TLS.Cert, s.config.TLS.CA)
37 if err != nil {
38 return nil, err
39 }
40 tlsConf = c
41 if port != "443" && hasDomains {
42 extra = " (WARNING: LetsEncrypt will attempt to connect to your domain on port 443)"
43 }
44 }
45 //tcp listen
46 l, err := net.Listen("tcp", host+":"+port)
47 if err != nil {
48 return nil, err
49 }
50 //optionally wrap in tls
51 proto := "http"
52 if tlsConf != nil {
53 proto += "s"
54 l = tls.NewListener(l, tlsConf)
55 }
56 if err == nil {
57 s.Infof("Listening on %s://%s:%s%s", proto, host, port, extra)
58 }
59 return l, nil
60 }
61
62 func (s *Server) tlsLetsEncrypt(domains []string) *tls.Config {
63 //prepare cert manager
64 m := &autocert.Manager{
65 Prompt: func(tosURL string) bool {
66 s.Infof("Accepting LetsEncrypt TOS and fetching certificate...")
67 return true
68 },
69 Email: settings.Env("LE_EMAIL"),
70 HostPolicy: autocert.HostWhitelist(domains...),
71 }
72 //configure file cache
73 c := settings.Env("LE_CACHE")
74 if c == "" {
75 h := os.Getenv("HOME")
76 if h == "" {
77 if u, err := user.Current(); err == nil {
78 h = u.HomeDir
79 }
80 }
81 c = filepath.Join(h, ".cache", "chisel")
82 }
83 if c != "-" {
84 s.Infof("LetsEncrypt cache directory %s", c)
85 m.Cache = autocert.DirCache(c)
86 }
87 //return lets-encrypt tls config
88 return m.TLSConfig()
89 }
90
91 func (s *Server) tlsKeyCert(key, cert string, ca string) (*tls.Config, error) {
92 keypair, err := tls.LoadX509KeyPair(cert, key)
93 if err != nil {
94 return nil, err
95 }
96 //file based tls config using tls defaults
97 c := &tls.Config{
98 Certificates: []tls.Certificate{keypair},
99 }
100 //mTLS requires server's CA
101 if ca != "" {
102 if err := addCA(ca, c); err != nil {
103 return nil, err
104 }
105 s.Infof("Loaded CA path: %s", ca)
106 }
107 return c, nil
108 }
109
110 func addCA(ca string, c *tls.Config) error {
111 fileInfo, err := os.Stat(ca)
112 if err != nil {
113 return err
114 }
115 clientCAPool := x509.NewCertPool()
116 if fileInfo.IsDir() {
117 //this is a directory holding CA bundle files
118 files, err := ioutil.ReadDir(ca)
119 if err != nil {
120 return err
121 }
122 //add all cert files from path
123 for _, file := range files {
124 f := file.Name()
125 if err := addPEMFile(filepath.Join(ca, f), clientCAPool); err != nil {
126 return err
127 }
128 }
129 } else {
130 //this is a CA bundle file
131 if err := addPEMFile(ca, clientCAPool); err != nil {
132 return err
133 }
134 }
135 //set client CAs and enable cert verification
136 c.ClientCAs = clientCAPool
137 c.ClientAuth = tls.RequireAndVerifyClientCert
138 return nil
139 }
140
141 func addPEMFile(path string, pool *x509.CertPool) error {
142 content, err := ioutil.ReadFile(path)
143 if err != nil {
144 return err
145 }
146 if !pool.AppendCertsFromPEM(content) {
147 return errors.New("Fail to load certificates from : " + path)
148 }
149 return nil
150 }
0 package ccrypto
1
2 // Deterministic crypto.Reader
3 // overview: half the result is used as the output
4 // [a|...] -> sha512(a) -> [b|output] -> sha512(b)
5
6 import (
7 "crypto/sha512"
8 "io"
9 )
10
11 const DetermRandIter = 2048
12
13 func NewDetermRand(seed []byte) io.Reader {
14 var out []byte
15 //strengthen seed
16 var next = seed
17 for i := 0; i < DetermRandIter; i++ {
18 next, out = hash(next)
19 }
20 return &determRand{
21 next: next,
22 out: out,
23 }
24 }
25
26 type determRand struct {
27 next, out []byte
28 }
29
30 func (d *determRand) Read(b []byte) (int, error) {
31 n := 0
32 l := len(b)
33 for n < l {
34 next, out := hash(d.next)
35 n += copy(b[n:], out)
36 d.next = next
37 }
38 return n, nil
39 }
40
41 func hash(input []byte) (next []byte, output []byte) {
42 nextout := sha512.Sum512(input)
43 return nextout[:sha512.Size/2], nextout[sha512.Size/2:]
44 }
0 package ccrypto
1
2 import (
3 "crypto/ecdsa"
4 "crypto/elliptic"
5 "crypto/rand"
6 "crypto/sha256"
7 "crypto/x509"
8 "encoding/base64"
9 "encoding/pem"
10 "fmt"
11
12 "golang.org/x/crypto/ssh"
13 )
14
15 //GenerateKey for use as an SSH private key
16 func GenerateKey(seed string) ([]byte, error) {
17 r := rand.Reader
18 if seed != "" {
19 r = NewDetermRand([]byte(seed))
20 }
21 priv, err := ecdsa.GenerateKey(elliptic.P256(), r)
22 if err != nil {
23 return nil, err
24 }
25 b, err := x509.MarshalECPrivateKey(priv)
26 if err != nil {
27 return nil, fmt.Errorf("Unable to marshal ECDSA private key: %v", err)
28 }
29 return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil
30 }
31
32 //FingerprintKey calculates the SHA256 hash of an SSH public key
33 func FingerprintKey(k ssh.PublicKey) string {
34 bytes := sha256.Sum256(k.Marshal())
35 return base64.StdEncoding.EncodeToString(bytes[:])
36 }
0 package cio
1
2 import (
3 "fmt"
4 "log"
5 "os"
6 )
7
8 //Logger is pkg/log Logger with prefixing and 2 log levels
9 type Logger struct {
10 Info, Debug bool
11 //internal
12 prefix string
13 logger *log.Logger
14 info, debug *bool
15 }
16
17 func NewLogger(prefix string) *Logger {
18 return NewLoggerFlag(prefix, log.Ldate|log.Ltime)
19 }
20
21 func NewLoggerFlag(prefix string, flag int) *Logger {
22 l := &Logger{
23 prefix: prefix,
24 logger: log.New(os.Stderr, "", flag),
25 Info: false,
26 Debug: false,
27 }
28 return l
29 }
30
31 func (l *Logger) Infof(f string, args ...interface{}) {
32 if l.IsInfo() {
33 l.logger.Printf(l.prefix+": "+f, args...)
34 }
35 }
36
37 func (l *Logger) Debugf(f string, args ...interface{}) {
38 if l.IsDebug() {
39 l.logger.Printf(l.prefix+": "+f, args...)
40 }
41 }
42
43 func (l *Logger) Errorf(f string, args ...interface{}) error {
44 return fmt.Errorf(l.prefix+": "+f, args...)
45 }
46
47 func (l *Logger) Fork(prefix string, args ...interface{}) *Logger {
48 //slip the parent prefix at the front
49 args = append([]interface{}{l.prefix}, args...)
50 ll := NewLogger(fmt.Sprintf("%s: "+prefix, args...))
51 //store link to parent settings too
52 ll.Info = l.Info
53 if l.info != nil {
54 ll.info = l.info
55 } else {
56 ll.info = &l.Info
57 }
58 ll.Debug = l.Debug
59 if l.debug != nil {
60 ll.debug = l.debug
61 } else {
62 ll.debug = &l.Debug
63 }
64 return ll
65 }
66
67 func (l *Logger) Prefix() string {
68 return l.prefix
69 }
70
71 func (l *Logger) IsInfo() bool {
72 return l.Info || (l.info != nil && *l.info)
73 }
74
75 func (l *Logger) IsDebug() bool {
76 return l.Debug || (l.debug != nil && *l.debug)
77 }
0 package cio
1
2 import (
3 "io"
4 "log"
5 "sync"
6 )
7
8 func Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) {
9 var sent, received int64
10 var wg sync.WaitGroup
11 var o sync.Once
12 close := func() {
13 src.Close()
14 dst.Close()
15 }
16 wg.Add(2)
17 go func() {
18 received, _ = io.Copy(src, dst)
19 o.Do(close)
20 wg.Done()
21 }()
22 go func() {
23 sent, _ = io.Copy(dst, src)
24 o.Do(close)
25 wg.Done()
26 }()
27 wg.Wait()
28 return sent, received
29 }
30
31 const vis = false
32
33 type pipeVisPrinter struct {
34 name string
35 }
36
37 func (p pipeVisPrinter) Write(b []byte) (int, error) {
38 log.Printf(">>> %s: %x", p.name, b)
39 return len(b), nil
40 }
41
42 func pipeVis(name string, r io.Reader) io.Reader {
43 if vis {
44 return io.TeeReader(r, pipeVisPrinter{name})
45 }
46 return r
47 }
0 package cio
1
2 import (
3 "io"
4 "io/ioutil"
5 "os"
6 )
7
8 //Stdio as a ReadWriteCloser
9 var Stdio = &struct {
10 io.ReadCloser
11 io.Writer
12 }{
13 ioutil.NopCloser(os.Stdin),
14 os.Stdout,
15 }
0 package cnet
1
2 import (
3 "io"
4 "net"
5 "time"
6 )
7
8 type rwcConn struct {
9 io.ReadWriteCloser
10 buff []byte
11 }
12
13 //NewRWCConn converts a RWC into a net.Conn
14 func NewRWCConn(rwc io.ReadWriteCloser) net.Conn {
15 c := rwcConn{
16 ReadWriteCloser: rwc,
17 }
18 return &c
19 }
20
21 func (c *rwcConn) LocalAddr() net.Addr {
22 return c
23 }
24
25 func (c *rwcConn) RemoteAddr() net.Addr {
26 return c
27 }
28
29 func (c *rwcConn) Network() string {
30 return "tcp"
31 }
32
33 func (c *rwcConn) String() string {
34 return ""
35 }
36
37 func (c *rwcConn) SetDeadline(t time.Time) error {
38 return nil //no-op
39 }
40
41 func (c *rwcConn) SetReadDeadline(t time.Time) error {
42 return nil //no-op
43 }
44
45 func (c *rwcConn) SetWriteDeadline(t time.Time) error {
46 return nil //no-op
47 }
0 package cnet
1
2 import (
3 "net"
4 "time"
5
6 "github.com/gorilla/websocket"
7 )
8
9 type wsConn struct {
10 *websocket.Conn
11 buff []byte
12 }
13
14 //NewWebSocketConn converts a websocket.Conn into a net.Conn
15 func NewWebSocketConn(websocketConn *websocket.Conn) net.Conn {
16 c := wsConn{
17 Conn: websocketConn,
18 }
19 return &c
20 }
21
22 //Read is not threadsafe though thats okay since there
23 //should never be more than one reader
24 func (c *wsConn) Read(dst []byte) (int, error) {
25 ldst := len(dst)
26 //use buffer or read new message
27 var src []byte
28 if len(c.buff) > 0 {
29 src = c.buff
30 c.buff = nil
31 } else if _, msg, err := c.Conn.ReadMessage(); err == nil {
32 src = msg
33 } else {
34 return 0, err
35 }
36 //copy src->dest
37 var n int
38 if len(src) > ldst {
39 //copy as much as possible of src into dst
40 n = copy(dst, src[:ldst])
41 //copy remainder into buffer
42 r := src[ldst:]
43 lr := len(r)
44 c.buff = make([]byte, lr)
45 copy(c.buff, r)
46 } else {
47 //copy all of src into dst
48 n = copy(dst, src)
49 }
50 //return bytes copied
51 return n, nil
52 }
53
54 func (c *wsConn) Write(b []byte) (int, error) {
55 if err := c.Conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
56 return 0, err
57 }
58 n := len(b)
59 return n, nil
60 }
61
62 func (c *wsConn) SetDeadline(t time.Time) error {
63 if err := c.Conn.SetReadDeadline(t); err != nil {
64 return err
65 }
66 return c.Conn.SetWriteDeadline(t)
67 }
0 package cnet
1
2 import (
3 "fmt"
4 "sync/atomic"
5 )
6
7 //ConnCount is a connection counter
8 type ConnCount struct {
9 count int32
10 open int32
11 }
12
13 func (c *ConnCount) New() int32 {
14 return atomic.AddInt32(&c.count, 1)
15 }
16
17 func (c *ConnCount) Open() {
18 atomic.AddInt32(&c.open, 1)
19 }
20
21 func (c *ConnCount) Close() {
22 atomic.AddInt32(&c.open, -1)
23 }
24
25 func (c *ConnCount) String() string {
26 return fmt.Sprintf("[%d/%d]", atomic.LoadInt32(&c.open), atomic.LoadInt32(&c.count))
27 }
0 package cnet
1
2 import (
3 "context"
4 "errors"
5 "net"
6 "net/http"
7 "sync"
8
9 "golang.org/x/sync/errgroup"
10 )
11
12 //HTTPServer extends net/http Server and
13 //adds graceful shutdowns
14 type HTTPServer struct {
15 *http.Server
16 waiterMux sync.Mutex
17 waiter *errgroup.Group
18 listenErr error
19 }
20
21 //NewHTTPServer creates a new HTTPServer
22 func NewHTTPServer() *HTTPServer {
23 return &HTTPServer{
24 Server: &http.Server{},
25 }
26
27 }
28
29 func (h *HTTPServer) GoListenAndServe(addr string, handler http.Handler) error {
30 return h.GoListenAndServeContext(context.Background(), addr, handler)
31 }
32
33 func (h *HTTPServer) GoListenAndServeContext(ctx context.Context, addr string, handler http.Handler) error {
34 if ctx == nil {
35 return errors.New("ctx must be set")
36 }
37 l, err := net.Listen("tcp", addr)
38 if err != nil {
39 return err
40 }
41 return h.GoServe(ctx, l, handler)
42 }
43
44 func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.Handler) error {
45 if ctx == nil {
46 return errors.New("ctx must be set")
47 }
48 h.waiterMux.Lock()
49 defer h.waiterMux.Unlock()
50 h.Handler = handler
51 h.waiter, ctx = errgroup.WithContext(ctx)
52 h.waiter.Go(func() error {
53 return h.Serve(l)
54 })
55 go func() {
56 <-ctx.Done()
57 h.Close()
58 }()
59 return nil
60 }
61
62 func (h *HTTPServer) Close() error {
63 h.waiterMux.Lock()
64 defer h.waiterMux.Unlock()
65 if h.waiter == nil {
66 return errors.New("not started yet")
67 }
68 return h.Server.Close()
69 }
70
71 func (h *HTTPServer) Wait() error {
72 h.waiterMux.Lock()
73 unset := h.waiter == nil
74 h.waiterMux.Unlock()
75 if unset {
76 return errors.New("not started yet")
77 }
78 h.waiterMux.Lock()
79 wait := h.waiter.Wait
80 h.waiterMux.Unlock()
81 err := wait()
82 if err == http.ErrServerClosed {
83 err = nil //success
84 }
85 return err
86 }
0 package cnet
1
2 import (
3 "io"
4 "net"
5 "sync/atomic"
6 "time"
7
8 "github.com/jpillora/chisel/share/cio"
9 "github.com/jpillora/sizestr"
10 )
11
12 //NewMeter to measure readers/writers
13 func NewMeter(l *cio.Logger) *Meter {
14 return &Meter{l: l}
15 }
16
17 //Meter can be inserted in the path or
18 //of a reader or writer to measure the
19 //throughput
20 type Meter struct {
21 //meter state
22 sent, recv int64
23 //print state
24 l *cio.Logger
25 printing uint32
26 last int64
27 lsent, lrecv int64
28 }
29
30 func (m *Meter) print() {
31 //move out of the read/write path asap
32 if atomic.CompareAndSwapUint32(&m.printing, 0, 1) {
33 go m.goprint()
34 }
35 }
36
37 func (m *Meter) goprint() {
38 time.Sleep(time.Second)
39 //snapshot
40 s := atomic.LoadInt64(&m.sent)
41 r := atomic.LoadInt64(&m.recv)
42 //compute speed
43 curr := time.Now().UnixNano()
44 last := atomic.LoadInt64(&m.last)
45 dt := time.Duration(curr-last) * time.Nanosecond
46 ls := atomic.LoadInt64(&m.lsent)
47 lr := atomic.LoadInt64(&m.lrecv)
48 //DEBUG
49 // m.l.Infof("%s = %d(%d-%d), %d(%d-%d)", dt, s-ls, s, ls, r-lr, r, lr)
50 //scale to per second V=D/T
51 sps := int64(float64(s-ls) / float64(dt) * float64(time.Second))
52 rps := int64(float64(r-lr) / float64(dt) * float64(time.Second))
53 if last > 0 && (sps != 0 || rps != 0) {
54 m.l.Debugf("write %s/s read %s/s", sizestr.ToString(sps), sizestr.ToString(rps))
55 }
56 //record last printed
57 atomic.StoreInt64(&m.lsent, s)
58 atomic.StoreInt64(&m.lrecv, r)
59 //done
60 atomic.StoreInt64(&m.last, curr)
61 atomic.StoreUint32(&m.printing, 0)
62 }
63
64 //TeeReader inserts Meter into the read path
65 //if the linked logger is in debug mode,
66 //otherwise this is a no-op
67 func (m *Meter) TeeReader(r io.Reader) io.Reader {
68 if m.l.IsDebug() {
69 return &meterReader{m, r}
70 }
71 return r
72 }
73
74 type meterReader struct {
75 *Meter
76 inner io.Reader
77 }
78
79 func (m *meterReader) Read(p []byte) (n int, err error) {
80 n, err = m.inner.Read(p)
81 atomic.AddInt64(&m.recv, int64(n))
82 m.Meter.print()
83 return
84 }
85
86 //TeeWriter inserts Meter into the write path
87 //if the linked logger is in debug mode,
88 //otherwise this is a no-op
89 func (m *Meter) TeeWriter(w io.Writer) io.Writer {
90 if m.l.IsDebug() {
91 return &meterWriter{m, w}
92 }
93 return w
94 }
95
96 type meterWriter struct {
97 *Meter
98 inner io.Writer
99 }
100
101 func (m *meterWriter) Write(p []byte) (n int, err error) {
102 n, err = m.inner.Write(p)
103 atomic.AddInt64(&m.sent, int64(n))
104 m.Meter.print()
105 return
106 }
107
108 //MeterConn inserts Meter into the connection path
109 //if the linked logger is in debug mode,
110 //otherwise this is a no-op
111 func MeterConn(l *cio.Logger, conn net.Conn) net.Conn {
112 m := NewMeter(l)
113 return &meterConn{
114 mread: m.TeeReader(conn),
115 mwrite: m.TeeWriter(conn),
116 Conn: conn,
117 }
118 }
119
120 type meterConn struct {
121 mread io.Reader
122 mwrite io.Writer
123 net.Conn
124 }
125
126 func (m *meterConn) Read(p []byte) (n int, err error) {
127 return m.mread.Read(p)
128 }
129
130 func (m *meterConn) Write(p []byte) (n int, err error) {
131 return m.mwrite.Write(p)
132 }
133
134 //MeterRWC inserts Meter into the RWC path
135 //if the linked logger is in debug mode,
136 //otherwise this is a no-op
137 func MeterRWC(l *cio.Logger, rwc io.ReadWriteCloser) io.ReadWriteCloser {
138 m := NewMeter(l)
139 return &struct {
140 io.Reader
141 io.Writer
142 io.Closer
143 }{
144 Reader: m.TeeReader(rwc),
145 Writer: m.TeeWriter(rwc),
146 Closer: rwc,
147 }
148 }
0 package chshare
1
2 //this file exists to maintain backwards compatibility
3
4 import (
5 "github.com/jpillora/chisel/share/ccrypto"
6 "github.com/jpillora/chisel/share/cio"
7 "github.com/jpillora/chisel/share/cnet"
8 "github.com/jpillora/chisel/share/cos"
9 "github.com/jpillora/chisel/share/settings"
10 "github.com/jpillora/chisel/share/tunnel"
11 )
12
13 const (
14 DetermRandIter = ccrypto.DetermRandIter
15 )
16
17 type (
18 Config = settings.Config
19 Remote = settings.Remote
20 Remotes = settings.Remotes
21 User = settings.User
22 Users = settings.Users
23 UserIndex = settings.UserIndex
24 HTTPServer = cnet.HTTPServer
25 ConnStats = cnet.ConnCount
26 Logger = cio.Logger
27 TCPProxy = tunnel.Proxy
28 )
29
30 var (
31 NewDetermRand = ccrypto.NewDetermRand
32 GenerateKey = ccrypto.GenerateKey
33 FingerprintKey = ccrypto.FingerprintKey
34 Pipe = cio.Pipe
35 NewLoggerFlag = cio.NewLoggerFlag
36 NewLogger = cio.NewLogger
37 Stdio = cio.Stdio
38 DecodeConfig = settings.DecodeConfig
39 DecodeRemote = settings.DecodeRemote
40 NewUsers = settings.NewUsers
41 NewUserIndex = settings.NewUserIndex
42 UserAllowAll = settings.UserAllowAll
43 ParseAuth = settings.ParseAuth
44 NewRWCConn = cnet.NewRWCConn
45 NewWebSocketConn = cnet.NewWebSocketConn
46 NewHTTPServer = cnet.NewHTTPServer
47 GoStats = cos.GoStats
48 SleepSignal = cos.SleepSignal
49 NewTCPProxy = tunnel.NewProxy
50 )
51
52 //EncodeConfig old version
53 func EncodeConfig(c *settings.Config) ([]byte, error) {
54 return settings.EncodeConfig(*c), nil
55 }
0 package cos
1
2 import (
3 "context"
4 "os"
5 "os/signal"
6 "time"
7 )
8
9 //InterruptContext returns a context which is
10 //cancelled on OS Interrupt
11 func InterruptContext() context.Context {
12 ctx, cancel := context.WithCancel(context.Background())
13 go func() {
14 sig := make(chan os.Signal, 1)
15 signal.Notify(sig, os.Interrupt) //windows compatible?
16 <-sig
17 signal.Stop(sig)
18 cancel()
19 }()
20 return ctx
21 }
22
23 //SleepSignal sleeps for the given duration,
24 //or until a SIGHUP is received
25 func SleepSignal(d time.Duration) {
26 <-AfterSignal(d)
27 }
0 // +build pprof
1
2 package cos
3
4 import (
5 "log"
6 "net/http"
7 _ "net/http/pprof" //import http profiler api
8 )
9
10 func init() {
11 go func() {
12 log.Fatal(http.ListenAndServe("localhost:6060", nil))
13 }()
14 log.Printf("[pprof] listening on 6060")
15 }
0 //+build !windows
1
2 package cos
3
4 import (
5 "log"
6 "os"
7 "os/signal"
8 "runtime"
9 "syscall"
10 "time"
11
12 "github.com/jpillora/sizestr"
13 )
14
15 //GoStats prints statistics to
16 //stdout on SIGUSR2 (posix-only)
17 func GoStats() {
18 //silence complaints from windows
19 const SIGUSR2 = syscall.Signal(0x1f)
20 time.Sleep(time.Second)
21 c := make(chan os.Signal, 1)
22 signal.Notify(c, SIGUSR2)
23 for range c {
24 memStats := runtime.MemStats{}
25 runtime.ReadMemStats(&memStats)
26 log.Printf("recieved SIGUSR2, go-routines: %d, go-memory-usage: %s",
27 runtime.NumGoroutine(),
28 sizestr.ToString(int64(memStats.Alloc)))
29 }
30 }
31
32 //AfterSignal returns a channel which will be closed
33 //after the given duration or until a SIGHUP is received
34 func AfterSignal(d time.Duration) <-chan struct{} {
35 ch := make(chan struct{})
36 go func() {
37 sig := make(chan os.Signal, 1)
38 signal.Notify(sig, syscall.SIGHUP)
39 select {
40 case <-time.After(d):
41 case <-sig:
42 }
43 signal.Stop(sig)
44 close(ch)
45 }()
46 return ch
47 }
0 //+build windows
1
2 package cos
3
4 import (
5 "time"
6 )
7
8 func GoStats() {
9 //noop
10 }
11
12 func AfterSignal(d time.Duration) <-chan struct{} {
13 ch := make(chan struct{})
14 go func() {
15 <-time.After(d)
16 close(ch)
17 }()
18 return ch
19 }
0 package settings
1
2 import (
3 "encoding/json"
4 "fmt"
5 )
6
7 type Config struct {
8 Version string
9 Remotes
10 }
11
12 func DecodeConfig(b []byte) (*Config, error) {
13 c := &Config{}
14 err := json.Unmarshal(b, c)
15 if err != nil {
16 return nil, fmt.Errorf("Invalid JSON config")
17 }
18 return c, nil
19 }
20
21 func EncodeConfig(c Config) []byte {
22 //Config doesn't have types that can fail to marshal
23 b, _ := json.Marshal(c)
24 return b
25 }
0 package settings
1
2 import (
3 "os"
4 "strconv"
5 "time"
6 )
7
8 //Env returns a chisel environment variable
9 func Env(name string) string {
10 return os.Getenv("CHISEL_" + name)
11 }
12
13 //EnvInt returns an integer using an environment variable, with a default fallback
14 func EnvInt(name string, def int) int {
15 if n, err := strconv.Atoi(Env(name)); err == nil {
16 return n
17 }
18 return def
19 }
20
21 //EnvDuration returns a duration using an environment variable, with a default fallback
22 func EnvDuration(name string, def time.Duration) time.Duration {
23 if n, err := time.ParseDuration(Env(name)); err == nil {
24 return n
25 }
26 return def
27 }
0 package settings
1
2 import (
3 "errors"
4 "net"
5 "net/url"
6 "regexp"
7 "strconv"
8 "strings"
9 )
10
11 // short-hand conversions (see remote_test)
12 // 3000 ->
13 // local 127.0.0.1:3000
14 // remote 127.0.0.1:3000
15 // foobar.com:3000 ->
16 // local 127.0.0.1:3000
17 // remote foobar.com:3000
18 // 3000:google.com:80 ->
19 // local 127.0.0.1:3000
20 // remote google.com:80
21 // 192.168.0.1:3000:google.com:80 ->
22 // local 192.168.0.1:3000
23 // remote google.com:80
24 // 127.0.0.1:1080:socks
25 // local 127.0.0.1:1080
26 // remote socks
27 // stdio:example.com:22
28 // local stdio
29 // remote example.com:22
30 // 1.1.1.1:53/udp
31 // local 127.0.0.1:53/udp
32 // remote 1.1.1.1:53/udp
33
34 type Remote struct {
35 LocalHost, LocalPort, LocalProto string
36 RemoteHost, RemotePort, RemoteProto string
37 Socks, Reverse, Stdio bool
38 }
39
40 const revPrefix = "R:"
41
42 func DecodeRemote(s string) (*Remote, error) {
43 reverse := false
44 if strings.HasPrefix(s, revPrefix) {
45 s = strings.TrimPrefix(s, revPrefix)
46 reverse = true
47 }
48 parts := regexp.MustCompile(`(\[[^\[\]]+\]|[^\[\]:]+):?`).FindAllStringSubmatch(s, -1)
49 if len(parts) <= 0 || len(parts) >= 5 {
50 return nil, errors.New("Invalid remote")
51 }
52 r := &Remote{Reverse: reverse}
53 //parse from back to front, to set 'remote' fields first,
54 //then to set 'local' fields second (allows the 'remote' side
55 //to provide the defaults)
56 for i := len(parts) - 1; i >= 0; i-- {
57 p := parts[i][1]
58 //remote portion is socks?
59 if i == len(parts)-1 && p == "socks" {
60 r.Socks = true
61 continue
62 }
63 //local portion is stdio?
64 if i == 0 && p == "stdio" {
65 r.Stdio = true
66 continue
67 }
68 p, proto := L4Proto(p)
69 if proto != "" {
70 if r.RemotePort == "" {
71 r.RemoteProto = proto
72 } else if r.LocalProto == "" {
73 r.LocalProto = proto
74 }
75 }
76 if isPort(p) {
77 if !r.Socks && r.RemotePort == "" {
78 r.RemotePort = p
79 }
80 r.LocalPort = p
81 continue
82 }
83 if !r.Socks && (r.RemotePort == "" && r.LocalPort == "") {
84 return nil, errors.New("Missing ports")
85 }
86 if !isHost(p) {
87 return nil, errors.New("Invalid host")
88 }
89 if !r.Socks && r.RemoteHost == "" {
90 r.RemoteHost = p
91 } else {
92 r.LocalHost = p
93 }
94 }
95 //remote string parsed, apply defaults...
96 if r.Socks {
97 //socks defaults
98 if r.LocalHost == "" {
99 r.LocalHost = "127.0.0.1"
100 }
101 if r.LocalPort == "" {
102 r.LocalPort = "1080"
103 }
104 } else {
105 //non-socks defaults
106 if r.LocalHost == "" {
107 r.LocalHost = "0.0.0.0"
108 }
109 if r.RemoteHost == "" {
110 r.RemoteHost = "127.0.0.1"
111 }
112 }
113 if r.RemoteProto == "" {
114 r.RemoteProto = "tcp"
115 }
116 if r.LocalProto == "" {
117 r.LocalProto = r.RemoteProto
118 }
119 if r.LocalProto != r.RemoteProto {
120 //TODO support cross protocol
121 //tcp <-> udp, is faily straight forward
122 //udp <-> tcp, is trickier since udp is stateless and tcp is not
123 return nil, errors.New("cross-protocol remotes are not supported yet")
124 }
125 if r.Socks && r.RemoteProto != "tcp" {
126 return nil, errors.New("only TCP SOCKS is supported")
127 }
128 if r.Stdio && r.Reverse {
129 return nil, errors.New("stdio cannot be reversed")
130 }
131 return r, nil
132 }
133
134 func isPort(s string) bool {
135 n, err := strconv.Atoi(s)
136 if err != nil {
137 return false
138 }
139 if n <= 0 || n > 65535 {
140 return false
141 }
142 return true
143 }
144
145 func isHost(s string) bool {
146 _, err := url.Parse("//" + s)
147 if err != nil {
148 return false
149 }
150 return true
151 }
152
153 var l4Proto = regexp.MustCompile(`(?i)\/(tcp|udp)$`)
154
155 //L4Proto extacts the layer-4 protocol from the given string
156 func L4Proto(s string) (head, proto string) {
157 if l4Proto.MatchString(s) {
158 l := len(s)
159 return strings.ToLower(s[:l-4]), s[l-3:]
160 }
161 return s, ""
162 }
163
164 //implement Stringer
165 func (r Remote) String() string {
166 sb := strings.Builder{}
167 if r.Reverse {
168 sb.WriteString(revPrefix)
169 }
170 sb.WriteString(strings.TrimPrefix(r.Local(), "0.0.0.0:"))
171 sb.WriteString("=>")
172 sb.WriteString(strings.TrimPrefix(r.Remote(), "127.0.0.1:"))
173 if r.RemoteProto == "udp" {
174 sb.WriteString("/udp")
175 }
176 return sb.String()
177 }
178
179 //Encode remote to a string
180 func (r Remote) Encode() string {
181 if r.LocalPort == "" {
182 r.LocalPort = r.RemotePort
183 }
184 local := r.Local()
185 remote := r.Remote()
186 if r.RemoteProto == "udp" {
187 remote += "/udp"
188 }
189 if r.Reverse {
190 return "R:" + local + ":" + remote
191 }
192 return local + ":" + remote
193 }
194
195 //Local is the decodable local portion
196 func (r Remote) Local() string {
197 if r.Stdio {
198 return "stdio"
199 }
200 if r.LocalHost == "" {
201 r.LocalHost = "0.0.0.0"
202 }
203 return r.LocalHost + ":" + r.LocalPort
204 }
205
206 //Remote is the decodable remote portion
207 func (r Remote) Remote() string {
208 if r.Socks {
209 return "socks"
210 }
211 if r.RemoteHost == "" {
212 r.RemoteHost = "127.0.0.1"
213 }
214 return r.RemoteHost + ":" + r.RemotePort
215 }
216
217 //UserAddr is checked when checking if a
218 //user has access to a given remote
219 func (r Remote) UserAddr() string {
220 if r.Reverse {
221 return "R:" + r.LocalHost + ":" + r.LocalPort
222 }
223 return r.RemoteHost + ":" + r.RemotePort
224 }
225
226 //CanListen checks if the port can be listened on
227 func (r Remote) CanListen() bool {
228 //valid protocols
229 switch r.LocalProto {
230 case "tcp":
231 conn, err := net.Listen("tcp", r.Local())
232 if err == nil {
233 conn.Close()
234 return true
235 }
236 return false
237 case "udp":
238 addr, err := net.ResolveUDPAddr("udp", r.Local())
239 if err != nil {
240 return false
241 }
242 conn, err := net.ListenUDP(r.LocalProto, addr)
243 if err == nil {
244 conn.Close()
245 return true
246 }
247 return false
248 }
249 //invalid
250 return false
251 }
252
253 type Remotes []*Remote
254
255 //Filter out forward reversed/non-reversed remotes
256 func (rs Remotes) Reversed(reverse bool) Remotes {
257 subset := Remotes{}
258 for _, r := range rs {
259 match := r.Reverse == reverse
260 if match {
261 subset = append(subset, r)
262 }
263 }
264 return subset
265 }
266
267 //Encode back into strings
268 func (rs Remotes) Encode() []string {
269 s := make([]string, len(rs))
270 for i, r := range rs {
271 s[i] = r.Encode()
272 }
273 return s
274 }
0 package settings
1
2 import (
3 "reflect"
4 "testing"
5 )
6
7 func TestRemoteDecode(t *testing.T) {
8 //test table
9 for i, test := range []struct {
10 Input string
11 Output Remote
12 Encoded string
13 }{
14 {
15 "3000",
16 Remote{
17 LocalPort: "3000",
18 RemoteHost: "127.0.0.1",
19 RemotePort: "3000",
20 },
21 "0.0.0.0:3000:127.0.0.1:3000",
22 },
23 {
24 "google.com:80",
25 Remote{
26 LocalPort: "80",
27 RemoteHost: "google.com",
28 RemotePort: "80",
29 },
30 "0.0.0.0:80:google.com:80",
31 },
32 {
33 "R:google.com:80",
34 Remote{
35 LocalPort: "80",
36 RemoteHost: "google.com",
37 RemotePort: "80",
38 Reverse: true,
39 },
40 "R:0.0.0.0:80:google.com:80",
41 },
42 {
43 "示例網站.com:80",
44 Remote{
45 LocalPort: "80",
46 RemoteHost: "示例網站.com",
47 RemotePort: "80",
48 },
49 "0.0.0.0:80:示例網站.com:80",
50 },
51 {
52 "socks",
53 Remote{
54 LocalHost: "127.0.0.1",
55 LocalPort: "1080",
56 Socks: true,
57 },
58 "127.0.0.1:1080:socks",
59 },
60 {
61 "127.0.0.1:1081:socks",
62 Remote{
63 LocalHost: "127.0.0.1",
64 LocalPort: "1081",
65 Socks: true,
66 },
67 "127.0.0.1:1081:socks",
68 },
69 {
70 "1.1.1.1:53/udp",
71 Remote{
72 LocalPort: "53",
73 LocalProto: "udp",
74 RemoteHost: "1.1.1.1",
75 RemotePort: "53",
76 RemoteProto: "udp",
77 },
78 "0.0.0.0:53:1.1.1.1:53/udp",
79 },
80 {
81 "localhost:5353:1.1.1.1:53/udp",
82 Remote{
83 LocalHost: "localhost",
84 LocalPort: "5353",
85 LocalProto: "udp",
86 RemoteHost: "1.1.1.1",
87 RemotePort: "53",
88 RemoteProto: "udp",
89 },
90 "localhost:5353:1.1.1.1:53/udp",
91 },
92 {
93 "[::1]:8080:google.com:80",
94 Remote{
95 LocalHost: "[::1]",
96 LocalPort: "8080",
97 RemoteHost: "google.com",
98 RemotePort: "80",
99 },
100 "[::1]:8080:google.com:80",
101 },
102 {
103 "R:[::]:3000:[::1]:3000",
104 Remote{
105 LocalHost: "[::]",
106 LocalPort: "3000",
107 RemoteHost: "[::1]",
108 RemotePort: "3000",
109 Reverse: true,
110 },
111 "R:[::]:3000:[::1]:3000",
112 },
113 } {
114 //expected defaults
115 expected := test.Output
116 if expected.LocalHost == "" {
117 expected.LocalHost = "0.0.0.0"
118 }
119 if expected.RemoteProto == "" {
120 expected.RemoteProto = "tcp"
121 }
122 if expected.LocalProto == "" {
123 expected.LocalProto = "tcp"
124 }
125 //compare
126 got, err := DecodeRemote(test.Input)
127 if err != nil {
128 t.Fatalf("decode #%d '%s' failed: %s", i+1, test.Input, err)
129 }
130 if !reflect.DeepEqual(got, &expected) {
131 t.Fatalf("decode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, expected, got)
132 }
133 if e := got.Encode(); test.Encoded != e {
134 t.Fatalf("encode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, test.Encoded, e)
135 }
136 }
137 }
0 package settings
1
2 import (
3 "regexp"
4 "strings"
5 )
6
7 var UserAllowAll = regexp.MustCompile("")
8
9 func ParseAuth(auth string) (string, string) {
10 if strings.Contains(auth, ":") {
11 pair := strings.SplitN(auth, ":", 2)
12 return pair[0], pair[1]
13 }
14 return "", ""
15 }
16
17 type User struct {
18 Name string
19 Pass string
20 Addrs []*regexp.Regexp
21 }
22
23 func (u *User) HasAccess(addr string) bool {
24 m := false
25 for _, r := range u.Addrs {
26 if r.MatchString(addr) {
27 m = true
28 break
29 }
30 }
31 return m
32 }
0 package settings
1
2 import (
3 "encoding/json"
4 "errors"
5 "fmt"
6 "io/ioutil"
7 "regexp"
8 "sync"
9
10 "github.com/fsnotify/fsnotify"
11 "github.com/jpillora/chisel/share/cio"
12 )
13
14 type Users struct {
15 sync.RWMutex
16 inner map[string]*User
17 }
18
19 func NewUsers() *Users {
20 return &Users{inner: map[string]*User{}}
21 }
22
23 // Len returns the numbers of users
24 func (u *Users) Len() int {
25 u.RLock()
26 l := len(u.inner)
27 u.RUnlock()
28 return l
29 }
30
31 // Get user from the index by key
32 func (u *Users) Get(key string) (*User, bool) {
33 u.RLock()
34 user, found := u.inner[key]
35 u.RUnlock()
36 return user, found
37 }
38
39 // Set a users into the list by specific key
40 func (u *Users) Set(key string, user *User) {
41 u.Lock()
42 u.inner[key] = user
43 u.Unlock()
44 }
45
46 // Del ete a users from the list
47 func (u *Users) Del(key string) {
48 u.Lock()
49 delete(u.inner, key)
50 u.Unlock()
51 }
52
53 // AddUser adds a users to the set
54 func (u *Users) AddUser(user *User) {
55 u.Set(user.Name, user)
56 }
57
58 // Reset all users to the given set,
59 // Use nil to remove all.
60 func (u *Users) Reset(users []*User) {
61 m := map[string]*User{}
62 for _, u := range users {
63 m[u.Name] = u
64 }
65 u.Lock()
66 u.inner = m
67 u.Unlock()
68 }
69
70 // UserIndex is a reloadable user source
71 type UserIndex struct {
72 *cio.Logger
73 *Users
74 configFile string
75 }
76
77 // NewUserIndex creates a source for users
78 func NewUserIndex(logger *cio.Logger) *UserIndex {
79 return &UserIndex{
80 Logger: logger.Fork("users"),
81 Users: NewUsers(),
82 }
83 }
84
85 // LoadUsers is responsible for loading users from a file
86 func (u *UserIndex) LoadUsers(configFile string) error {
87 u.configFile = configFile
88 u.Infof("Loading configuration file %s", configFile)
89 if err := u.loadUserIndex(); err != nil {
90 return err
91 }
92 if err := u.addWatchEvents(); err != nil {
93 return err
94 }
95 return nil
96 }
97
98 // watchEvents is responsible for watching for updates to the file and reloading
99 func (u *UserIndex) addWatchEvents() error {
100 watcher, err := fsnotify.NewWatcher()
101 if err != nil {
102 return err
103 }
104 if err := watcher.Add(u.configFile); err != nil {
105 return err
106 }
107 go func() {
108 for e := range watcher.Events {
109 if e.Op&fsnotify.Write != fsnotify.Write {
110 continue
111 }
112 if err := u.loadUserIndex(); err != nil {
113 u.Infof("Failed to reload the users configuration: %s", err)
114 } else {
115 u.Debugf("Users configuration successfully reloaded from: %s", u.configFile)
116 }
117 }
118 }()
119 return nil
120 }
121
122 // loadUserIndex is responsible for loading the users configuration
123 func (u *UserIndex) loadUserIndex() error {
124 if u.configFile == "" {
125 return errors.New("configuration file not set")
126 }
127 b, err := ioutil.ReadFile(u.configFile)
128 if err != nil {
129 return fmt.Errorf("Failed to read auth file: %s, error: %s", u.configFile, err)
130 }
131 var raw map[string][]string
132 if err := json.Unmarshal(b, &raw); err != nil {
133 return errors.New("Invalid JSON: " + err.Error())
134 }
135 users := []*User{}
136 for auth, remotes := range raw {
137 user := &User{}
138 user.Name, user.Pass = ParseAuth(auth)
139 if user.Name == "" {
140 return errors.New("Invalid user:pass string")
141 }
142 for _, r := range remotes {
143 if r == "" || r == "*" {
144 user.Addrs = append(user.Addrs, UserAllowAll)
145 } else {
146 re, err := regexp.Compile(r)
147 if err != nil {
148 return errors.New("Invalid address regex")
149 }
150 user.Addrs = append(user.Addrs, re)
151 }
152 }
153 users = append(users, user)
154 }
155 //swap
156 u.Reset(users)
157 return nil
158 }
0 package tunnel
1
2 import (
3 "bytes"
4 "context"
5 "errors"
6 "io/ioutil"
7 "log"
8 "os"
9 "sync"
10 "time"
11
12 "github.com/armon/go-socks5"
13 "github.com/jpillora/chisel/share/cio"
14 "github.com/jpillora/chisel/share/cnet"
15 "github.com/jpillora/chisel/share/settings"
16 "golang.org/x/crypto/ssh"
17 "golang.org/x/sync/errgroup"
18 )
19
20 //Config a Tunnel
21 type Config struct {
22 *cio.Logger
23 Inbound bool
24 Outbound bool
25 Socks bool
26 KeepAlive time.Duration
27 }
28
29 //Tunnel represents an SSH tunnel with proxy capabilities.
30 //Both chisel client and server are Tunnels.
31 //chisel client has a single set of remotes, whereas
32 //chisel server has multiple sets of remotes (one set per client).
33 //Each remote has a 1:1 mapping to a proxy.
34 //Proxies listen, send data over ssh, and the other end of the ssh connection
35 //communicates with the endpoint and returns the response.
36 type Tunnel struct {
37 Config
38 //ssh connection
39 activeConnMut sync.RWMutex
40 activatingConn waitGroup
41 activeConn ssh.Conn
42 //proxies
43 proxyCount int
44 //internals
45 connStats cnet.ConnCount
46 socksServer *socks5.Server
47 }
48
49 //New Tunnel from the given Config
50 func New(c Config) *Tunnel {
51 c.Logger = c.Logger.Fork("tun")
52 t := &Tunnel{
53 Config: c,
54 }
55 t.activatingConn.Add(1)
56 //setup socks server (not listening on any port!)
57 extra := ""
58 if c.Socks {
59 sl := log.New(ioutil.Discard, "", 0)
60 if t.Logger.Debug {
61 sl = log.New(os.Stdout, "[socks]", log.Ldate|log.Ltime)
62 }
63 t.socksServer, _ = socks5.New(&socks5.Config{Logger: sl})
64 extra += " (SOCKS enabled)"
65 }
66 t.Debugf("Created%s", extra)
67 return t
68 }
69
70 //BindSSH provides an active SSH for use for tunnelling
71 func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error {
72 //link ctx to ssh-conn
73 go func() {
74 <-ctx.Done()
75 if c.Close() == nil {
76 t.Debugf("SSH cancelled")
77 }
78 t.activatingConn.DoneAll()
79 }()
80 //mark active and unblock
81 t.activeConnMut.Lock()
82 if t.activeConn != nil {
83 panic("double bind ssh")
84 }
85 t.activeConn = c
86 t.activeConnMut.Unlock()
87 t.activatingConn.Done()
88 //optional keepalive loop against this connection
89 if t.Config.KeepAlive > 0 {
90 go t.keepAliveLoop(c)
91 }
92 //block until closed
93 go t.handleSSHRequests(reqs)
94 go t.handleSSHChannels(chans)
95 t.Debugf("SSH connected")
96 err := c.Wait()
97 t.Debugf("SSH disconnected")
98 //mark inactive and block
99 t.activatingConn.Add(1)
100 t.activeConnMut.Lock()
101 t.activeConn = nil
102 t.activeConnMut.Unlock()
103 return err
104 }
105
106 //getSSH blocks while connecting
107 func (t *Tunnel) getSSH(ctx context.Context) ssh.Conn {
108 //cancelled already?
109 if isDone(ctx) {
110 return nil
111 }
112 t.activeConnMut.RLock()
113 c := t.activeConn
114 t.activeConnMut.RUnlock()
115 //connected already?
116 if c != nil {
117 return c
118 }
119 //connecting...
120 select {
121 case <-ctx.Done(): //cancelled
122 return nil
123 case <-time.After(settings.EnvDuration("SSH_WAIT", 35*time.Second)):
124 return nil //a bit longer than ssh timeout
125 case <-t.activatingConnWait():
126 t.activeConnMut.RLock()
127 c := t.activeConn
128 t.activeConnMut.RUnlock()
129 return c
130 }
131 }
132
133 func (t *Tunnel) activatingConnWait() <-chan struct{} {
134 ch := make(chan struct{})
135 go func() {
136 t.activatingConn.Wait()
137 close(ch)
138 }()
139 return ch
140 }
141
142 //BindRemotes converts the given remotes into proxies, and blocks
143 //until the caller cancels the context or there is a proxy error.
144 func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error {
145 if len(remotes) == 0 {
146 return errors.New("no remotes")
147 }
148 if !t.Inbound {
149 return errors.New("inbound connections blocked")
150 }
151 proxies := make([]*Proxy, len(remotes))
152 for i, remote := range remotes {
153 p, err := NewProxy(t.Logger, t, t.proxyCount, remote)
154 if err != nil {
155 return err
156 }
157 proxies[i] = p
158 t.proxyCount++
159 }
160 //TODO: handle tunnel close
161 eg, ctx := errgroup.WithContext(ctx)
162 for _, proxy := range proxies {
163 p := proxy
164 eg.Go(func() error {
165 return p.Run(ctx)
166 })
167 }
168 t.Debugf("Bound proxies")
169 err := eg.Wait()
170 t.Debugf("Unbound proxies")
171 return err
172 }
173
174 func (t *Tunnel) keepAliveLoop(sshConn ssh.Conn) {
175 //ping forever
176 for {
177 time.Sleep(t.Config.KeepAlive)
178 _, b, err := sshConn.SendRequest("ping", true, nil)
179 if err != nil {
180 break
181 }
182 if len(b) > 0 && !bytes.Equal(b, []byte("pong")) {
183 t.Debugf("strange ping response")
184 break
185 }
186 }
187 //close ssh connection on abnormal ping
188 sshConn.Close()
189 }
0 package tunnel
1
2 import (
3 "context"
4 "io"
5 "net"
6
7 "github.com/jpillora/chisel/share/cio"
8 "github.com/jpillora/chisel/share/settings"
9 "github.com/jpillora/sizestr"
10 "golang.org/x/crypto/ssh"
11 )
12
13 //sshTunnel exposes a subset of Tunnel to subtypes
14 type sshTunnel interface {
15 getSSH(ctx context.Context) ssh.Conn
16 }
17
18 //Proxy is the inbound portion of a Tunnel
19 type Proxy struct {
20 *cio.Logger
21 sshTun sshTunnel
22 id int
23 count int
24 remote *settings.Remote
25 dialer net.Dialer
26 tcp *net.TCPListener
27 udp *udpListener
28 }
29
30 //NewProxy creates a Proxy
31 func NewProxy(logger *cio.Logger, sshTun sshTunnel, index int, remote *settings.Remote) (*Proxy, error) {
32 id := index + 1
33 p := &Proxy{
34 Logger: logger.Fork("proxy#%s", remote.String()),
35 sshTun: sshTun,
36 id: id,
37 remote: remote,
38 }
39 return p, p.listen()
40 }
41
42 func (p *Proxy) listen() error {
43 if p.remote.Stdio {
44 //TODO check if pipes active?
45 } else if p.remote.LocalProto == "tcp" {
46 addr, err := net.ResolveTCPAddr("tcp", p.remote.LocalHost+":"+p.remote.LocalPort)
47 if err != nil {
48 return p.Errorf("resolve: %s", err)
49 }
50 l, err := net.ListenTCP("tcp", addr)
51 if err != nil {
52 return p.Errorf("tcp: %s", err)
53 }
54 p.Infof("Listening")
55 p.tcp = l
56 } else if p.remote.LocalProto == "udp" {
57 l, err := listenUDP(p.Logger, p.sshTun, p.remote)
58 if err != nil {
59 return err
60 }
61 p.Infof("Listening")
62 p.udp = l
63 } else {
64 return p.Errorf("unknown local proto")
65 }
66 return nil
67 }
68
69 //Run enables the proxy and blocks while its active,
70 //close the proxy by cancelling the context.
71 func (p *Proxy) Run(ctx context.Context) error {
72 if p.remote.Stdio {
73 return p.runStdio(ctx)
74 } else if p.remote.LocalProto == "tcp" {
75 return p.runTCP(ctx)
76 } else if p.remote.LocalProto == "udp" {
77 return p.udp.run(ctx)
78 }
79 panic("should not get here")
80 }
81
82 func (p *Proxy) runStdio(ctx context.Context) error {
83 defer p.Infof("Closed")
84 for {
85 p.pipeRemote(ctx, cio.Stdio)
86 select {
87 case <-ctx.Done():
88 return nil
89 default:
90 // the connection is not ready yet, keep waiting
91 }
92 }
93 }
94
95 func (p *Proxy) runTCP(ctx context.Context) error {
96 done := make(chan struct{})
97 //implements missing net.ListenContext
98 go func() {
99 select {
100 case <-ctx.Done():
101 p.tcp.Close()
102 case <-done:
103 }
104 }()
105 for {
106 src, err := p.tcp.Accept()
107 if err != nil {
108 select {
109 case <-ctx.Done():
110 //listener closed
111 err = nil
112 default:
113 p.Infof("Accept error: %s", err)
114 }
115 close(done)
116 return err
117 }
118 go p.pipeRemote(ctx, src)
119 }
120 }
121
122 func (p *Proxy) pipeRemote(ctx context.Context, src io.ReadWriteCloser) {
123 defer src.Close()
124 p.count++
125 cid := p.count
126 l := p.Fork("conn#%d", cid)
127 l.Debugf("Open")
128 sshConn := p.sshTun.getSSH(ctx)
129 if sshConn == nil {
130 l.Debugf("No remote connection")
131 return
132 }
133 //ssh request for tcp connection for this proxy's remote
134 dst, reqs, err := sshConn.OpenChannel("chisel", []byte(p.remote.Remote()))
135 if err != nil {
136 l.Infof("Stream error: %s", err)
137 return
138 }
139 go ssh.DiscardRequests(reqs)
140 //then pipe
141 s, r := cio.Pipe(src, dst)
142 l.Debugf("Close (sent %s received %s)", sizestr.ToString(s), sizestr.ToString(r))
143 }
0 package tunnel
1
2 import (
3 "context"
4 "encoding/gob"
5 "fmt"
6 "io"
7 "net"
8 "strings"
9 "sync"
10 "sync/atomic"
11 "time"
12
13 "github.com/jpillora/chisel/share/cio"
14 "github.com/jpillora/chisel/share/settings"
15 "github.com/jpillora/sizestr"
16 "golang.org/x/crypto/ssh"
17 "golang.org/x/sync/errgroup"
18 )
19
20 //listenUDP is a special listener which forwards packets via
21 //the bound ssh connection. tricky part is multiplexing lots of
22 //udp clients through the entry node. each will listen on its
23 //own source-port for a response:
24 // (random)
25 // src-1 1111->... dst-1 6345->7777
26 // src-2 2222->... <---> udp <---> udp <-> dst-1 7543->7777
27 // src-3 3333->... listener handler dst-1 1444->7777
28 //
29 //we must store these mappings (1111-6345, etc) in memory for a length
30 //of time, so that when the exit node receives a response on 6345, it
31 //knows to return it to 1111.
32 func listenUDP(l *cio.Logger, sshTun sshTunnel, remote *settings.Remote) (*udpListener, error) {
33 a, err := net.ResolveUDPAddr("udp", remote.Local())
34 if err != nil {
35 return nil, l.Errorf("resolve: %s", err)
36 }
37 conn, err := net.ListenUDP("udp", a)
38 if err != nil {
39 return nil, l.Errorf("listen: %s", err)
40 }
41 //ready
42 u := &udpListener{
43 Logger: l,
44 sshTun: sshTun,
45 remote: remote,
46 inbound: conn,
47 }
48 return u, nil
49 }
50
51 type udpListener struct {
52 *cio.Logger
53 sshTun sshTunnel
54 remote *settings.Remote
55 inbound *net.UDPConn
56 outboundMut sync.Mutex
57 outbound *udpChannel
58 sent, recv int64
59 }
60
61 func (u *udpListener) run(ctx context.Context) error {
62 defer u.inbound.Close()
63 //udp doesnt accept connections,
64 //udp simply forwards packets
65 //and therefore only needs to listen
66 eg, ctx := errgroup.WithContext(ctx)
67 eg.Go(func() error {
68 return u.runInbound(ctx)
69 })
70 eg.Go(func() error {
71 return u.runOutbound(ctx)
72 })
73 if err := eg.Wait(); err != nil {
74 u.Debugf("listen: %s", err)
75 return err
76 }
77 u.Debugf("Close (sent %s received %s)", sizestr.ToString(u.sent), sizestr.ToString(u.recv))
78 return nil
79 }
80
81 func (u *udpListener) runInbound(ctx context.Context) error {
82 const maxMTU = 9012
83 buff := make([]byte, maxMTU)
84 for !isDone(ctx) {
85 //read from inbound udp
86 u.inbound.SetReadDeadline(time.Now().Add(time.Second))
87 n, addr, err := u.inbound.ReadFromUDP(buff)
88 if e, ok := err.(net.Error); ok && (e.Timeout() || e.Temporary()) {
89 continue
90 }
91 if err != nil {
92 return u.Errorf("read error: %w", err)
93 }
94 //upsert ssh channel
95 uc, err := u.getUDPChan(ctx)
96 if err != nil {
97 if strings.HasSuffix(err.Error(), "EOF") {
98 continue
99 }
100 return u.Errorf("inbound-udpchan: %w", err)
101 }
102 //send over channel, including source address
103 b := buff[:n]
104 if err := uc.encode(addr.String(), b); err != nil {
105 if strings.HasSuffix(err.Error(), "EOF") {
106 continue //dropped packet...
107 }
108 return u.Errorf("encode error: %w", err)
109 }
110 //stats
111 atomic.AddInt64(&u.sent, int64(n))
112 }
113 return nil
114 }
115
116 func (u *udpListener) runOutbound(ctx context.Context) error {
117 for !isDone(ctx) {
118 //upsert ssh channel
119 uc, err := u.getUDPChan(ctx)
120 if err != nil {
121 if strings.HasSuffix(err.Error(), "EOF") {
122 continue
123 }
124 return u.Errorf("outbound-udpchan: %w", err)
125 }
126 //receive from channel, including source address
127 p := udpPacket{}
128 if err := uc.decode(&p); err == io.EOF {
129 //outbound ssh disconnected, get new connection...
130 continue
131 } else if err != nil {
132 return u.Errorf("decode error: %w", err)
133 }
134 //write back to inbound udp
135 addr, err := net.ResolveUDPAddr("udp", p.Src)
136 if err != nil {
137 return u.Errorf("resolve error: %w", err)
138 }
139 n, err := u.inbound.WriteToUDP(p.Payload, addr)
140 if err != nil {
141 return u.Errorf("write error: %w", err)
142 }
143 //stats
144 atomic.AddInt64(&u.recv, int64(n))
145 }
146 return nil
147 }
148
149 func (u *udpListener) getUDPChan(ctx context.Context) (*udpChannel, error) {
150 u.outboundMut.Lock()
151 defer u.outboundMut.Unlock()
152 //cached
153 if u.outbound != nil {
154 return u.outbound, nil
155 }
156 //not cached, bind
157 sshConn := u.sshTun.getSSH(ctx)
158 if sshConn == nil {
159 return nil, fmt.Errorf("ssh-conn nil")
160 }
161 //ssh request for udp packets for this proxy's remote,
162 //just "udp" since the remote address is sent with each packet
163 dstAddr := u.remote.Remote() + "/udp"
164 rwc, reqs, err := sshConn.OpenChannel("chisel", []byte(dstAddr))
165 if err != nil {
166 return nil, fmt.Errorf("ssh-chan error: %s", err)
167 }
168 go ssh.DiscardRequests(reqs)
169 //remove on disconnect
170 go u.unsetUDPChan(sshConn)
171 //ready
172 o := &udpChannel{
173 r: gob.NewDecoder(rwc),
174 w: gob.NewEncoder(rwc),
175 c: rwc,
176 }
177 u.outbound = o
178 u.Debugf("aquired channel")
179 return o, nil
180 }
181
182 func (u *udpListener) unsetUDPChan(sshConn ssh.Conn) {
183 sshConn.Wait()
184 u.Debugf("lost channel")
185 u.outboundMut.Lock()
186 u.outbound = nil
187 u.outboundMut.Unlock()
188 }
0 package tunnel
1
2 import (
3 "fmt"
4 "io"
5 "net"
6 "strings"
7
8 "github.com/jpillora/chisel/share/cio"
9 "github.com/jpillora/chisel/share/cnet"
10 "github.com/jpillora/chisel/share/settings"
11 "github.com/jpillora/sizestr"
12 "golang.org/x/crypto/ssh"
13 )
14
15 func (t *Tunnel) handleSSHRequests(reqs <-chan *ssh.Request) {
16 for r := range reqs {
17 switch r.Type {
18 case "ping":
19 r.Reply(true, []byte("pong"))
20 default:
21 t.Debugf("Unknown request: %s", r.Type)
22 }
23 }
24 }
25
26 func (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) {
27 for ch := range chans {
28 go t.handleSSHChannel(ch)
29 }
30 }
31
32 func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) {
33 if !t.Config.Outbound {
34 t.Debugf("Denied outbound connection")
35 ch.Reject(ssh.Prohibited, "Denied outbound connection")
36 return
37 }
38 remote := string(ch.ExtraData())
39 //extract protocol
40 hostPort, proto := settings.L4Proto(remote)
41 udp := proto == "udp"
42 socks := hostPort == "socks"
43 if socks && t.socksServer == nil {
44 t.Debugf("Denied socks request, please enable socks")
45 ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled")
46 return
47 }
48 sshChan, reqs, err := ch.Accept()
49 if err != nil {
50 t.Debugf("Failed to accept stream: %s", err)
51 return
52 }
53 stream := io.ReadWriteCloser(sshChan)
54 //cnet.MeterRWC(t.Logger.Fork("sshchan"), sshChan)
55 defer stream.Close()
56 go ssh.DiscardRequests(reqs)
57 l := t.Logger.Fork("conn#%d", t.connStats.New())
58 //ready to handle
59 t.connStats.Open()
60 l.Debugf("Open %s", t.connStats.String())
61 if socks {
62 err = t.handleSocks(stream)
63 } else if udp {
64 err = t.handleUDP(l, stream, hostPort)
65 } else {
66 err = t.handleTCP(l, stream, hostPort)
67 }
68 t.connStats.Close()
69 errmsg := ""
70 if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
71 errmsg = fmt.Sprintf(" (error %s)", err)
72 }
73 l.Debugf("Close %s%s", t.connStats.String(), errmsg)
74 }
75
76 func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error {
77 return t.socksServer.ServeConn(cnet.NewRWCConn(src))
78 }
79
80 func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error {
81 dst, err := net.Dial("tcp", hostPort)
82 if err != nil {
83 return err
84 }
85 s, r := cio.Pipe(src, dst)
86 l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r))
87 return nil
88 }
0 package tunnel
1
2 import (
3 "encoding/gob"
4 "io"
5 "net"
6 "os"
7 "sync"
8 "time"
9
10 "github.com/jpillora/chisel/share/cio"
11 "github.com/jpillora/chisel/share/settings"
12 )
13
14 func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error {
15 conns := &udpConns{
16 Logger: l,
17 m: map[string]*udpConn{},
18 }
19 defer conns.closeAll()
20 h := &udpHandler{
21 Logger: l,
22 hostPort: hostPort,
23 udpChannel: &udpChannel{
24 r: gob.NewDecoder(rwc),
25 w: gob.NewEncoder(rwc),
26 c: rwc,
27 },
28 udpConns: conns,
29 }
30 for {
31 p := udpPacket{}
32 if err := h.handleWrite(&p); err != nil {
33 return err
34 }
35 }
36 }
37
38 type udpHandler struct {
39 *cio.Logger
40 hostPort string
41 *udpChannel
42 *udpConns
43 }
44
45 func (h *udpHandler) handleWrite(p *udpPacket) error {
46 if err := h.r.Decode(&p); err != nil {
47 return err
48 }
49 //dial now, we know we must write
50 conn, exists, err := h.udpConns.dial(p.Src, h.hostPort)
51 if err != nil {
52 return err
53 }
54 //however, we dont know if we must read...
55 //spawn up to <max-conns> go-routines to wait
56 //for a reply.
57 //TODO configurable
58 //TODO++ dont use go-routines, switch to pollable
59 // array of listeners where all listeners are
60 // sweeped periodically, removing the idle ones
61 const maxConns = 100
62 if !exists {
63 if h.udpConns.len() <= maxConns {
64 go h.handleRead(p, conn)
65 } else {
66 h.Debugf("exceeded max udp connections (%d)", maxConns)
67 }
68 }
69 _, err = conn.Write(p.Payload)
70 if err != nil {
71 return err
72 }
73 return nil
74 }
75
76 func (h *udpHandler) handleRead(p *udpPacket, conn *udpConn) {
77 //ensure connection is cleaned up
78 defer h.udpConns.remove(conn.id)
79 const maxMTU = 9012
80 buff := make([]byte, maxMTU)
81 for {
82 //response must arrive within 15 seconds
83 deadline := settings.EnvDuration("UDP_DEADLINE", 15*time.Second)
84 conn.SetReadDeadline(time.Now().Add(deadline))
85 //read response
86 n, err := conn.Read(buff)
87 if err != nil {
88 if !os.IsTimeout(err) && err != io.EOF {
89 h.Debugf("read error: %s", err)
90 }
91 break
92 }
93 b := buff[:n]
94 //encode back over ssh connection
95 err = h.udpChannel.encode(p.Src, b)
96 if err != nil {
97 h.Debugf("encode error: %s", err)
98 return
99 }
100 }
101 }
102
103 type udpConns struct {
104 *cio.Logger
105 sync.Mutex
106 m map[string]*udpConn
107 }
108
109 func (cs *udpConns) dial(id, addr string) (*udpConn, bool, error) {
110 cs.Lock()
111 defer cs.Unlock()
112 conn, ok := cs.m[id]
113 if !ok {
114 c, err := net.Dial("udp", addr)
115 if err != nil {
116 return nil, false, err
117 }
118 conn = &udpConn{
119 id: id,
120 Conn: c, // cnet.MeterConn(cs.Logger.Fork(addr), c),
121 }
122 cs.m[id] = conn
123 }
124 return conn, ok, nil
125 }
126
127 func (cs *udpConns) len() int {
128 cs.Lock()
129 l := len(cs.m)
130 cs.Unlock()
131 return l
132 }
133
134 func (cs *udpConns) remove(id string) {
135 cs.Lock()
136 delete(cs.m, id)
137 cs.Unlock()
138 }
139
140 func (cs *udpConns) closeAll() {
141 cs.Lock()
142 for id, conn := range cs.m {
143 conn.Close()
144 delete(cs.m, id)
145 }
146 cs.Unlock()
147 }
148
149 type udpConn struct {
150 id string
151 net.Conn
152 }
0 package tunnel
1
2 import (
3 "context"
4 "encoding/gob"
5 "io"
6 )
7
8 type udpPacket struct {
9 Src string
10 Payload []byte
11 }
12
13 func init() {
14 gob.Register(&udpPacket{})
15 }
16
17 //udpChannel encodes/decodes udp payloads over a stream
18 type udpChannel struct {
19 r *gob.Decoder
20 w *gob.Encoder
21 c io.Closer
22 }
23
24 func (o *udpChannel) encode(src string, b []byte) error {
25 return o.w.Encode(udpPacket{
26 Src: src,
27 Payload: b,
28 })
29 }
30
31 func (o *udpChannel) decode(p *udpPacket) error {
32 return o.r.Decode(p)
33 }
34
35 func isDone(ctx context.Context) bool {
36 select {
37 case <-ctx.Done():
38 return true
39 default:
40 return false
41 }
42 }
0 package tunnel
1
2 import (
3 "sync"
4 "sync/atomic"
5 )
6
7 type waitGroup struct {
8 inner sync.WaitGroup
9 n int32
10 }
11
12 func (w *waitGroup) Add(n int) {
13 atomic.AddInt32(&w.n, int32(n))
14 w.inner.Add(n)
15 }
16
17 func (w *waitGroup) Done() {
18 if n := atomic.LoadInt32(&w.n); n > 0 && atomic.CompareAndSwapInt32(&w.n, n, n-1) {
19 w.inner.Done()
20 }
21 }
22
23 func (w *waitGroup) DoneAll() {
24 for atomic.LoadInt32(&w.n) > 0 {
25 w.Done()
26 }
27 }
28
29 func (w *waitGroup) Wait() {
30 w.inner.Wait()
31 }
0 package chshare
1
2 //ProtocolVersion of chisel. When backwards
3 //incompatible changes are made, this will
4 //be incremented to signify a protocol
5 //mismatch.
6 const ProtocolVersion = "chisel-v3"
7
8 var BuildVersion = "0.0.0-src"
0 //chisel end-to-end test
1 //======================
2 //
3 // (direct)
4 // .--------------->----------------.
5 // / chisel chisel \
6 // request--->client:2001--->server:2002---->fileserver:3000
7 // \ /
8 // '--> crowbar:4001--->crowbar:4002'
9 // client server
10 //
11 // crowbar and chisel binaries should be in your PATH
12
13 package main
14
15 import (
16 "flag"
17 "fmt"
18 "io"
19 "io/ioutil"
20 "log"
21 "net/http"
22 "os"
23 "os/exec"
24 "path"
25 "strconv"
26
27 "github.com/jpillora/chisel/share/cnet"
28
29 "time"
30 )
31
32 const ENABLE_CROWBAR = false
33
34 const (
35 B = 1
36 KB = 1000 * B
37 MB = 1000 * KB
38 GB = 1000 * MB
39 )
40
41 func run() {
42 flag.Parse()
43 args := flag.Args()
44 if len(args) == 0 {
45 fatal("go run main.go [test] or [bench]")
46 }
47 for _, a := range args {
48 switch a {
49 case "test":
50 test()
51 case "bench":
52 bench()
53 }
54 }
55 }
56
57 //test
58 func test() {
59 testTunnel("2001", 500)
60 testTunnel("2001", 50000)
61 }
62
63 //benchmark
64 func bench() {
65 benchSizes("3000")
66 benchSizes("2001")
67 if ENABLE_CROWBAR {
68 benchSizes("4001")
69 }
70 }
71
72 func benchSizes(port string) {
73 for size := 1; size <= 100*MB; size *= 10 {
74 testTunnel(port, size)
75 }
76 }
77
78 func testTunnel(port string, size int) {
79 t0 := time.Now()
80 resp, err := requestFile(port, size)
81 if err != nil {
82 fatal(err)
83 }
84 if resp.StatusCode != 200 {
85 fatal(err)
86 }
87
88 n, err := io.Copy(ioutil.Discard, resp.Body)
89 if err != nil {
90 fatal(err)
91 }
92 t1 := time.Now()
93 fmt.Printf(":%s => %d bytes in %s\n", port, size, t1.Sub(t0))
94 if int(n) != size {
95 fatalf("%d bytes expected, got %d", size, n)
96 }
97 }
98
99 //============================
100
101 func requestFile(port string, size int) (*http.Response, error) {
102 url := "http://127.0.0.1:" + port + "/" + strconv.Itoa(size)
103 // fmt.Println(url)
104 return http.Get(url)
105 }
106
107 func makeFileServer() *cnet.HTTPServer {
108 bsize := 3 * MB
109 bytes := make([]byte, bsize)
110 //filling huge buffer
111 for i := 0; i < len(bytes); i++ {
112 bytes[i] = byte(i)
113 }
114
115 s := cnet.NewHTTPServer()
116 s.Server.SetKeepAlivesEnabled(false)
117 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 rsize, _ := strconv.Atoi(r.URL.Path[1:])
119 for rsize >= bsize {
120 w.Write(bytes)
121 rsize -= bsize
122 }
123 w.Write(bytes[:rsize])
124 })
125 s.GoListenAndServe("0.0.0.0:3000", handler)
126 return s
127 }
128
129 //============================
130
131 func fatal(args ...interface{}) {
132 panic(fmt.Sprint(args...))
133 }
134 func fatalf(f string, args ...interface{}) {
135 panic(fmt.Sprintf(f, args...))
136 }
137
138 //global setup
139 func main() {
140
141 fs := makeFileServer()
142 go func() {
143 err := fs.Wait()
144 if err != nil {
145 fmt.Printf("fs server closed (%s)\n", err)
146 }
147 }()
148
149 if ENABLE_CROWBAR {
150 dir, _ := os.Getwd()
151 cd := exec.Command("crowbard",
152 `-listen`, "0.0.0.0:4002",
153 `-userfile`, path.Join(dir, "userfile"))
154 if err := cd.Start(); err != nil {
155 fatal(err)
156 }
157 go func() {
158 fatalf("crowbard: %v", cd.Wait())
159 }()
160 defer cd.Process.Kill()
161
162 time.Sleep(100 * time.Millisecond)
163
164 cf := exec.Command("crowbar-forward",
165 "-local=0.0.0.0:4001",
166 "-server=http://127.0.0.1:4002",
167 "-remote=127.0.0.1:3000",
168 "-username", "foo",
169 "-password", "bar")
170 if err := cf.Start(); err != nil {
171 fatal(err)
172 }
173 defer cf.Process.Kill()
174 }
175
176 time.Sleep(100 * time.Millisecond)
177
178 hd := exec.Command("chisel", "server",
179 // "-v",
180 "--key", "foobar",
181 "--port", "2002")
182 hd.Stdout = os.Stdout
183 if err := hd.Start(); err != nil {
184 fatal(err)
185 }
186 defer hd.Process.Kill()
187
188 time.Sleep(100 * time.Millisecond)
189
190 hf := exec.Command("chisel", "client",
191 // "-v",
192 "--fingerprint", "mOz4rg9zlQ409XAhhj6+fDDVwQMY42CL3Zg2W2oTYxA=",
193 "127.0.0.1:2002",
194 "2001:3000")
195 hf.Stdout = os.Stdout
196 if err := hf.Start(); err != nil {
197 fatal(err)
198 }
199 defer hf.Process.Kill()
200
201 time.Sleep(100 * time.Millisecond)
202
203 defer func() {
204 if r := recover(); r != nil {
205 log.Print(r)
206 }
207 }()
208 run()
209
210 fs.Close()
211 }
0
1 ### Performance
2
3 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.
4
5 In this simple benchmark, we have:
6
7 ```
8 (direct)
9 .--------------->----------------.
10 / chisel chisel \
11 request--->client:2001--->server:2002---->fileserver:3000
12 \ /
13 '--> crowbar:4001--->crowbar:4002'
14 client server
15 ```
16
17 Note, we're using an in-memory "file" server on localhost for these tests
18
19 _direct_
20
21 ```
22 :3000 => 1 bytes in 1.291417ms
23 :3000 => 10 bytes in 713.525µs
24 :3000 => 100 bytes in 562.48µs
25 :3000 => 1000 bytes in 595.445µs
26 :3000 => 10000 bytes in 1.053298ms
27 :3000 => 100000 bytes in 741.351µs
28 :3000 => 1000000 bytes in 1.367143ms
29 :3000 => 10000000 bytes in 8.601549ms
30 :3000 => 100000000 bytes in 76.3939ms
31 ```
32
33 `chisel`
34
35 ```
36 :2001 => 1 bytes in 1.351976ms
37 :2001 => 10 bytes in 1.106086ms
38 :2001 => 100 bytes in 1.005729ms
39 :2001 => 1000 bytes in 1.254396ms
40 :2001 => 10000 bytes in 1.139777ms
41 :2001 => 100000 bytes in 2.35437ms
42 :2001 => 1000000 bytes in 11.502673ms
43 :2001 => 10000000 bytes in 123.130246ms
44 :2001 => 100000000 bytes in 966.48636ms
45 ```
46
47 ~100MB in **~1 second**
48
49 `crowbar`
50
51 ```
52 :4001 => 1 bytes in 3.335797ms
53 :4001 => 10 bytes in 1.453007ms
54 :4001 => 100 bytes in 1.811727ms
55 :4001 => 1000 bytes in 1.621525ms
56 :4001 => 10000 bytes in 5.20729ms
57 :4001 => 100000 bytes in 38.461926ms
58 :4001 => 1000000 bytes in 358.784864ms
59 :4001 => 10000000 bytes in 3.603206487s
60 :4001 => 100000000 bytes in 36.332395213s
61 ```
62
63 ~100MB in **36 seconds**
64
65 See `test/bench/main.go`
0 foo:bar
0 package e2e_test
1
2 import (
3 "testing"
4
5 chclient "github.com/jpillora/chisel/client"
6 chserver "github.com/jpillora/chisel/server"
7 )
8
9 //TODO tests for:
10 // - failed auth
11 // - dynamic auth (server add/remove user)
12 // - watch auth file
13
14 func TestAuth(t *testing.T) {
15 tmpPort1 := availablePort()
16 tmpPort2 := availablePort()
17 //setup server, client, fileserver
18 teardown := simpleSetup(t,
19 &chserver.Config{
20 KeySeed: "foobar",
21 Auth: "../bench/userfile",
22 },
23 &chclient.Config{
24 Remotes: []string{
25 "0.0.0.0:" + tmpPort1 + ":127.0.0.1:$FILEPORT",
26 "0.0.0.0:" + tmpPort2 + ":localhost:$FILEPORT",
27 },
28 Auth: "foo:bar",
29 })
30 defer teardown()
31 //test first remote
32 result, err := post("http://localhost:"+tmpPort1, "foo")
33 if err != nil {
34 t.Fatal(err)
35 }
36 if result != "foo!" {
37 t.Fatalf("expected exclamation mark added")
38 }
39 //test second remote
40 result, err = post("http://localhost:"+tmpPort2, "bar")
41 if err != nil {
42 t.Fatal(err)
43 }
44 if result != "bar!" {
45 t.Fatalf("expected exclamation mark added again")
46 }
47 }
0 package e2e_test
1
2 import (
3 "testing"
4
5 chclient "github.com/jpillora/chisel/client"
6 chserver "github.com/jpillora/chisel/server"
7 )
8
9 func TestBase(t *testing.T) {
10 tmpPort := availablePort()
11 //setup server, client, fileserver
12 teardown := simpleSetup(t,
13 &chserver.Config{},
14 &chclient.Config{
15 Remotes: []string{tmpPort + ":$FILEPORT"},
16 })
17 defer teardown()
18 //test remote
19 result, err := post("http://localhost:"+tmpPort, "foo")
20 if err != nil {
21 t.Fatal(err)
22 }
23 if result != "foo!" {
24 t.Fatalf("expected exclamation mark added")
25 }
26 }
27
28 func TestReverse(t *testing.T) {
29 tmpPort := availablePort()
30 //setup server, client, fileserver
31 teardown := simpleSetup(t,
32 &chserver.Config{
33 Reverse: true,
34 },
35 &chclient.Config{
36 Remotes: []string{"R:" + tmpPort + ":$FILEPORT"},
37 })
38 defer teardown()
39 //test remote (this goes through the server and out the client)
40 result, err := post("http://localhost:"+tmpPort, "foo")
41 if err != nil {
42 t.Fatal(err)
43 }
44 if result != "foo!" {
45 t.Fatalf("expected exclamation mark added")
46 }
47 }
0 package e2e_test
1
2 //TODO tests for:
3 // client -> CONNECT proxy -> server -> endpoint
4 // client -> SOCKS proxy -> server -> endpoint
0 package e2e_test
1
2 import (
3 "context"
4 "io/ioutil"
5 "log"
6 "net"
7 "net/http"
8 "strings"
9 "testing"
10 "time"
11
12 chclient "github.com/jpillora/chisel/client"
13 chserver "github.com/jpillora/chisel/server"
14 )
15
16 const debug = true
17
18 //test layout configuration
19 type testLayout struct {
20 server *chserver.Config
21 client *chclient.Config
22 fileServer bool
23 udpEcho bool
24 udpServer bool
25 }
26
27 func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chclient.Client, teardown func()) {
28 //start of the world
29 // goroutines := runtime.NumGoroutine()
30 //root cancel
31 ctx, cancel := context.WithCancel(context.Background())
32 //fileserver (fake endpoint)
33 filePort := availablePort()
34 if tl.fileServer {
35 fileAddr := "127.0.0.1:" + filePort
36 f := http.Server{
37 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38 b, _ := ioutil.ReadAll(r.Body)
39 w.Write(append(b, '!'))
40 }),
41 }
42 fl, err := net.Listen("tcp", fileAddr)
43 if err != nil {
44 t.Fatal(err)
45 }
46 log.Printf("fileserver: listening on %s", fileAddr)
47 go func() {
48 f.Serve(fl)
49 cancel()
50 }()
51 go func() {
52 <-ctx.Done()
53 f.Close()
54 }()
55 }
56 //server
57 server, err := chserver.NewServer(tl.server)
58 if err != nil {
59 t.Fatal(err)
60 }
61 server.Debug = debug
62 port := availablePort()
63 if err := server.StartContext(ctx, "127.0.0.1", port); err != nil {
64 t.Fatal(err)
65 }
66 go func() {
67 server.Wait()
68 server.Infof("Closed")
69 cancel()
70 }()
71 //client (with defaults)
72 tl.client.Fingerprint = server.GetFingerprint()
73 if tl.server.TLS.Key != "" {
74 //the domain name has to be localhost to match the ssl cert
75 tl.client.Server = "https://localhost:" + port
76 } else {
77 tl.client.Server = "http://127.0.0.1:" + port
78 }
79 for i, r := range tl.client.Remotes {
80 //convert $FILEPORT into the allocated port for this test case
81 if tl.fileServer {
82 tl.client.Remotes[i] = strings.Replace(r, "$FILEPORT", filePort, 1)
83 }
84 }
85 client, err = chclient.NewClient(tl.client)
86 if err != nil {
87 t.Fatal(err)
88 }
89 client.Debug = debug
90 if err := client.Start(ctx); err != nil {
91 t.Fatal(err)
92 }
93 go func() {
94 client.Wait()
95 client.Infof("Closed")
96 cancel()
97 }()
98 //cancel context tree, and wait for both client and server to stop
99 teardown = func() {
100 cancel()
101 server.Wait()
102 client.Wait()
103 //confirm goroutines have been cleaned up
104 // time.Sleep(500 * time.Millisecond)
105 // TODO remove sleep
106 // d := runtime.NumGoroutine() - goroutines
107 // if d != 0 {
108 // pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
109 // t.Fatalf("goroutines left %d", d)
110 // }
111 }
112 //wait a bit...
113 //TODO: client signal API, similar to os.Notify(signal)
114 // wait for client setup
115 time.Sleep(50 * time.Millisecond)
116 //ready
117 return server, client, teardown
118 }
119
120 func simpleSetup(t *testing.T, s *chserver.Config, c *chclient.Config) context.CancelFunc {
121 conf := testLayout{
122 server: s,
123 client: c,
124 fileServer: true,
125 }
126 _, _, teardown := conf.setup(t)
127 return teardown
128 }
129
130 func post(url, body string) (string, error) {
131 resp, err := http.Post(url, "text/plain", strings.NewReader(body))
132 if err != nil {
133 return "", err
134 }
135 b, err := ioutil.ReadAll(resp.Body)
136 if err != nil {
137 return "", err
138 }
139 return string(b), nil
140 }
141
142 func availablePort() string {
143 l, err := net.Listen("tcp", "127.0.0.1:0")
144 if err != nil {
145 log.Panic(err)
146 }
147 l.Close()
148 _, port, err := net.SplitHostPort(l.Addr().String())
149 if err != nil {
150 log.Panic(err)
151 }
152 return port
153 }
0 package e2e_test
1
2 //TODO tests for:
3 // - SOCKS-client -> [client -> server SOCKS] -> endpoint
4 // - SOCKS-client -> [server -> client SOCKS] -> endpoint
0 -----BEGIN CERTIFICATE-----
1 MIICezCCAeQCCQDwdWskfbwmzzANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UEBhMC
2 dXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZCb3N0b24xDzANBgNVBAoMBkNoaXNl
3 bDENMAsGA1UECwwEdGVzdDESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYJKoZIhvcN
4 AQkBFhF3aWxseGlhQGdtYWlsLmNvbTAeFw0yMDA4MjQxOTQ4MTdaFw0zMDA4MjIx
5 OTQ4MTdaMIGBMQswCQYDVQQGEwJ1czELMAkGA1UECAwCbWExDzANBgNVBAcMBkJv
6 c3RvbjEPMA0GA1UECgwGQ2hpc2VsMQ0wCwYDVQQLDAR0ZXN0MRIwEAYDVQQDDAls
7 b2NhbGhvc3QxIDAeBgkqhkiG9w0BCQEWEXdpbGx4aWFAZ21haWwuY29tMIGfMA0G
8 CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC128s6L6YN0eNSNbI40URFHd9xzfnPlUcH
9 n9n7D6YkJL7LsTAtUfjubNAX0Q1gclDnDZCfYi9UZVzzID4s1gZJZAEZGnce8loO
10 a+WcPUgIOJngk2bwUHfrWPl+R5mvE9p60rfYNdo86wLMaLAJu+VagNmaoilSU7OS
11 uZ/AgTUMFQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAEzq2qsH5VfmjUcvlhS4a7X5
12 zOAtEIVB1+oef/1NcyT3PaMX0ry0Ddbo3NJs3G9KTF0k+TCGtT7nAG2jRQvs6omZ
13 3+9C3x+6TQq+95KMBWXuZLZEPNa4iCGFbGrHq4wcWDehBAPSjdctqnmowd8yIgov
14 gNSN2xEMPNKYIhHt0lyc
15 -----END CERTIFICATE-----
0 -----BEGIN CERTIFICATE-----
1 MIICfTCCAeYCCQDIXTlEp6na1zANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC
2 dXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZCb3N0b24xDzANBgNVBAoMBkNoaXNl
3 bDENMAsGA1UECwwEdGVzdDETMBEGA1UEAwwKbVRMU0NsaWVudDEgMB4GCSqGSIb3
4 DQEJARYRd2lsbHhpYUBnbWFpbC5jb20wHhcNMjAwODI0MTk0ODQxWhcNMzAwODIy
5 MTk0ODQxWjCBgjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZC
6 b3N0b24xDzANBgNVBAoMBkNoaXNlbDENMAsGA1UECwwEdGVzdDETMBEGA1UEAwwK
7 bVRMU0NsaWVudDEgMB4GCSqGSIb3DQEJARYRd2lsbHhpYUBnbWFpbC5jb20wgZ8w
8 DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4jt9TxHsCNegij34we4yzOykAuMVuz
9 DzW++Jh4/xWeOoU3xb7I2ETIzmusIM70o2lm+e+gy9VfAAXaNgZg63QV54jRn2nk
10 BWoXJYvYOJwt5YzOsLkbh6epSlrqYI0H34Sy5rEkacXCkcpcEvom/tvJ+SpHyIL1
11 PYNN1CCx/eg5AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAJysuLKCgVqMW628SFcpu
12 ojtBSNy2KETDwmMTaLg/XTaAPOvxAO3W9F7KJ1JxVFf2oIW7ROL9sP862lSMQLZ5
13 R45pBlPZycb1CQplD50wMqknaaMJ1qnld9Jkv802cJa2riqzdHb5rnjrewmuLOOB
14 V0cZ9PJA3KdXbJW1o+WjQz4=
15 -----END CERTIFICATE-----
0 -----BEGIN RSA PRIVATE KEY-----
1 MIICWwIBAAKBgQDOI7fU8R7AjXoIo9+MHuMszspALjFbsw81vviYeP8VnjqFN8W+
2 yNhEyM5rrCDO9KNpZvnvoMvVXwAF2jYGYOt0FeeI0Z9p5AVqFyWL2DicLeWMzrC5
3 G4enqUpa6mCNB9+EsuaxJGnFwpHKXBL6Jv7byfkqR8iC9T2DTdQgsf3oOQIDAQAB
4 AoGAArLhAz6s4mR3xokusgzteHa0myZ/qu2rM07uvkBHRqctqPTT9+11N2FRooM8
5 Yrk9MnIQr5xxTrfRrkHvFyJJstNX809ve9Klu1vbT+S19se/m+jLKTOtOoYoPRaK
6 w7ekvjhLct00zQevphEX30xqA2S3HSjWD3HmjVwdadAUgQECQQDz+LShNRkJ97+n
7 hiRgShHupW7CmAb67hrenbbzkCaY8Kf9cAFiscEmjH+lZsbufCgzVvHKDNKi9/JN
8 dPTSQvURAkEA2E2D0BqTDOiqjwyueSr2V5m63mzWR0Jd1TAl0dxB6SBumYQQ1FFP
9 DmQ/J3lcT2RTS+PmKAkuPpSOalw1kqggqQJAbotPVQgZG1IdjguS6epF68sbv6Jg
10 70v58sqlfgDf7EaG56fbiNuf+BaLM+e41ZB+Kp0Hm5Rp0JvmN0B6OddK8QJAcZbD
11 UdWiw3SrnNOcDCVzmC0y5Ptiy6kefYX7VmnEcxiE/DlOXTEVwwkB4UjqIQcedwwH
12 IZ8wmcyJvXEO8SU5gQJAfHxBcFdX2vrDNNjm5GG11zrT86Ii+ieXa0Ty5vapRSsz
13 FQH3KnM2t7nNDMlFOaHuvVXHmPasudtxBDc5xDoHNA==
14 -----END RSA PRIVATE KEY-----
0 -----BEGIN CERTIFICATE-----
1 MIICfTCCAeYCCQDIXTlEp6na1zANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMC
2 dXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZCb3N0b24xDzANBgNVBAoMBkNoaXNl
3 bDENMAsGA1UECwwEdGVzdDETMBEGA1UEAwwKbVRMU0NsaWVudDEgMB4GCSqGSIb3
4 DQEJARYRd2lsbHhpYUBnbWFpbC5jb20wHhcNMjAwODI0MTk0ODQxWhcNMzAwODIy
5 MTk0ODQxWjCBgjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZC
6 b3N0b24xDzANBgNVBAoMBkNoaXNlbDENMAsGA1UECwwEdGVzdDETMBEGA1UEAwwK
7 bVRMU0NsaWVudDEgMB4GCSqGSIb3DQEJARYRd2lsbHhpYUBnbWFpbC5jb20wgZ8w
8 DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4jt9TxHsCNegij34we4yzOykAuMVuz
9 DzW++Jh4/xWeOoU3xb7I2ETIzmusIM70o2lm+e+gy9VfAAXaNgZg63QV54jRn2nk
10 BWoXJYvYOJwt5YzOsLkbh6epSlrqYI0H34Sy5rEkacXCkcpcEvom/tvJ+SpHyIL1
11 PYNN1CCx/eg5AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAJysuLKCgVqMW628SFcpu
12 ojtBSNy2KETDwmMTaLg/XTaAPOvxAO3W9F7KJ1JxVFf2oIW7ROL9sP862lSMQLZ5
13 R45pBlPZycb1CQplD50wMqknaaMJ1qnld9Jkv802cJa2riqzdHb5rnjrewmuLOOB
14 V0cZ9PJA3KdXbJW1o+WjQz4=
15 -----END CERTIFICATE-----
0 -----BEGIN CERTIFICATE-----
1 MIICezCCAeQCCQDwdWskfbwmzzANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UEBhMC
2 dXMxCzAJBgNVBAgMAm1hMQ8wDQYDVQQHDAZCb3N0b24xDzANBgNVBAoMBkNoaXNl
3 bDENMAsGA1UECwwEdGVzdDESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYJKoZIhvcN
4 AQkBFhF3aWxseGlhQGdtYWlsLmNvbTAeFw0yMDA4MjQxOTQ4MTdaFw0zMDA4MjIx
5 OTQ4MTdaMIGBMQswCQYDVQQGEwJ1czELMAkGA1UECAwCbWExDzANBgNVBAcMBkJv
6 c3RvbjEPMA0GA1UECgwGQ2hpc2VsMQ0wCwYDVQQLDAR0ZXN0MRIwEAYDVQQDDAls
7 b2NhbGhvc3QxIDAeBgkqhkiG9w0BCQEWEXdpbGx4aWFAZ21haWwuY29tMIGfMA0G
8 CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC128s6L6YN0eNSNbI40URFHd9xzfnPlUcH
9 n9n7D6YkJL7LsTAtUfjubNAX0Q1gclDnDZCfYi9UZVzzID4s1gZJZAEZGnce8loO
10 a+WcPUgIOJngk2bwUHfrWPl+R5mvE9p60rfYNdo86wLMaLAJu+VagNmaoilSU7OS
11 uZ/AgTUMFQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAEzq2qsH5VfmjUcvlhS4a7X5
12 zOAtEIVB1+oef/1NcyT3PaMX0ry0Ddbo3NJs3G9KTF0k+TCGtT7nAG2jRQvs6omZ
13 3+9C3x+6TQq+95KMBWXuZLZEPNa4iCGFbGrHq4wcWDehBAPSjdctqnmowd8yIgov
14 gNSN2xEMPNKYIhHt0lyc
15 -----END CERTIFICATE-----
0 -----BEGIN RSA PRIVATE KEY-----
1 MIICXAIBAAKBgQC128s6L6YN0eNSNbI40URFHd9xzfnPlUcHn9n7D6YkJL7LsTAt
2 UfjubNAX0Q1gclDnDZCfYi9UZVzzID4s1gZJZAEZGnce8loOa+WcPUgIOJngk2bw
3 UHfrWPl+R5mvE9p60rfYNdo86wLMaLAJu+VagNmaoilSU7OSuZ/AgTUMFQIDAQAB
4 AoGBAJva4JLfXyqc5HsCNdlnz2CEt4irBBsZTiSEpKX7xWFYdIPROP6+L972NmkS
5 6qnrjtZV08oktXdY344l5eM7EWrFnqKH1pyTRUPnyKGY53jY4yZMad1GYMXLo8Mj
6 gkEOsfIhuieEBKGXAX54moDLTFzn14q+V+7g3OrLmMYXFN7JAkEA54S00eeuy0Eg
7 6+qx9dO4iDBp3qut5PShjda4M11MWobRQH71gO0g25qSrnw0x7BXJQl1hhgYtSUy
8 zbaF+5ZORwJBAMkWxt55YDpXdP4vudugmzP7F8aSB6rUysowlv1uStFhARNYwgs+
9 Tl9EGhFPF0ganNv4di1iYLarIKoWas8nNMMCQEex4ekKzSdmUNKeCGQvH3sVOwPY
10 uG4pj4oED2DgqI90JoLJji9Rv5YiBQCBuDqKkkIG7t0Kw0P9dAEeX9lsT2sCQD5x
11 iznEmSQkyliwe1d/LRLcMwrfh+/9eieFJS33lNYl+E6IrmENbQraO/oKBGHImdMY
12 +aGoPf4bb95BbdN8Cj8CQECXWVHkFFVQ2B78r7ENWnmbVG5XJW/iwfaZtmbsPPrI
13 KqGlB8leQcLLlCC48SCLlc64VtWOaJVxBDyvO4NuD/U=
14 -----END RSA PRIVATE KEY-----
0 package e2e_test
1
2 import (
3 "testing"
4
5 chclient "github.com/jpillora/chisel/client"
6 chserver "github.com/jpillora/chisel/server"
7 )
8
9 func TestTLS(t *testing.T) {
10 tmpPort := availablePort()
11 //setup server, client, fileserver
12 teardown := simpleSetup(t,
13 &chserver.Config{
14 TLS: chserver.TLSConfig{
15 Cert: "tls/server-crt/server.crt",
16 Key: "tls/server-crt/server.key",
17 CA: "tls/server-ca/client.crt",
18 },
19 },
20 &chclient.Config{
21 Remotes: []string{tmpPort + ":$FILEPORT"},
22 TLS: chclient.TLSConfig{
23 //for self signed cert, it needs the server cert, for real cert, this need to be the trusted CA cert
24 CA: "tls/client-ca/server.crt",
25 Cert: "tls/client-crt/client.crt",
26 Key: "tls/client-crt/client.key",
27 },
28 Server: "https://localhost:" + tmpPort,
29 })
30 defer teardown()
31 //test remote
32 result, err := post("http://localhost:"+tmpPort, "foo")
33 if err != nil {
34 t.Fatal(err)
35 }
36 if result != "foo!" {
37 t.Fatalf("expected exclamation mark added")
38 }
39 }
40
41 func TestMTLS(t *testing.T) {
42 tmpPort := availablePort()
43 //setup server, client, fileserver
44 teardown := simpleSetup(t,
45 &chserver.Config{
46 TLS: chserver.TLSConfig{
47 CA: "tls/server-ca",
48 Cert: "tls/server-crt/server.crt",
49 Key: "tls/server-crt/server.key",
50 },
51 },
52 &chclient.Config{
53 Remotes: []string{tmpPort + ":$FILEPORT"},
54 TLS: chclient.TLSConfig{
55 //for self signed cert, it needs the server cert, for real cert, this need to be the trusted CA cert
56 CA: "tls/client-ca/server.crt",
57 Cert: "tls/client-crt/client.crt",
58 Key: "tls/client-crt/client.key",
59 },
60 Server: "https://localhost:" + tmpPort,
61 })
62 defer teardown()
63 //test remote
64 result, err := post("http://localhost:"+tmpPort, "foo")
65 if err != nil {
66 t.Fatal(err)
67 }
68 if result != "foo!" {
69 t.Fatalf("expected exclamation mark added")
70 }
71 }
72
73 func TestTLSMissingClientCert(t *testing.T) {
74 tmpPort := availablePort()
75 //setup server, client, fileserver
76 teardown := simpleSetup(t,
77 &chserver.Config{
78 TLS: chserver.TLSConfig{
79 CA: "tls/server-ca/client.crt",
80 Cert: "tls/server-crt/server.crt",
81 Key: "tls/server-crt/server.key",
82 },
83 },
84 &chclient.Config{
85 Remotes: []string{tmpPort + ":$FILEPORT"},
86 TLS: chclient.TLSConfig{
87 CA: "tls/client-ca/server.crt",
88 //provide no client cert, server should reject the client request
89 //Cert: "tls/client-crt/client.crt",
90 //Key: "tls/client-crt/client.key",
91 },
92 Server: "https://localhost:" + tmpPort,
93 })
94 defer teardown()
95 //test remote
96 _, err := post("http://localhost:"+tmpPort, "foo")
97 if err == nil {
98 t.Fatal(err)
99 }
100 }
101
102 func TestTLSMissingClientCA(t *testing.T) {
103 tmpPort := availablePort()
104 //setup server, client, fileserver
105 teardown := simpleSetup(t,
106 &chserver.Config{
107 TLS: chserver.TLSConfig{
108 //specify a CA which does not match the client cert
109 //server should reject the client request
110 CA: "tls/server-crt/server.crt",
111 Cert: "tls/server-crt/server.crt",
112 Key: "tls/server-crt/server.key",
113 },
114 },
115 &chclient.Config{
116 Remotes: []string{tmpPort + ":$FILEPORT"},
117 TLS: chclient.TLSConfig{
118 //for self signed cert, it needs the server cert, for real cert, this need to be the trusted CA cert
119 CA: "tls/client-ca/server.crt",
120 Cert: "tls/client-crt/client.crt",
121 Key: "tls/client-crt/client.key",
122 },
123 Server: "https://localhost:" + tmpPort,
124 })
125 defer teardown()
126 //test remote
127 _, err := post("http://localhost:"+tmpPort, "foo")
128 if err == nil {
129 t.Fatal(err)
130 }
131 }
0 package e2e_test
1
2 import (
3 "log"
4 "net"
5 "testing"
6 "time"
7
8 chclient "github.com/jpillora/chisel/client"
9 chserver "github.com/jpillora/chisel/server"
10 "golang.org/x/sync/errgroup"
11 )
12
13 func TestUDP(t *testing.T) {
14 //listen on random udp port
15 echoPort := availableUDPPort()
16 a, _ := net.ResolveUDPAddr("udp", ":"+echoPort)
17 l, err := net.ListenUDP("udp", a)
18 if err != nil {
19 t.Fatal(err)
20 }
21 //chisel client+server
22 inboundPort := availableUDPPort()
23 teardown := simpleSetup(t,
24 &chserver.Config{},
25 &chclient.Config{
26 Remotes: []string{
27 inboundPort + ":" + echoPort + "/udp",
28 },
29 },
30 )
31 defer teardown()
32 //fake udp server, read and echo back duplicated, close
33 eg := errgroup.Group{}
34 eg.Go(func() error {
35 defer l.Close()
36 b := make([]byte, 128)
37 n, a, err := l.ReadFrom(b)
38 if err != nil {
39 return err
40 }
41 if _, err = l.WriteTo(append(b[:n], b[:n]...), a); err != nil {
42 return err
43 }
44 return nil
45 })
46 //fake udp client
47 conn, err := net.Dial("udp4", "localhost:"+inboundPort)
48 if err != nil {
49 t.Fatal(err)
50 }
51 //write bazz through the tunnel
52 if _, err := conn.Write([]byte("bazz")); err != nil {
53 t.Fatal(err)
54 }
55 //receive bazzbazz back
56 b := make([]byte, 128)
57 conn.SetReadDeadline(time.Now().Add(2 * time.Second))
58 n, err := conn.Read(b)
59 if err != nil {
60 t.Fatal(err)
61 return
62 }
63 //udp server should close correctly
64 if err := eg.Wait(); err != nil {
65 t.Fatal(err)
66 return
67 }
68 //ensure expected
69 s := string(b[:n])
70 if s != "bazzbazz" {
71 t.Fatalf("expected double bazz")
72 }
73 }
74
75 func availableUDPPort() string {
76 a, _ := net.ResolveUDPAddr("udp", ":0")
77 l, err := net.ListenUDP("udp", a)
78 if err != nil {
79 log.Panicf("availability listen: %s", err)
80 }
81 l.Close()
82 _, port, err := net.SplitHostPort(l.LocalAddr().String())
83 if err != nil {
84 log.Panic(err)
85 }
86 return port
87 }