New upstream version 1.7.4
Sophie Brun
3 years ago
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 | } |