├── .github ├── Dockerfile ├── dependabot.yml ├── goreleaser.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── client ├── client.go ├── client_connect.go └── client_test.go ├── example ├── Flyfile ├── fly.toml ├── reverse-tunneling-authenticated.md └── users.json ├── go.mod ├── go.sum ├── main.go ├── server ├── server.go ├── server_handler.go └── server_listen.go ├── share ├── ccrypto │ ├── determ_rand.go │ ├── generate_key_go119.go │ ├── keys.go │ └── keys_helpers.go ├── cio │ ├── logger.go │ ├── pipe.go │ └── stdio.go ├── cnet │ ├── conn_rwc.go │ ├── conn_ws.go │ ├── connstats.go │ ├── http_server.go │ └── meter.go ├── compat.go ├── cos │ ├── common.go │ ├── pprof.go │ ├── signal.go │ └── signal_windows.go ├── settings │ ├── config.go │ ├── env.go │ ├── remote.go │ ├── remote_test.go │ ├── user.go │ └── users.go ├── tunnel │ ├── tunnel.go │ ├── tunnel_in_proxy.go │ ├── tunnel_in_proxy_udp.go │ ├── tunnel_out_ssh.go │ ├── tunnel_out_ssh_udp.go │ ├── udp.go │ └── wg.go └── version.go └── test ├── bench ├── main.go ├── perf.md └── userfile └── e2e ├── auth_test.go ├── base_test.go ├── cert_utils_test.go ├── proxy_test.go ├── setup_test.go ├── socks_test.go ├── tls_test.go └── udp_test.go /.github/Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang:alpine AS build 3 | RUN apk update && apk add git 4 | ADD . /src 5 | WORKDIR /src 6 | ENV CGO_ENABLED=0 7 | RUN go build \ 8 | -ldflags "-X github.com/jpillora/chisel/share.BuildVersion=$(git describe --abbrev=0 --tags)" \ 9 | -o /tmp/bin 10 | # run stage 11 | FROM scratch 12 | LABEL maintainer="dev@jpillora.com" 13 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 14 | WORKDIR /app 15 | COPY --from=build /tmp/bin /app/bin 16 | ENTRYPOINT ["/app/bin"] -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | # Dependencies listed in go.mod 10 | - package-ecosystem: "gomod" 11 | directory: "/" # Location of package manifests 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/goreleaser.yml: -------------------------------------------------------------------------------- 1 | # test this file with 2 | # goreleaser release --config goreleaser.yml --clean --snapshot 3 | version: 2 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | ldflags: 8 | - -s -w -X github.com/jpillora/chisel/share.BuildVersion={{.Version}} 9 | flags: 10 | - -trimpath 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | - openbsd 16 | goarch: 17 | - 386 18 | - amd64 19 | - arm 20 | - arm64 21 | - ppc64 22 | - ppc64le 23 | - mips 24 | - mipsle 25 | - mips64 26 | - mips64le 27 | - s390x 28 | goarm: 29 | - 5 30 | - 6 31 | - 7 32 | gomips: 33 | - hardfloat 34 | - softfloat 35 | nfpms: 36 | - maintainer: "https://github.com/{{ .Env.GITHUB_USER }}" 37 | formats: 38 | - deb 39 | - rpm 40 | - apk 41 | archives: 42 | - format: gz 43 | files: 44 | - none* 45 | release: 46 | draft: true 47 | prerelease: auto 48 | changelog: 49 | sort: asc 50 | filters: 51 | exclude: 52 | - "^docs:" 53 | - "^test:" 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: {} 4 | push: {} 5 | permissions: write-all 6 | jobs: 7 | # ================ 8 | # BUILD AND TEST JOB 9 | # ================ 10 | test: 11 | name: Build & Test 12 | strategy: 13 | matrix: 14 | # optionally test/build across multiple platforms/Go-versions 15 | go-version: ["stable"] # '1.16', '1.17', '1.18, 16 | platform: [ubuntu-latest, macos-latest, windows-latest] 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Set up Go 24 | uses: actions/setup-go@v3 25 | with: 26 | go-version: ${{ matrix.go-version }} 27 | check-latest: true 28 | - name: Build 29 | run: go build -v -o /dev/null . 30 | - name: Test 31 | run: go test -v ./... 32 | # ================ 33 | # RELEASE BINARIES (on push "v*" tag) 34 | # ================ 35 | release_binaries: 36 | name: Release Binaries 37 | needs: test 38 | if: startsWith(github.ref, 'refs/tags/v') 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Check out code 42 | uses: actions/checkout@v3 43 | - name: goreleaser 44 | if: success() 45 | uses: docker://goreleaser/goreleaser:latest 46 | env: 47 | GITHUB_USER: ${{ github.repository_owner }} 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | args: release --config .github/goreleaser.yml 51 | # ================ 52 | # RELEASE DOCKER IMAGES (on push "v*" tag) 53 | # ================ 54 | release_docker: 55 | name: Release Docker Images 56 | needs: test 57 | if: startsWith(github.ref, 'refs/tags/v') 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Check out code 61 | uses: actions/checkout@v3 62 | - name: Set up QEMU 63 | uses: docker/setup-qemu-action@v2 64 | - name: Set up Docker Buildx 65 | uses: docker/setup-buildx-action@v2 66 | - name: Login to DockerHub 67 | uses: docker/login-action@v2 68 | with: 69 | username: jpillora 70 | password: ${{ secrets.DOCKERHUB_TOKEN }} 71 | - name: Docker meta 72 | id: meta 73 | uses: docker/metadata-action@v4 74 | with: 75 | images: jpillora/chisel 76 | tags: | 77 | type=semver,pattern={{version}} 78 | type=semver,pattern={{major}}.{{minor}} 79 | type=semver,pattern={{major}} 80 | - name: Build and push 81 | uses: docker/build-push-action@v3 82 | with: 83 | context: . 84 | file: .github/Dockerfile 85 | platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/386,linux/arm/v7,linux/arm/v6 86 | push: true 87 | tags: ${{ steps.meta.outputs.tags }} 88 | labels: ${{ steps.meta.outputs.labels }} 89 | cache-from: type=gha 90 | cache-to: type=gha,mode=max 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.swp 3 | .idea/ 4 | chisel 5 | bin/ 6 | release/ 7 | tmp/ 8 | *.orig 9 | debug 10 | 11 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 12 | *.o 13 | *.a 14 | *.so 15 | 16 | # Folders 17 | _obj 18 | _test 19 | 20 | # Architecture specific extensions/prefixes 21 | *.[568vq] 22 | [568vq].out 23 | 24 | *.cgo1.go 25 | *.cgo2.c 26 | _cgo_defun.c 27 | _cgo_gotypes.go 28 | _cgo_export.* 29 | 30 | _testmain.go 31 | 32 | *.exe 33 | *.test 34 | *.prof 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jaime Pillora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --abbrev=0 --tags) 2 | BUILD=$(shell git rev-parse HEAD) 3 | DIRBASE=./build 4 | DIR=${DIRBASE}/${VERSION}/${BUILD}/bin 5 | 6 | LDFLAGS=-ldflags "-s -w ${XBUILD} -buildid=${BUILD} -X github.com/jpillora/chisel/share.BuildVersion=${VERSION}" 7 | 8 | GOFILES=`go list ./...` 9 | GOFILESNOTEST=`go list ./... | grep -v test` 10 | 11 | # Make Directory to store executables 12 | $(shell mkdir -p ${DIR}) 13 | 14 | all: 15 | @goreleaser build --skip-validate --single-target --config .github/goreleaser.yml 16 | 17 | freebsd: lint 18 | env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-freebsd_amd64 . 19 | 20 | linux: lint 21 | env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-linux_amd64 . 22 | 23 | windows: lint 24 | env CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-windows_amd64 . 25 | 26 | darwin: 27 | env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-darwin_amd64 . 28 | 29 | docker: 30 | @docker build . 31 | 32 | dep: ## Get the dependencies 33 | @go get -u github.com/goreleaser/goreleaser 34 | @go get -u github.com/boumenot/gocover-cobertura 35 | @go get -v -d ./... 36 | @go get -u all 37 | @go mod tidy 38 | 39 | lint: ## Lint the files 40 | @go fmt ${GOFILES} 41 | @go vet ${GOFILESNOTEST} 42 | 43 | test: ## Run unit tests 44 | @go test -coverprofile=${DIR}/coverage.out -race -short ${GOFILESNOTEST} 45 | @go tool cover -html=${DIR}/coverage.out -o ${DIR}/coverage.html 46 | @gocover-cobertura < ${DIR}/coverage.out > ${DIR}/coverage.xml 47 | 48 | release: lint test 49 | goreleaser release --config .github/goreleaser.yml 50 | 51 | clean: 52 | rm -rf ${DIRBASE}/* 53 | 54 | .PHONY: all freebsd linux windows docker dep lint test release clean -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chisel 2 | 3 | [![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) 4 | 5 | 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. 6 | 7 | ![overview](https://docs.google.com/drawings/d/1p53VWxzGNfy8rjr-mW8pvisJmhkoLl82vAgctO_6f1w/pub?w=960&h=720) 8 | 9 | ## Table of Contents 10 | 11 | - [Features](#features) 12 | - [Install](#install) 13 | - [Demo](#demo) 14 | - [Usage](#usage) 15 | - [Contributing](#contributing) 16 | - [Changelog](#changelog) 17 | - [License](#license) 18 | 19 | ## Features 20 | 21 | - Easy to use 22 | - [Performant](./test/bench/perf.md)\* 23 | - [Encrypted connections](#security) using the SSH protocol (via `crypto/ssh`) 24 | - [Authenticated connections](#authentication); authenticated client connections with a users config file, authenticated server connections with fingerprint matching. 25 | - Client auto-reconnects with [exponential backoff](https://github.com/jpillora/backoff) 26 | - Clients can create multiple tunnel endpoints over one TCP connection 27 | - Clients can optionally pass through SOCKS or HTTP CONNECT proxies 28 | - Reverse port forwarding (Connections go through the server and out the client) 29 | - Server optionally doubles as a [reverse proxy](http://golang.org/pkg/net/http/httputil/#NewSingleHostReverseProxy) 30 | - Server optionally allows [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections (See [guide below](#socks5-guide)) 31 | - Clients optionally allow [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections from a reversed port forward 32 | - Client connections over stdio which supports `ssh -o ProxyCommand` providing SSH over HTTP 33 | 34 | ## Install 35 | 36 | ### Binaries 37 | 38 | [![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) 39 | 40 | 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` 41 | 42 | ### Docker 43 | 44 | [![Docker Pulls](https://img.shields.io/docker/pulls/jpillora/chisel.svg)](https://hub.docker.com/r/jpillora/chisel/) [![Image Size](https://img.shields.io/docker/image-size/jpillora/chisel/latest)](https://microbadger.com/images/jpillora/chisel) 45 | 46 | ```sh 47 | docker run --rm -it jpillora/chisel --help 48 | ``` 49 | 50 | ### Fedora 51 | 52 | The package is maintained by the Fedora community. If you encounter issues related to the usage of the RPM, please use this [issue tracker](https://bugzilla.redhat.com/buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&classification=Fedora&component=chisel&list_id=11614537&product=Fedora&product=Fedora%20EPEL). 53 | 54 | ```sh 55 | sudo dnf -y install chisel 56 | ``` 57 | 58 | ### Source 59 | 60 | ```sh 61 | $ go install github.com/jpillora/chisel@latest 62 | ``` 63 | 64 | ## Demo 65 | 66 | A [demo app](https://chisel-demo.herokuapp.com) on Heroku is running this `chisel server`: 67 | 68 | ```sh 69 | $ chisel server --port $PORT --proxy http://example.com 70 | # listens on $PORT, proxy web requests to http://example.com 71 | ``` 72 | 73 | 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: 74 | 75 | ```sh 76 | $ chisel client https://chisel-demo.herokuapp.com 3000 77 | # connects to chisel server at https://chisel-demo.herokuapp.com, 78 | # tunnels your localhost:3000 to the server's localhost:3000 79 | ``` 80 | 81 | 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). 82 | 83 | ## Usage 84 | 85 | 88 | 89 | 90 | ``` plain 91 | $ chisel --help 92 | 93 | Usage: chisel [command] [--help] 94 | 95 | Version: X.Y.Z 96 | 97 | Commands: 98 | server - runs chisel in server mode 99 | client - runs chisel in client mode 100 | 101 | Read more: 102 | https://github.com/jpillora/chisel 103 | 104 | ``` 105 | 106 | 107 | 108 | 109 | ``` plain 110 | $ chisel server --help 111 | 112 | Usage: chisel server [options] 113 | 114 | Options: 115 | 116 | --host, Defines the HTTP listening host – the network interface 117 | (defaults the environment variable HOST and falls back to 0.0.0.0). 118 | 119 | --port, -p, Defines the HTTP listening port (defaults to the environment 120 | variable PORT and fallsback to port 8080). 121 | 122 | --key, (deprecated use --keygen and --keyfile instead) 123 | An optional string to seed the generation of a ECDSA public 124 | and private key pair. All communications will be secured using this 125 | key pair. Share the subsequent fingerprint with clients to enable detection 126 | of man-in-the-middle attacks (defaults to the CHISEL_KEY environment 127 | variable, otherwise a new key is generate each run). 128 | 129 | --keygen, A path to write a newly generated PEM-encoded SSH private key file. 130 | If users depend on your --key fingerprint, you may also include your --key to 131 | output your existing key. Use - (dash) to output the generated key to stdout. 132 | 133 | --keyfile, An optional path to a PEM-encoded SSH private key. When 134 | this flag is set, the --key option is ignored, and the provided private key 135 | is used to secure all communications. (defaults to the CHISEL_KEY_FILE 136 | environment variable). Since ECDSA keys are short, you may also set keyfile 137 | to an inline base64 private key (e.g. chisel server --keygen - | base64). 138 | 139 | --authfile, An optional path to a users.json file. This file should 140 | be an object with users defined like: 141 | { 142 | "": ["",""] 143 | } 144 | when connects, their will be verified and then 145 | each of the remote addresses will be compared against the list 146 | of address regular expressions for a match. Addresses will 147 | always come in the form ":" for normal remotes 148 | and "R::" for reverse port forwarding 149 | remotes. This file will be automatically reloaded on change. 150 | 151 | --auth, An optional string representing a single user with full 152 | access, in the form of . It is equivalent to creating an 153 | authfile with {"": [""]}. If unset, it will use the 154 | environment variable AUTH. 155 | 156 | --keepalive, An optional keepalive interval. Since the underlying 157 | transport is HTTP, in many instances we'll be traversing through 158 | proxies, often these proxies will close idle connections. You must 159 | specify a time with a unit, for example '5s' or '2m'. Defaults 160 | to '25s' (set to 0s to disable). 161 | 162 | --backend, Specifies another HTTP server to proxy requests to when 163 | chisel receives a normal HTTP request. Useful for hiding chisel in 164 | plain sight. 165 | 166 | --socks5, Allow clients to access the internal SOCKS5 proxy. See 167 | chisel client --help for more information. 168 | 169 | --reverse, Allow clients to specify reverse port forwarding remotes 170 | in addition to normal remotes. 171 | 172 | --tls-key, Enables TLS and provides optional path to a PEM-encoded 173 | TLS private key. When this flag is set, you must also set --tls-cert, 174 | and you cannot set --tls-domain. 175 | 176 | --tls-cert, Enables TLS and provides optional path to a PEM-encoded 177 | TLS certificate. When this flag is set, you must also set --tls-key, 178 | and you cannot set --tls-domain. 179 | 180 | --tls-domain, Enables TLS and automatically acquires a TLS key and 181 | certificate using LetsEncrypt. Setting --tls-domain requires port 443. 182 | You may specify multiple --tls-domain flags to serve multiple domains. 183 | The resulting files are cached in the "$HOME/.cache/chisel" directory. 184 | You can modify this path by setting the CHISEL_LE_CACHE variable, 185 | or disable caching by setting this variable to "-". You can optionally 186 | provide a certificate notification email by setting CHISEL_LE_EMAIL. 187 | 188 | --tls-ca, a path to a PEM encoded CA certificate bundle or a directory 189 | holding multiple PEM encode CA certificate bundle files, which is used to 190 | validate client connections. The provided CA certificates will be used 191 | instead of the system roots. This is commonly used to implement mutual-TLS. 192 | 193 | --pid Generate pid file in current working directory 194 | 195 | -v, Enable verbose logging 196 | 197 | --help, This help text 198 | 199 | Signals: 200 | The chisel process is listening for: 201 | a SIGUSR2 to print process stats, and 202 | a SIGHUP to short-circuit the client reconnect timer 203 | 204 | Version: 205 | X.Y.Z 206 | 207 | Read more: 208 | https://github.com/jpillora/chisel 209 | 210 | ``` 211 | 212 | 213 | 214 | 215 | ``` plain 216 | $ chisel client --help 217 | 218 | Usage: chisel client [options] [remote] [remote] ... 219 | 220 | is the URL to the chisel server. 221 | 222 | s are remote connections tunneled through the server, each of 223 | which come in the form: 224 | 225 | :::/ 226 | 227 | ■ local-host defaults to 0.0.0.0 (all interfaces). 228 | ■ local-port defaults to remote-port. 229 | ■ remote-port is required*. 230 | ■ remote-host defaults to 0.0.0.0 (server localhost). 231 | ■ protocol defaults to tcp. 232 | 233 | which shares : from the server to the client 234 | as :, or: 235 | 236 | R::::/ 237 | 238 | which does reverse port forwarding, sharing : 239 | from the client to the server's :. 240 | 241 | example remotes 242 | 243 | 3000 244 | example.com:3000 245 | 3000:google.com:80 246 | 192.168.0.5:3000:google.com:80 247 | socks 248 | 5000:socks 249 | R:2222:localhost:22 250 | R:socks 251 | R:5000:socks 252 | stdio:example.com:22 253 | 1.1.1.1:53/udp 254 | 255 | When the chisel server has --socks5 enabled, remotes can 256 | specify "socks" in place of remote-host and remote-port. 257 | The default local host and port for a "socks" remote is 258 | 127.0.0.1:1080. Connections to this remote will terminate 259 | at the server's internal SOCKS5 proxy. 260 | 261 | When the chisel server has --reverse enabled, remotes can 262 | be prefixed with R to denote that they are reversed. That 263 | is, the server will listen and accept connections, and they 264 | will be proxied through the client which specified the remote. 265 | Reverse remotes specifying "R:socks" will listen on the server's 266 | default socks port (1080) and terminate the connection at the 267 | client's internal SOCKS5 proxy. 268 | 269 | When stdio is used as local-host, the tunnel will connect standard 270 | input/output of this program with the remote. This is useful when 271 | combined with ssh ProxyCommand. You can use 272 | ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \ 273 | user@example.com 274 | to connect to an SSH server through the tunnel. 275 | 276 | Options: 277 | 278 | --fingerprint, A *strongly recommended* fingerprint string 279 | to perform host-key validation against the server's public key. 280 | Fingerprint mismatches will close the connection. 281 | Fingerprints are generated by hashing the ECDSA public key using 282 | SHA256 and encoding the result in base64. 283 | Fingerprints must be 44 characters containing a trailing equals (=). 284 | 285 | --auth, An optional username and password (client authentication) 286 | in the form: ":". These credentials are compared to 287 | the credentials inside the server's --authfile. defaults to the 288 | AUTH environment variable. 289 | 290 | --keepalive, An optional keepalive interval. Since the underlying 291 | transport is HTTP, in many instances we'll be traversing through 292 | proxies, often these proxies will close idle connections. You must 293 | specify a time with a unit, for example '5s' or '2m'. Defaults 294 | to '25s' (set to 0s to disable). 295 | 296 | --max-retry-count, Maximum number of times to retry before exiting. 297 | Defaults to unlimited. 298 | 299 | --max-retry-interval, Maximum wait time before retrying after a 300 | disconnection. Defaults to 5 minutes. 301 | 302 | --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be 303 | used to reach the chisel server. Authentication can be specified 304 | inside the URL. 305 | For example, http://admin:password@my-server.com:8081 306 | or: socks://admin:password@my-server.com:1080 307 | 308 | --header, Set a custom header in the form "HeaderName: HeaderContent". 309 | Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World") 310 | 311 | --hostname, Optionally set the 'Host' header (defaults to the host 312 | found in the server url). 313 | 314 | --sni, Override the ServerName when using TLS (defaults to the 315 | hostname). 316 | 317 | --tls-ca, An optional root certificate bundle used to verify the 318 | chisel server. Only valid when connecting to the server with 319 | "https" or "wss". By default, the operating system CAs will be used. 320 | 321 | --tls-skip-verify, Skip server TLS certificate verification of 322 | chain and host name (if TLS is used for transport connections to 323 | server). If set, client accepts any TLS certificate presented by 324 | the server and any host name in that certificate. This only affects 325 | transport https (wss) connection. Chisel server's public key 326 | may be still verified (see --fingerprint) after inner connection 327 | is established. 328 | 329 | --tls-key, a path to a PEM encoded private key used for client 330 | authentication (mutual-TLS). 331 | 332 | --tls-cert, a path to a PEM encoded certificate matching the provided 333 | private key. The certificate must have client authentication 334 | enabled (mutual-TLS). 335 | 336 | --pid Generate pid file in current working directory 337 | 338 | -v, Enable verbose logging 339 | 340 | --help, This help text 341 | 342 | Signals: 343 | The chisel process is listening for: 344 | a SIGUSR2 to print process stats, and 345 | a SIGHUP to short-circuit the client reconnect timer 346 | 347 | Version: 348 | X.Y.Z 349 | 350 | Read more: 351 | https://github.com/jpillora/chisel 352 | 353 | ``` 354 | 355 | 356 | ### Security 357 | 358 | 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 file, using the `--keyfile` option. 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. 359 | 360 | ### Authentication 361 | 362 | 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. 363 | 364 | 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/. 365 | 366 | ### SOCKS5 Guide with Docker 367 | 368 | 1. Print a new private key to the terminal 369 | 370 | ```sh 371 | chisel server --keygen - 372 | # or save it to disk --keygen /path/to/mykey 373 | ``` 374 | 375 | 1. Start your chisel server 376 | 377 | ```sh 378 | jpillora/chisel server --keyfile '' -p 9312 --socks5 379 | ``` 380 | 381 | 1. Connect your chisel client (using server's fingerprint) 382 | 383 | ```sh 384 | chisel client --fingerprint '' :9312 socks 385 | ``` 386 | 387 | 1. Point your SOCKS5 clients (e.g. OS/Browser) to: 388 | 389 | ``` 390 | :1080 391 | ``` 392 | 393 | 1. Now you have an encrypted, authenticated SOCKS5 connection over HTTP 394 | 395 | 396 | #### Caveats 397 | 398 | Since WebSockets support is required: 399 | 400 | - 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) 401 | - PaaS providers vary in their support for WebSockets 402 | - Heroku has full support 403 | - Openshift has full support though connections are only accepted on ports 8443 and 8080 404 | - Google App Engine has **no** support (Track this on [their repo](https://code.google.com/p/googleappengine/issues/detail?id=2535)) 405 | 406 | ## Contributing 407 | 408 | - http://golang.org/doc/code.html 409 | - http://golang.org/doc/effective_go.html 410 | - `github.com/jpillora/chisel/share` contains the shared package 411 | - `github.com/jpillora/chisel/server` contains the server package 412 | - `github.com/jpillora/chisel/client` contains the client package 413 | 414 | ## Changelog 415 | 416 | - `1.0` - Initial release 417 | - `1.1` - Replaced simple symmetric encryption for ECDSA SSH 418 | - `1.2` - Added SOCKS5 (server) and HTTP CONNECT (client) support 419 | - `1.3` - Added reverse tunnelling support 420 | - `1.4` - Added arbitrary HTTP header support 421 | - `1.5` - Added reverse SOCKS support (by @aus) 422 | - `1.6` - Added client stdio support (by @BoleynSu) 423 | - `1.7` - Added UDP support 424 | - `1.8` - Move to a `scratch`Docker image 425 | - `1.9` - Bump to Go 1.21. Switch from `--key` seed to P256 key strings with `--key{gen,file}` (by @cmenginnz) 426 | - `1.10` - Bump to Go 1.22. Add `.rpm` `.deb` and `.akp` to releases. Fix bad version comparison. 427 | 428 | ## License 429 | 430 | [MIT](https://github.com/jpillora/chisel/blob/master/LICENSE) © Jaime Pillora 431 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package chclient 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "regexp" 16 | "strings" 17 | "time" 18 | 19 | "github.com/gorilla/websocket" 20 | chshare "github.com/jpillora/chisel/share" 21 | "github.com/jpillora/chisel/share/ccrypto" 22 | "github.com/jpillora/chisel/share/cio" 23 | "github.com/jpillora/chisel/share/cnet" 24 | "github.com/jpillora/chisel/share/settings" 25 | "github.com/jpillora/chisel/share/tunnel" 26 | 27 | "golang.org/x/crypto/ssh" 28 | "golang.org/x/net/proxy" 29 | "golang.org/x/sync/errgroup" 30 | ) 31 | 32 | // Config represents a client configuration 33 | type Config struct { 34 | Fingerprint string 35 | Auth string 36 | KeepAlive time.Duration 37 | MaxRetryCount int 38 | MaxRetryInterval time.Duration 39 | Server string 40 | Proxy string 41 | Remotes []string 42 | Headers http.Header 43 | TLS TLSConfig 44 | DialContext func(ctx context.Context, network, addr string) (net.Conn, error) 45 | Verbose bool 46 | } 47 | 48 | // TLSConfig for a Client 49 | type TLSConfig struct { 50 | SkipVerify bool 51 | CA string 52 | Cert string 53 | Key string 54 | ServerName string 55 | } 56 | 57 | // Client represents a client instance 58 | type Client struct { 59 | *cio.Logger 60 | config *Config 61 | computed settings.Config 62 | sshConfig *ssh.ClientConfig 63 | tlsConfig *tls.Config 64 | proxyURL *url.URL 65 | server string 66 | connCount cnet.ConnCount 67 | stop func() 68 | eg *errgroup.Group 69 | tunnel *tunnel.Tunnel 70 | } 71 | 72 | // NewClient creates a new client instance 73 | func NewClient(c *Config) (*Client, error) { 74 | //apply default scheme 75 | if !strings.HasPrefix(c.Server, "http") { 76 | c.Server = "http://" + c.Server 77 | } 78 | if c.MaxRetryInterval < time.Second { 79 | c.MaxRetryInterval = 5 * time.Minute 80 | } 81 | u, err := url.Parse(c.Server) 82 | if err != nil { 83 | return nil, err 84 | } 85 | //swap to websockets scheme 86 | u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1) 87 | //apply default port 88 | if !regexp.MustCompile(`:\d+$`).MatchString(u.Host) { 89 | if u.Scheme == "wss" { 90 | u.Host = u.Host + ":443" 91 | } else { 92 | u.Host = u.Host + ":80" 93 | } 94 | } 95 | hasReverse := false 96 | hasSocks := false 97 | hasStdio := false 98 | client := &Client{ 99 | Logger: cio.NewLogger("client"), 100 | config: c, 101 | computed: settings.Config{ 102 | Version: chshare.BuildVersion, 103 | }, 104 | server: u.String(), 105 | tlsConfig: nil, 106 | } 107 | //set default log level 108 | client.Logger.Info = true 109 | //configure tls 110 | if u.Scheme == "wss" { 111 | tc := &tls.Config{} 112 | if c.TLS.ServerName != "" { 113 | tc.ServerName = c.TLS.ServerName 114 | } 115 | //certificate verification config 116 | if c.TLS.SkipVerify { 117 | client.Infof("TLS verification disabled") 118 | tc.InsecureSkipVerify = true 119 | } else if c.TLS.CA != "" { 120 | rootCAs := x509.NewCertPool() 121 | if b, err := os.ReadFile(c.TLS.CA); err != nil { 122 | return nil, fmt.Errorf("Failed to load file: %s", c.TLS.CA) 123 | } else if ok := rootCAs.AppendCertsFromPEM(b); !ok { 124 | return nil, fmt.Errorf("Failed to decode PEM: %s", c.TLS.CA) 125 | } else { 126 | client.Infof("TLS verification using CA %s", c.TLS.CA) 127 | tc.RootCAs = rootCAs 128 | } 129 | } 130 | //provide client cert and key pair for mtls 131 | if c.TLS.Cert != "" && c.TLS.Key != "" { 132 | c, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key) 133 | if err != nil { 134 | return nil, fmt.Errorf("Error loading client cert and key pair: %v", err) 135 | } 136 | tc.Certificates = []tls.Certificate{c} 137 | } else if c.TLS.Cert != "" || c.TLS.Key != "" { 138 | return nil, fmt.Errorf("Please specify client BOTH cert and key") 139 | } 140 | client.tlsConfig = tc 141 | } 142 | //validate remotes 143 | for _, s := range c.Remotes { 144 | r, err := settings.DecodeRemote(s) 145 | if err != nil { 146 | return nil, fmt.Errorf("Failed to decode remote '%s': %s", s, err) 147 | } 148 | if r.Socks { 149 | hasSocks = true 150 | } 151 | if r.Reverse { 152 | hasReverse = true 153 | } 154 | if r.Stdio { 155 | if hasStdio { 156 | return nil, errors.New("Only one stdio is allowed") 157 | } 158 | hasStdio = true 159 | } 160 | //confirm non-reverse tunnel is available 161 | if !r.Reverse && !r.Stdio && !r.CanListen() { 162 | return nil, fmt.Errorf("Client cannot listen on %s", r.String()) 163 | } 164 | client.computed.Remotes = append(client.computed.Remotes, r) 165 | } 166 | //outbound proxy 167 | if p := c.Proxy; p != "" { 168 | client.proxyURL, err = url.Parse(p) 169 | if err != nil { 170 | return nil, fmt.Errorf("Invalid proxy URL (%s)", err) 171 | } 172 | } 173 | //ssh auth and config 174 | user, pass := settings.ParseAuth(c.Auth) 175 | client.sshConfig = &ssh.ClientConfig{ 176 | User: user, 177 | Auth: []ssh.AuthMethod{ssh.Password(pass)}, 178 | ClientVersion: "SSH-" + chshare.ProtocolVersion + "-client", 179 | HostKeyCallback: client.verifyServer, 180 | Timeout: settings.EnvDuration("SSH_TIMEOUT", 30*time.Second), 181 | } 182 | //prepare client tunnel 183 | client.tunnel = tunnel.New(tunnel.Config{ 184 | Logger: client.Logger, 185 | Inbound: true, //client always accepts inbound 186 | Outbound: hasReverse, 187 | Socks: hasReverse && hasSocks, 188 | KeepAlive: client.config.KeepAlive, 189 | }) 190 | return client, nil 191 | } 192 | 193 | // Run starts client and blocks while connected 194 | func (c *Client) Run() error { 195 | ctx, cancel := context.WithCancel(context.Background()) 196 | defer cancel() 197 | if err := c.Start(ctx); err != nil { 198 | return err 199 | } 200 | return c.Wait() 201 | } 202 | 203 | func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error { 204 | expect := c.config.Fingerprint 205 | if expect == "" { 206 | return nil 207 | } 208 | got := ccrypto.FingerprintKey(key) 209 | _, err := base64.StdEncoding.DecodeString(expect) 210 | if _, ok := err.(base64.CorruptInputError); ok { 211 | c.Logger.Infof("Specified deprecated MD5 fingerprint (%s), please update to the new SHA256 fingerprint: %s", expect, got) 212 | return c.verifyLegacyFingerprint(key) 213 | } else if err != nil { 214 | return fmt.Errorf("Error decoding fingerprint: %w", err) 215 | } 216 | if got != expect { 217 | return fmt.Errorf("Invalid fingerprint (%s)", got) 218 | } 219 | //overwrite with complete fingerprint 220 | c.Infof("Fingerprint %s", got) 221 | return nil 222 | } 223 | 224 | // verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints 225 | func (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error { 226 | bytes := md5.Sum(key.Marshal()) 227 | strbytes := make([]string, len(bytes)) 228 | for i, b := range bytes { 229 | strbytes[i] = fmt.Sprintf("%02x", b) 230 | } 231 | got := strings.Join(strbytes, ":") 232 | expect := c.config.Fingerprint 233 | if !strings.HasPrefix(got, expect) { 234 | return fmt.Errorf("Invalid fingerprint (%s)", got) 235 | } 236 | return nil 237 | } 238 | 239 | // Start client and does not block 240 | func (c *Client) Start(ctx context.Context) error { 241 | ctx, cancel := context.WithCancel(ctx) 242 | c.stop = cancel 243 | eg, ctx := errgroup.WithContext(ctx) 244 | c.eg = eg 245 | via := "" 246 | if c.proxyURL != nil { 247 | via = " via " + c.proxyURL.String() 248 | } 249 | c.Infof("Connecting to %s%s\n", c.server, via) 250 | //connect to chisel server 251 | eg.Go(func() error { 252 | return c.connectionLoop(ctx) 253 | }) 254 | //listen sockets 255 | eg.Go(func() error { 256 | clientInbound := c.computed.Remotes.Reversed(false) 257 | if len(clientInbound) == 0 { 258 | return nil 259 | } 260 | return c.tunnel.BindRemotes(ctx, clientInbound) 261 | }) 262 | return nil 263 | } 264 | 265 | func (c *Client) setProxy(u *url.URL, d *websocket.Dialer) error { 266 | // CONNECT proxy 267 | if !strings.HasPrefix(u.Scheme, "socks") { 268 | d.Proxy = func(*http.Request) (*url.URL, error) { 269 | return u, nil 270 | } 271 | return nil 272 | } 273 | // SOCKS5 proxy 274 | if u.Scheme != "socks" && u.Scheme != "socks5h" { 275 | return fmt.Errorf( 276 | "unsupported socks proxy type: %s:// (only socks5h:// or socks:// is supported)", 277 | u.Scheme, 278 | ) 279 | } 280 | var auth *proxy.Auth 281 | if u.User != nil { 282 | pass, _ := u.User.Password() 283 | auth = &proxy.Auth{ 284 | User: u.User.Username(), 285 | Password: pass, 286 | } 287 | } 288 | socksDialer, err := proxy.SOCKS5("tcp", u.Host, auth, proxy.Direct) 289 | if err != nil { 290 | return err 291 | } 292 | d.NetDial = socksDialer.Dial 293 | return nil 294 | } 295 | 296 | // Wait blocks while the client is running. 297 | func (c *Client) Wait() error { 298 | return c.eg.Wait() 299 | } 300 | 301 | // Close manually stops the client 302 | func (c *Client) Close() error { 303 | if c.stop != nil { 304 | c.stop() 305 | } 306 | return nil 307 | } 308 | -------------------------------------------------------------------------------- /client/client_connect.go: -------------------------------------------------------------------------------- 1 | package chclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 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, 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 | maxAttemptVal := fmt.Sprint(maxAttempt) 41 | if maxAttempt < 0 { 42 | maxAttemptVal = "unlimited" 43 | } 44 | msg += fmt.Sprintf(" (Attempt: %d/%s)", attempt, maxAttemptVal) 45 | } 46 | c.Infof(msg) 47 | } 48 | //give up? 49 | if 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 bool, err error) { 69 | //already closed? 70 | select { 71 | case <-ctx.Done(): 72 | return 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 | NetDialContext: c.config.DialContext, 86 | } 87 | //optional proxy 88 | if p := c.proxyURL; p != nil { 89 | if err := c.setProxy(p, &d); err != nil { 90 | return false, err 91 | } 92 | } 93 | wsConn, _, err := d.DialContext(ctx, c.server, c.config.Headers) 94 | if err != nil { 95 | return false, err 96 | } 97 | conn := cnet.NewWebSocketConn(wsConn) 98 | // perform SSH handshake on net.Conn 99 | c.Debugf("Handshaking...") 100 | sshConn, chans, reqs, err := ssh.NewClientConn(conn, "", c.sshConfig) 101 | if err != nil { 102 | e := err.Error() 103 | if strings.Contains(e, "unable to authenticate") { 104 | c.Infof("Authentication failed") 105 | c.Debugf(e) 106 | } else { 107 | c.Infof(e) 108 | } 109 | return false, err 110 | } 111 | defer sshConn.Close() 112 | // chisel client handshake (reverse of server handshake) 113 | // send configuration 114 | c.Debugf("Sending config") 115 | t0 := time.Now() 116 | _, configerr, err := sshConn.SendRequest( 117 | "config", 118 | true, 119 | settings.EncodeConfig(c.computed), 120 | ) 121 | if err != nil { 122 | c.Infof("Config verification failed") 123 | return false, err 124 | } 125 | if len(configerr) > 0 { 126 | return false, errors.New(string(configerr)) 127 | } 128 | c.Infof("Connected (Latency %s)", time.Since(t0)) 129 | //connected, handover ssh connection for tunnel to use, and block 130 | err = c.tunnel.BindSSH(ctx, sshConn, reqs, chans) 131 | c.Infof("Disconnected") 132 | connected = time.Since(t0) > 5*time.Second 133 | return connected, err 134 | } 135 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package chclient 2 | 3 | import ( 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 := ccrypto.GenerateKeyGo119(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 := ccrypto.GenerateKeyGo119(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 := ccrypto.GenerateKeyGo119(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 | } 115 | -------------------------------------------------------------------------------- /example/Flyfile: -------------------------------------------------------------------------------- 1 | FROM jpillora/chisel 2 | ENTRYPOINT ["/app/bin", "server", "--port", "443", "--tls-domain", "chisel.jpillora.com"] -------------------------------------------------------------------------------- /example/fly.toml: -------------------------------------------------------------------------------- 1 | app = "jp-chisel" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | processes = [] 5 | 6 | [build] 7 | dockerfile = "Flyfile" 8 | 9 | [[services]] 10 | internal_port = 443 11 | protocol = "tcp" 12 | [[services.ports]] 13 | port = "443" -------------------------------------------------------------------------------- /example/reverse-tunneling-authenticated.md: -------------------------------------------------------------------------------- 1 | # Reverse Tunneling 2 | 3 | > **Use Case**: Host a website on your Raspberry Pi without opening ports on your router. 4 | 5 | This guide will show you how to use an internet-facing server (for example, a cloud VPS) as a relay to bounce down TCP traffic on port 80 to your Raspberry Pi. 6 | 7 | ## Chisel CLI 8 | 9 | ### Server 10 | 11 | Setup a relay server on the VPS to bounce down TCP traffic on port 80: 12 | 13 | ```bash 14 | #!/bin/bash 15 | 16 | # ⬇️ Start Chisel server in Reverse mode 17 | chisel server --reverse \ 18 | 19 | # ⬇️ Use the include users.json as an authfile 20 | --authfile="./users.json" \ 21 | ``` 22 | 23 | The corresponding `authfile` might look like this: 24 | 25 | ```json 26 | { 27 | "foo:bar": ["0.0.0.0:80"] 28 | } 29 | ``` 30 | 31 | ### Client 32 | 33 | Setup a chisel client to receive bounced-down traffic and forward it to the webserver running on the Pi: 34 | 35 | ```bash 36 | #!/bin/bash 37 | 38 | chisel client \ 39 | 40 | # ⬇️ Authenticates user "foo" with password "bar" 41 | --auth="foo:bar" \ 42 | 43 | # ⬇️ Connects to chisel relay server example.com 44 | # listening on the default ("fallback") port, 8080 45 | example.com \ 46 | 47 | # ⬇️ Reverse tunnels port 80 on the relay server to 48 | # port 80 on your Pi. 49 | R:80:localhost:80 50 | ``` 51 | 52 | --- 53 | 54 | ## Chisel Container 55 | 56 | This guide makes use of Docker and Docker compose to accomplish the same task as the above guide. 57 | ### Server 58 | 59 | Setup a relay server on the VPS to bounce down TCP traffic on port 80: 60 | 61 | ```yaml 62 | version: '3' 63 | 64 | services: 65 | chisel: 66 | image: jpillora/chisel 67 | restart: unless-stopped 68 | container_name: chisel 69 | # ⬇️ Pass CLI arguments one at a time in an array, as required by Docker compose. 70 | command: 71 | - 'server' 72 | # ⬇️ Use the --key=value syntax, since Docker compose doesn't parse whitespace well. 73 | - '--authfile=/users.json' 74 | - '--reverse' 75 | # ⬇️ Mount the authfile as a Docker volume 76 | volumes: 77 | - './users.json:/users.json' 78 | # ⬇️ Give the container unrestricted access to the Docker host's network 79 | network_mode: host 80 | ``` 81 | 82 | The `authfile` (`users.json`) remains the same as in the non-containerized version - shown again with the username `foo` and password `bar`. 83 | 84 | ```json 85 | { 86 | "foo:bar": ["0.0.0.0:80"] 87 | } 88 | ``` 89 | 90 | ### Client 91 | 92 | Setup an instance of the Chisel client on the Pi to receive relayed TCP traffic and feed it to the web server: 93 | 94 | ```yaml 95 | version: '3' 96 | 97 | services: 98 | chisel: 99 | # ⬇️ Delay starting Chisel server until the web server container is started. 100 | depends_on: 101 | - webserver 102 | image: jpillora/chisel 103 | restart: unless-stopped 104 | container_name: 'chisel' 105 | command: 106 | - 'client' 107 | # ⬇️ Use username `foo` and password `bar` to authenticate with Chisel server. 108 | - '--auth=foo:bar' 109 | # ⬇️ Domain & port of Chisel server. Port defaults to 8080 on server, but must be manually set on client. 110 | - 'proxy.example.com:8080' 111 | # ⬇️ Reverse tunnel traffic from the chisel server to the web server container, identified in Docker using DNS by its service name `webserver`. 112 | - 'R:80:webserver:80' 113 | networks: 114 | - internal 115 | # ⬇️ Basic Nginx webserver for demo purposes. 116 | webserver: 117 | image: nginx 118 | restart: unless-stopped 119 | container_name: nginx 120 | networks: 121 | - internal 122 | 123 | # ⬇️ Make use of a Docker network called `internal`. 124 | networks: 125 | internal: 126 | ``` 127 | -------------------------------------------------------------------------------- /example/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "root:toor": [ 3 | "" 4 | ], 5 | "foo:bar": [ 6 | "^0.0.0.0:3000$" 7 | ], 8 | "ping:pong": [ 9 | "^0.0.0.0:[45]000$", 10 | "^example.com:80$", 11 | "^R:0.0.0.0:7000$" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jpillora/chisel 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 7 | github.com/fsnotify/fsnotify v1.6.0 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/jpillora/backoff v1.0.0 10 | github.com/jpillora/requestlog v1.0.0 11 | github.com/jpillora/sizestr v1.0.0 12 | golang.org/x/crypto v0.16.0 13 | golang.org/x/net v0.14.0 14 | golang.org/x/sync v0.5.0 15 | ) 16 | 17 | require ( 18 | github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect 19 | github.com/jpillora/ansi v1.0.3 // indirect 20 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect 21 | golang.org/x/sys v0.15.0 // indirect 22 | golang.org/x/text v0.14.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM= 2 | github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os= 3 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 4 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 5 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 6 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 7 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 8 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | github.com/jpillora/ansi v1.0.3 h1:nn4Jzti0EmRfDxm7JtEs5LzCbNwd5sv+0aE+LdS9/ZQ= 10 | github.com/jpillora/ansi v1.0.3/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY= 11 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 12 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 13 | github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA= 14 | github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8= 15 | github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw= 16 | github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0= 17 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= 18 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= 19 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 20 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 21 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 22 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 23 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 24 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 25 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 27 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 29 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 30 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 31 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 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/ccrypto" 18 | "github.com/jpillora/chisel/share/cos" 19 | "github.com/jpillora/chisel/share/settings" 20 | ) 21 | 22 | var help = ` 23 | Usage: chisel [command] [--help] 24 | 25 | Version: ` + chshare.BuildVersion + ` (` + runtime.Version() + `) 26 | 27 | Commands: 28 | server - runs chisel in server mode 29 | client - runs chisel in client mode 30 | 31 | Read more: 32 | https://github.com/jpillora/chisel 33 | 34 | ` 35 | 36 | func main() { 37 | 38 | version := flag.Bool("version", false, "") 39 | v := flag.Bool("v", false, "") 40 | flag.Bool("help", false, "") 41 | flag.Bool("h", false, "") 42 | flag.Usage = func() {} 43 | flag.Parse() 44 | 45 | if *version || *v { 46 | fmt.Println(chshare.BuildVersion) 47 | os.Exit(0) 48 | } 49 | 50 | args := flag.Args() 51 | 52 | subcmd := "" 53 | if len(args) > 0 { 54 | subcmd = args[0] 55 | args = args[1:] 56 | } 57 | 58 | switch subcmd { 59 | case "server": 60 | server(args) 61 | case "client": 62 | client(args) 63 | default: 64 | fmt.Print(help) 65 | os.Exit(0) 66 | } 67 | } 68 | 69 | var commonHelp = ` 70 | --pid Generate pid file in current working directory 71 | 72 | -v, Enable verbose logging 73 | 74 | --help, This help text 75 | 76 | Signals: 77 | The chisel process is listening for: 78 | a SIGUSR2 to print process stats, and 79 | a SIGHUP to short-circuit the client reconnect timer 80 | 81 | Version: 82 | ` + chshare.BuildVersion + ` (` + runtime.Version() + `) 83 | 84 | Read more: 85 | https://github.com/jpillora/chisel 86 | 87 | ` 88 | 89 | func generatePidFile() { 90 | pid := []byte(strconv.Itoa(os.Getpid())) 91 | if err := os.WriteFile("chisel.pid", pid, 0644); err != nil { 92 | log.Fatal(err) 93 | } 94 | } 95 | 96 | var serverHelp = ` 97 | Usage: chisel server [options] 98 | 99 | Options: 100 | 101 | --host, Defines the HTTP listening host – the network interface 102 | (defaults the environment variable HOST and falls back to 0.0.0.0). 103 | 104 | --port, -p, Defines the HTTP listening port (defaults to the environment 105 | variable PORT and fallsback to port 8080). 106 | 107 | --key, (deprecated use --keygen and --keyfile instead) 108 | An optional string to seed the generation of a ECDSA public 109 | and private key pair. All communications will be secured using this 110 | key pair. Share the subsequent fingerprint with clients to enable detection 111 | of man-in-the-middle attacks (defaults to the CHISEL_KEY environment 112 | variable, otherwise a new key is generate each run). 113 | 114 | --keygen, A path to write a newly generated PEM-encoded SSH private key file. 115 | If users depend on your --key fingerprint, you may also include your --key to 116 | output your existing key. Use - (dash) to output the generated key to stdout. 117 | 118 | --keyfile, An optional path to a PEM-encoded SSH private key. When 119 | this flag is set, the --key option is ignored, and the provided private key 120 | is used to secure all communications. (defaults to the CHISEL_KEY_FILE 121 | environment variable). Since ECDSA keys are short, you may also set keyfile 122 | to an inline base64 private key (e.g. chisel server --keygen - | base64). 123 | 124 | --authfile, An optional path to a users.json file. This file should 125 | be an object with users defined like: 126 | { 127 | "": ["",""] 128 | } 129 | when connects, their will be verified and then 130 | each of the remote addresses will be compared against the list 131 | of address regular expressions for a match. Addresses will 132 | always come in the form ":" for normal remotes 133 | and "R::" for reverse port forwarding 134 | remotes. This file will be automatically reloaded on change. 135 | 136 | --auth, An optional string representing a single user with full 137 | access, in the form of . It is equivalent to creating an 138 | authfile with {"": [""]}. If unset, it will use the 139 | environment variable AUTH. 140 | 141 | --keepalive, An optional keepalive interval. Since the underlying 142 | transport is HTTP, in many instances we'll be traversing through 143 | proxies, often these proxies will close idle connections. You must 144 | specify a time with a unit, for example '5s' or '2m'. Defaults 145 | to '25s' (set to 0s to disable). 146 | 147 | --backend, Specifies another HTTP server to proxy requests to when 148 | chisel receives a normal HTTP request. Useful for hiding chisel in 149 | plain sight. 150 | 151 | --socks5, Allow clients to access the internal SOCKS5 proxy. See 152 | chisel client --help for more information. 153 | 154 | --reverse, Allow clients to specify reverse port forwarding remotes 155 | in addition to normal remotes. 156 | 157 | --tls-key, Enables TLS and provides optional path to a PEM-encoded 158 | TLS private key. When this flag is set, you must also set --tls-cert, 159 | and you cannot set --tls-domain. 160 | 161 | --tls-cert, Enables TLS and provides optional path to a PEM-encoded 162 | TLS certificate. When this flag is set, you must also set --tls-key, 163 | and you cannot set --tls-domain. 164 | 165 | --tls-domain, Enables TLS and automatically acquires a TLS key and 166 | certificate using LetsEncrypt. Setting --tls-domain requires port 443. 167 | You may specify multiple --tls-domain flags to serve multiple domains. 168 | The resulting files are cached in the "$HOME/.cache/chisel" directory. 169 | You can modify this path by setting the CHISEL_LE_CACHE variable, 170 | or disable caching by setting this variable to "-". You can optionally 171 | provide a certificate notification email by setting CHISEL_LE_EMAIL. 172 | 173 | --tls-ca, a path to a PEM encoded CA certificate bundle or a directory 174 | holding multiple PEM encode CA certificate bundle files, which is used to 175 | validate client connections. The provided CA certificates will be used 176 | instead of the system roots. This is commonly used to implement mutual-TLS. 177 | ` + commonHelp 178 | 179 | func server(args []string) { 180 | 181 | flags := flag.NewFlagSet("server", flag.ContinueOnError) 182 | 183 | config := &chserver.Config{} 184 | flags.StringVar(&config.KeySeed, "key", "", "") 185 | flags.StringVar(&config.KeyFile, "keyfile", "", "") 186 | flags.StringVar(&config.AuthFile, "authfile", "", "") 187 | flags.StringVar(&config.Auth, "auth", "", "") 188 | flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "") 189 | flags.StringVar(&config.Proxy, "proxy", "", "") 190 | flags.StringVar(&config.Proxy, "backend", "", "") 191 | flags.BoolVar(&config.Socks5, "socks5", false, "") 192 | flags.BoolVar(&config.Reverse, "reverse", false, "") 193 | flags.StringVar(&config.TLS.Key, "tls-key", "", "") 194 | flags.StringVar(&config.TLS.Cert, "tls-cert", "", "") 195 | flags.Var(multiFlag{&config.TLS.Domains}, "tls-domain", "") 196 | flags.StringVar(&config.TLS.CA, "tls-ca", "", "") 197 | 198 | host := flags.String("host", "", "") 199 | p := flags.String("p", "", "") 200 | port := flags.String("port", "", "") 201 | pid := flags.Bool("pid", false, "") 202 | verbose := flags.Bool("v", false, "") 203 | keyGen := flags.String("keygen", "", "") 204 | 205 | flags.Usage = func() { 206 | fmt.Print(serverHelp) 207 | os.Exit(0) 208 | } 209 | flags.Parse(args) 210 | 211 | if *keyGen != "" { 212 | if err := ccrypto.GenerateKeyFile(*keyGen, config.KeySeed); err != nil { 213 | log.Fatal(err) 214 | } 215 | return 216 | } 217 | 218 | if config.KeySeed != "" { 219 | log.Print("Option `--key` is deprecated and will be removed in a future version of chisel.") 220 | log.Print("Please use `chisel server --keygen /file/path`, followed by `chisel server --keyfile /file/path` to specify the SSH private key") 221 | } 222 | 223 | if *host == "" { 224 | *host = os.Getenv("HOST") 225 | } 226 | if *host == "" { 227 | *host = "0.0.0.0" 228 | } 229 | if *port == "" { 230 | *port = *p 231 | } 232 | if *port == "" { 233 | *port = os.Getenv("PORT") 234 | } 235 | if *port == "" { 236 | *port = "8080" 237 | } 238 | if config.KeyFile == "" { 239 | config.KeyFile = settings.Env("KEY_FILE") 240 | } else if config.KeySeed == "" { 241 | config.KeySeed = settings.Env("KEY") 242 | } 243 | if config.Auth == "" { 244 | config.Auth = os.Getenv("AUTH") 245 | } 246 | s, err := chserver.NewServer(config) 247 | if err != nil { 248 | log.Fatal(err) 249 | } 250 | s.Debug = *verbose 251 | if *pid { 252 | generatePidFile() 253 | } 254 | go cos.GoStats() 255 | ctx := cos.InterruptContext() 256 | if err := s.StartContext(ctx, *host, *port); err != nil { 257 | log.Fatal(err) 258 | } 259 | if err := s.Wait(); err != nil { 260 | log.Fatal(err) 261 | } 262 | } 263 | 264 | type multiFlag struct { 265 | values *[]string 266 | } 267 | 268 | func (flag multiFlag) String() string { 269 | return strings.Join(*flag.values, ", ") 270 | } 271 | 272 | func (flag multiFlag) Set(arg string) error { 273 | *flag.values = append(*flag.values, arg) 274 | return nil 275 | } 276 | 277 | type headerFlags struct { 278 | http.Header 279 | } 280 | 281 | func (flag *headerFlags) String() string { 282 | out := "" 283 | for k, v := range flag.Header { 284 | out += fmt.Sprintf("%s: %s\n", k, v) 285 | } 286 | return out 287 | } 288 | 289 | func (flag *headerFlags) Set(arg string) error { 290 | index := strings.Index(arg, ":") 291 | if index < 0 { 292 | return fmt.Errorf(`Invalid header (%s). Should be in the format "HeaderName: HeaderContent"`, arg) 293 | } 294 | if flag.Header == nil { 295 | flag.Header = http.Header{} 296 | } 297 | key := arg[0:index] 298 | value := arg[index+1:] 299 | flag.Header.Set(key, strings.TrimSpace(value)) 300 | return nil 301 | } 302 | 303 | var clientHelp = ` 304 | Usage: chisel client [options] [remote] [remote] ... 305 | 306 | is the URL to the chisel server. 307 | 308 | s are remote connections tunneled through the server, each of 309 | which come in the form: 310 | 311 | :::/ 312 | 313 | ■ local-host defaults to 0.0.0.0 (all interfaces). 314 | ■ local-port defaults to remote-port. 315 | ■ remote-port is required*. 316 | ■ remote-host defaults to 0.0.0.0 (server localhost). 317 | ■ protocol defaults to tcp. 318 | 319 | which shares : from the server to the client 320 | as :, or: 321 | 322 | R::::/ 323 | 324 | which does reverse port forwarding, sharing : 325 | from the client to the server's :. 326 | 327 | example remotes 328 | 329 | 3000 330 | example.com:3000 331 | 3000:google.com:80 332 | 192.168.0.5:3000:google.com:80 333 | socks 334 | 5000:socks 335 | R:2222:localhost:22 336 | R:socks 337 | R:5000:socks 338 | stdio:example.com:22 339 | 1.1.1.1:53/udp 340 | 341 | When the chisel server has --socks5 enabled, remotes can 342 | specify "socks" in place of remote-host and remote-port. 343 | The default local host and port for a "socks" remote is 344 | 127.0.0.1:1080. Connections to this remote will terminate 345 | at the server's internal SOCKS5 proxy. 346 | 347 | When the chisel server has --reverse enabled, remotes can 348 | be prefixed with R to denote that they are reversed. That 349 | is, the server will listen and accept connections, and they 350 | will be proxied through the client which specified the remote. 351 | Reverse remotes specifying "R:socks" will listen on the server's 352 | default socks port (1080) and terminate the connection at the 353 | client's internal SOCKS5 proxy. 354 | 355 | When stdio is used as local-host, the tunnel will connect standard 356 | input/output of this program with the remote. This is useful when 357 | combined with ssh ProxyCommand. You can use 358 | ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \ 359 | user@example.com 360 | to connect to an SSH server through the tunnel. 361 | 362 | Options: 363 | 364 | --fingerprint, A *strongly recommended* fingerprint string 365 | to perform host-key validation against the server's public key. 366 | Fingerprint mismatches will close the connection. 367 | Fingerprints are generated by hashing the ECDSA public key using 368 | SHA256 and encoding the result in base64. 369 | Fingerprints must be 44 characters containing a trailing equals (=). 370 | 371 | --auth, An optional username and password (client authentication) 372 | in the form: ":". These credentials are compared to 373 | the credentials inside the server's --authfile. defaults to the 374 | AUTH environment variable. 375 | 376 | --keepalive, An optional keepalive interval. Since the underlying 377 | transport is HTTP, in many instances we'll be traversing through 378 | proxies, often these proxies will close idle connections. You must 379 | specify a time with a unit, for example '5s' or '2m'. Defaults 380 | to '25s' (set to 0s to disable). 381 | 382 | --max-retry-count, Maximum number of times to retry before exiting. 383 | Defaults to unlimited. 384 | 385 | --max-retry-interval, Maximum wait time before retrying after a 386 | disconnection. Defaults to 5 minutes. 387 | 388 | --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be 389 | used to reach the chisel server. Authentication can be specified 390 | inside the URL. 391 | For example, http://admin:password@my-server.com:8081 392 | or: socks://admin:password@my-server.com:1080 393 | 394 | --header, Set a custom header in the form "HeaderName: HeaderContent". 395 | Can be used multiple times. (e.g --header "Foo: Bar" --header "Hello: World") 396 | 397 | --hostname, Optionally set the 'Host' header (defaults to the host 398 | found in the server url). 399 | 400 | --sni, Override the ServerName when using TLS (defaults to the 401 | hostname). 402 | 403 | --tls-ca, An optional root certificate bundle used to verify the 404 | chisel server. Only valid when connecting to the server with 405 | "https" or "wss". By default, the operating system CAs will be used. 406 | 407 | --tls-skip-verify, Skip server TLS certificate verification of 408 | chain and host name (if TLS is used for transport connections to 409 | server). If set, client accepts any TLS certificate presented by 410 | the server and any host name in that certificate. This only affects 411 | transport https (wss) connection. Chisel server's public key 412 | may be still verified (see --fingerprint) after inner connection 413 | is established. 414 | 415 | --tls-key, a path to a PEM encoded private key used for client 416 | authentication (mutual-TLS). 417 | 418 | --tls-cert, a path to a PEM encoded certificate matching the provided 419 | private key. The certificate must have client authentication 420 | enabled (mutual-TLS). 421 | ` + commonHelp 422 | 423 | func client(args []string) { 424 | flags := flag.NewFlagSet("client", flag.ContinueOnError) 425 | config := chclient.Config{Headers: http.Header{}} 426 | flags.StringVar(&config.Fingerprint, "fingerprint", "", "") 427 | flags.StringVar(&config.Auth, "auth", "", "") 428 | flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "") 429 | flags.IntVar(&config.MaxRetryCount, "max-retry-count", -1, "") 430 | flags.DurationVar(&config.MaxRetryInterval, "max-retry-interval", 0, "") 431 | flags.StringVar(&config.Proxy, "proxy", "", "") 432 | flags.StringVar(&config.TLS.CA, "tls-ca", "", "") 433 | flags.BoolVar(&config.TLS.SkipVerify, "tls-skip-verify", false, "") 434 | flags.StringVar(&config.TLS.Cert, "tls-cert", "", "") 435 | flags.StringVar(&config.TLS.Key, "tls-key", "", "") 436 | flags.Var(&headerFlags{config.Headers}, "header", "") 437 | hostname := flags.String("hostname", "", "") 438 | sni := flags.String("sni", "", "") 439 | pid := flags.Bool("pid", false, "") 440 | verbose := flags.Bool("v", false, "") 441 | flags.Usage = func() { 442 | fmt.Print(clientHelp) 443 | os.Exit(0) 444 | } 445 | flags.Parse(args) 446 | //pull out options, put back remaining args 447 | args = flags.Args() 448 | if len(args) < 2 { 449 | log.Fatalf("A server and least one remote is required") 450 | } 451 | config.Server = args[0] 452 | config.Remotes = args[1:] 453 | //default auth 454 | if config.Auth == "" { 455 | config.Auth = os.Getenv("AUTH") 456 | } 457 | //move hostname onto headers 458 | if *hostname != "" { 459 | config.Headers.Set("Host", *hostname) 460 | config.TLS.ServerName = *hostname 461 | } 462 | 463 | if *sni != "" { 464 | config.TLS.ServerName = *sni 465 | } 466 | 467 | //ready 468 | c, err := chclient.NewClient(&config) 469 | if err != nil { 470 | log.Fatal(err) 471 | } 472 | c.Debug = *verbose 473 | if *pid { 474 | generatePidFile() 475 | } 476 | go cos.GoStats() 477 | ctx := cos.InterruptContext() 478 | if err := c.Start(ctx); err != nil { 479 | log.Fatal(err) 480 | } 481 | if err := c.Wait(); err != nil { 482 | log.Fatal(err) 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package chserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "os" 11 | "regexp" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | chshare "github.com/jpillora/chisel/share" 16 | "github.com/jpillora/chisel/share/ccrypto" 17 | "github.com/jpillora/chisel/share/cio" 18 | "github.com/jpillora/chisel/share/cnet" 19 | "github.com/jpillora/chisel/share/settings" 20 | "github.com/jpillora/requestlog" 21 | "golang.org/x/crypto/ssh" 22 | ) 23 | 24 | // Config is the configuration for the chisel service 25 | type Config struct { 26 | KeySeed string 27 | KeyFile string 28 | AuthFile string 29 | Auth string 30 | Proxy string 31 | Socks5 bool 32 | Reverse bool 33 | KeepAlive time.Duration 34 | TLS TLSConfig 35 | } 36 | 37 | // Server respresent a chisel service 38 | type Server struct { 39 | *cio.Logger 40 | config *Config 41 | fingerprint string 42 | httpServer *cnet.HTTPServer 43 | reverseProxy *httputil.ReverseProxy 44 | sessCount int32 45 | sessions *settings.Users 46 | sshConfig *ssh.ServerConfig 47 | users *settings.UserIndex 48 | } 49 | 50 | var upgrader = websocket.Upgrader{ 51 | CheckOrigin: func(r *http.Request) bool { return true }, 52 | ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), 53 | WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0), 54 | } 55 | 56 | // NewServer creates and returns a new chisel server 57 | func NewServer(c *Config) (*Server, error) { 58 | server := &Server{ 59 | config: c, 60 | httpServer: cnet.NewHTTPServer(), 61 | Logger: cio.NewLogger("server"), 62 | sessions: settings.NewUsers(), 63 | } 64 | server.Info = true 65 | server.users = settings.NewUserIndex(server.Logger) 66 | if c.AuthFile != "" { 67 | if err := server.users.LoadUsers(c.AuthFile); err != nil { 68 | return nil, err 69 | } 70 | } 71 | if c.Auth != "" { 72 | u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}} 73 | u.Name, u.Pass = settings.ParseAuth(c.Auth) 74 | if u.Name != "" { 75 | server.users.AddUser(u) 76 | } 77 | } 78 | 79 | var pemBytes []byte 80 | var err error 81 | if c.KeyFile != "" { 82 | var key []byte 83 | 84 | if ccrypto.IsChiselKey([]byte(c.KeyFile)) { 85 | key = []byte(c.KeyFile) 86 | } else { 87 | key, err = os.ReadFile(c.KeyFile) 88 | if err != nil { 89 | log.Fatalf("Failed to read key file %s", c.KeyFile) 90 | } 91 | } 92 | 93 | pemBytes = key 94 | if ccrypto.IsChiselKey(key) { 95 | pemBytes, err = ccrypto.ChiselKey2PEM(key) 96 | if err != nil { 97 | log.Fatalf("Invalid key %s", string(key)) 98 | } 99 | } 100 | } else { 101 | //generate private key (optionally using seed) 102 | pemBytes, err = ccrypto.Seed2PEM(c.KeySeed) 103 | if err != nil { 104 | log.Fatal("Failed to generate key") 105 | } 106 | } 107 | 108 | //convert into ssh.PrivateKey 109 | private, err := ssh.ParsePrivateKey(pemBytes) 110 | if err != nil { 111 | log.Fatal("Failed to parse key") 112 | } 113 | //fingerprint this key 114 | server.fingerprint = ccrypto.FingerprintKey(private.PublicKey()) 115 | //create ssh config 116 | server.sshConfig = &ssh.ServerConfig{ 117 | ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server", 118 | PasswordCallback: server.authUser, 119 | } 120 | server.sshConfig.AddHostKey(private) 121 | //setup reverse proxy 122 | if c.Proxy != "" { 123 | u, err := url.Parse(c.Proxy) 124 | if err != nil { 125 | return nil, err 126 | } 127 | if u.Host == "" { 128 | return nil, server.Errorf("Missing protocol (%s)", u) 129 | } 130 | server.reverseProxy = httputil.NewSingleHostReverseProxy(u) 131 | //always use proxy host 132 | server.reverseProxy.Director = func(r *http.Request) { 133 | //enforce origin, keep path 134 | r.URL.Scheme = u.Scheme 135 | r.URL.Host = u.Host 136 | r.Host = u.Host 137 | } 138 | } 139 | //print when reverse tunnelling is enabled 140 | if c.Reverse { 141 | server.Infof("Reverse tunnelling enabled") 142 | } 143 | return server, nil 144 | } 145 | 146 | // Run is responsible for starting the chisel service. 147 | // Internally this calls Start then Wait. 148 | func (s *Server) Run(host, port string) error { 149 | if err := s.Start(host, port); err != nil { 150 | return err 151 | } 152 | return s.Wait() 153 | } 154 | 155 | // Start is responsible for kicking off the http server 156 | func (s *Server) Start(host, port string) error { 157 | return s.StartContext(context.Background(), host, port) 158 | } 159 | 160 | // StartContext is responsible for kicking off the http server, 161 | // and can be closed by cancelling the provided context 162 | func (s *Server) StartContext(ctx context.Context, host, port string) error { 163 | s.Infof("Fingerprint %s", s.fingerprint) 164 | if s.users.Len() > 0 { 165 | s.Infof("User authentication enabled") 166 | } 167 | if s.reverseProxy != nil { 168 | s.Infof("Reverse proxy enabled") 169 | } 170 | l, err := s.listener(host, port) 171 | if err != nil { 172 | return err 173 | } 174 | h := http.Handler(http.HandlerFunc(s.handleClientHandler)) 175 | if s.Debug { 176 | o := requestlog.DefaultOptions 177 | o.TrustProxy = true 178 | h = requestlog.WrapWith(h, o) 179 | } 180 | return s.httpServer.GoServe(ctx, l, h) 181 | } 182 | 183 | // Wait waits for the http server to close 184 | func (s *Server) Wait() error { 185 | return s.httpServer.Wait() 186 | } 187 | 188 | // Close forcibly closes the http server 189 | func (s *Server) Close() error { 190 | return s.httpServer.Close() 191 | } 192 | 193 | // GetFingerprint is used to access the server fingerprint 194 | func (s *Server) GetFingerprint() string { 195 | return s.fingerprint 196 | } 197 | 198 | // authUser is responsible for validating the ssh user / password combination 199 | func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { 200 | // check if user authentication is enabled and if not, allow all 201 | if s.users.Len() == 0 { 202 | return nil, nil 203 | } 204 | // check the user exists and has matching password 205 | n := c.User() 206 | user, found := s.users.Get(n) 207 | if !found || user.Pass != string(password) { 208 | s.Debugf("Login failed for user: %s", n) 209 | return nil, errors.New("Invalid authentication for username: %s") 210 | } 211 | // insert the user session map 212 | // TODO this should probably have a lock on it given the map isn't thread-safe 213 | s.sessions.Set(string(c.SessionID()), user) 214 | return nil, nil 215 | } 216 | 217 | // AddUser adds a new user into the server user index 218 | func (s *Server) AddUser(user, pass string, addrs ...string) error { 219 | authorizedAddrs := []*regexp.Regexp{} 220 | for _, addr := range addrs { 221 | authorizedAddr, err := regexp.Compile(addr) 222 | if err != nil { 223 | return err 224 | } 225 | authorizedAddrs = append(authorizedAddrs, authorizedAddr) 226 | } 227 | s.users.AddUser(&settings.User{ 228 | Name: user, 229 | Pass: pass, 230 | Addrs: authorizedAddrs, 231 | }) 232 | return nil 233 | } 234 | 235 | // DeleteUser removes a user from the server user index 236 | func (s *Server) DeleteUser(user string) { 237 | s.users.Del(user) 238 | } 239 | 240 | // ResetUsers in the server user index. 241 | // Use nil to remove all. 242 | func (s *Server) ResetUsers(users []*settings.User) { 243 | s.users.Reset(users) 244 | } 245 | -------------------------------------------------------------------------------- /server/server_handler.go: -------------------------------------------------------------------------------- 1 | package chserver 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "sync/atomic" 7 | "time" 8 | 9 | chshare "github.com/jpillora/chisel/share" 10 | "github.com/jpillora/chisel/share/cnet" 11 | "github.com/jpillora/chisel/share/settings" 12 | "github.com/jpillora/chisel/share/tunnel" 13 | "golang.org/x/crypto/ssh" 14 | "golang.org/x/sync/errgroup" 15 | ) 16 | 17 | // handleClientHandler is the main http websocket handler for the chisel server 18 | func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) { 19 | //websockets upgrade AND has chisel prefix 20 | upgrade := strings.ToLower(r.Header.Get("Upgrade")) 21 | protocol := r.Header.Get("Sec-WebSocket-Protocol") 22 | if upgrade == "websocket" { 23 | if protocol == chshare.ProtocolVersion { 24 | s.handleWebsocket(w, r) 25 | return 26 | } 27 | //print into server logs and silently fall-through 28 | s.Infof("ignored client connection using protocol '%s', expected '%s'", 29 | protocol, chshare.ProtocolVersion) 30 | } 31 | //proxy target was provided 32 | if s.reverseProxy != nil { 33 | s.reverseProxy.ServeHTTP(w, r) 34 | return 35 | } 36 | //no proxy defined, provide access to health/version checks 37 | switch r.URL.Path { 38 | case "/health": 39 | w.Write([]byte("OK\n")) 40 | return 41 | case "/version": 42 | w.Write([]byte(chshare.BuildVersion)) 43 | return 44 | } 45 | //missing :O 46 | w.WriteHeader(404) 47 | w.Write([]byte("Not found")) 48 | } 49 | 50 | // handleWebsocket is responsible for handling the websocket connection 51 | func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) { 52 | id := atomic.AddInt32(&s.sessCount, 1) 53 | l := s.Fork("session#%d", id) 54 | wsConn, err := upgrader.Upgrade(w, req, nil) 55 | if err != nil { 56 | l.Debugf("Failed to upgrade (%s)", err) 57 | return 58 | } 59 | conn := cnet.NewWebSocketConn(wsConn) 60 | // perform SSH handshake on net.Conn 61 | l.Debugf("Handshaking with %s...", req.RemoteAddr) 62 | sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig) 63 | if err != nil { 64 | s.Debugf("Failed to handshake (%s)", err) 65 | return 66 | } 67 | // pull the users from the session map 68 | var user *settings.User 69 | if s.users.Len() > 0 { 70 | sid := string(sshConn.SessionID()) 71 | u, ok := s.sessions.Get(sid) 72 | if !ok { 73 | panic("bug in ssh auth handler") 74 | } 75 | user = u 76 | s.sessions.Del(sid) 77 | } 78 | // chisel server handshake (reverse of client handshake) 79 | // verify configuration 80 | l.Debugf("Verifying configuration") 81 | // wait for request, with timeout 82 | var r *ssh.Request 83 | select { 84 | case r = <-reqs: 85 | case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)): 86 | l.Debugf("Timeout waiting for configuration") 87 | sshConn.Close() 88 | return 89 | } 90 | failed := func(err error) { 91 | l.Debugf("Failed: %s", err) 92 | r.Reply(false, []byte(err.Error())) 93 | } 94 | if r.Type != "config" { 95 | failed(s.Errorf("expecting config request")) 96 | return 97 | } 98 | c, err := settings.DecodeConfig(r.Payload) 99 | if err != nil { 100 | failed(s.Errorf("invalid config")) 101 | return 102 | } 103 | //print if client and server versions dont match 104 | cv := strings.TrimPrefix(c.Version, "v") 105 | if cv == "" { 106 | cv = "" 107 | } 108 | sv := strings.TrimPrefix(chshare.BuildVersion, "v") 109 | if cv != sv { 110 | l.Infof("Client version (%s) differs from server version (%s)", cv, sv) 111 | } 112 | //validate remotes 113 | for _, r := range c.Remotes { 114 | //if user is provided, ensure they have 115 | //access to the desired remotes 116 | if user != nil { 117 | addr := r.UserAddr() 118 | if !user.HasAccess(addr) { 119 | failed(s.Errorf("access to '%s' denied", addr)) 120 | return 121 | } 122 | } 123 | //confirm reverse tunnels are allowed 124 | if r.Reverse && !s.config.Reverse { 125 | l.Debugf("Denied reverse port forwarding request, please enable --reverse") 126 | failed(s.Errorf("Reverse port forwaring not enabled on server")) 127 | return 128 | } 129 | //confirm reverse tunnel is available 130 | if r.Reverse && !r.CanListen() { 131 | failed(s.Errorf("Server cannot listen on %s", r.String())) 132 | return 133 | } 134 | } 135 | //successfuly validated config! 136 | r.Reply(true, nil) 137 | //tunnel per ssh connection 138 | tunnel := tunnel.New(tunnel.Config{ 139 | Logger: l, 140 | Inbound: s.config.Reverse, 141 | Outbound: true, //server always accepts outbound 142 | Socks: s.config.Socks5, 143 | KeepAlive: s.config.KeepAlive, 144 | }) 145 | //bind 146 | eg, ctx := errgroup.WithContext(req.Context()) 147 | eg.Go(func() error { 148 | //connected, handover ssh connection for tunnel to use, and block 149 | return tunnel.BindSSH(ctx, sshConn, reqs, chans) 150 | }) 151 | eg.Go(func() error { 152 | //connected, setup reversed-remotes? 153 | serverInbound := c.Remotes.Reversed(true) 154 | if len(serverInbound) == 0 { 155 | return nil 156 | } 157 | //block 158 | return tunnel.BindRemotes(ctx, serverInbound) 159 | }) 160 | err = eg.Wait() 161 | if err != nil && !strings.HasSuffix(err.Error(), "EOF") { 162 | l.Debugf("Closed connection (%s)", err) 163 | } else { 164 | l.Debugf("Closed connection") 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /server/server_listen.go: -------------------------------------------------------------------------------- 1 | package chserver 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 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 := os.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 := os.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 | } 151 | -------------------------------------------------------------------------------- /share/ccrypto/determ_rand.go: -------------------------------------------------------------------------------- 1 | package ccrypto 2 | 3 | // Deterministic crypto.Reader 4 | // overview: half the result is used as the output 5 | // [a|...] -> sha512(a) -> [b|output] -> sha512(b) 6 | 7 | import ( 8 | "crypto/sha512" 9 | "io" 10 | ) 11 | 12 | const DetermRandIter = 2048 13 | 14 | func NewDetermRand(seed []byte) io.Reader { 15 | var out []byte 16 | //strengthen seed 17 | var next = seed 18 | for i := 0; i < DetermRandIter; i++ { 19 | next, out = hash(next) 20 | } 21 | return &determRand{ 22 | next: next, 23 | out: out, 24 | } 25 | } 26 | 27 | type determRand struct { 28 | next, out []byte 29 | } 30 | 31 | func (d *determRand) Read(b []byte) (int, error) { 32 | n := 0 33 | l := len(b) 34 | for n < l { 35 | next, out := hash(d.next) 36 | n += copy(b[n:], out) 37 | d.next = next 38 | } 39 | return n, nil 40 | } 41 | 42 | func hash(input []byte) (next []byte, output []byte) { 43 | nextout := sha512.Sum512(input) 44 | return nextout[:sha512.Size/2], nextout[sha512.Size/2:] 45 | } 46 | -------------------------------------------------------------------------------- /share/ccrypto/generate_key_go119.go: -------------------------------------------------------------------------------- 1 | package ccrypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "io" 7 | "math/big" 8 | ) 9 | 10 | var one = new(big.Int).SetInt64(1) 11 | 12 | // This function is copied from ecdsa.GenerateKey() of Go 1.19 13 | func GenerateKeyGo119(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) { 14 | k, err := randFieldElement(c, rand) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | priv := new(ecdsa.PrivateKey) 20 | priv.PublicKey.Curve = c 21 | priv.D = k 22 | priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes()) 23 | return priv, nil 24 | } 25 | 26 | // This function is copied from Go 1.19 27 | func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) { 28 | params := c.Params() 29 | // Note that for P-521 this will actually be 63 bits more than the order, as 30 | // division rounds down, but the extra bit is inconsequential. 31 | b := make([]byte, params.N.BitLen()/8+8) 32 | _, err = io.ReadFull(rand, b) 33 | if err != nil { 34 | return 35 | } 36 | 37 | k = new(big.Int).SetBytes(b) 38 | n := new(big.Int).Sub(params.N, one) 39 | k.Mod(k, n) 40 | k.Add(k, one) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /share/ccrypto/keys.go: -------------------------------------------------------------------------------- 1 | package ccrypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | // GenerateKey generates a PEM key 13 | func GenerateKey(seed string) ([]byte, error) { 14 | return Seed2PEM(seed) 15 | } 16 | 17 | // GenerateKeyFile generates an ChiselKey 18 | func GenerateKeyFile(keyFilePath, seed string) error { 19 | chiselKey, err := seed2ChiselKey(seed) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if keyFilePath == "-" { 25 | fmt.Print(string(chiselKey)) 26 | return nil 27 | } 28 | return os.WriteFile(keyFilePath, chiselKey, 0600) 29 | } 30 | 31 | // FingerprintKey calculates the SHA256 hash of an SSH public key 32 | func FingerprintKey(k ssh.PublicKey) string { 33 | bytes := sha256.Sum256(k.Marshal()) 34 | return base64.StdEncoding.EncodeToString(bytes[:]) 35 | } 36 | -------------------------------------------------------------------------------- /share/ccrypto/keys_helpers.go: -------------------------------------------------------------------------------- 1 | package ccrypto 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/pem" 10 | "strings" 11 | ) 12 | 13 | const ChiselKeyPrefix = "ck-" 14 | 15 | // Relations between entities: 16 | // 17 | // .............> PEM <........... 18 | // . ^ . 19 | // . | . 20 | // . | . 21 | // Seed -------> PrivateKey . 22 | // . ^ . 23 | // . | . 24 | // . V . 25 | // ..........> ChiselKey ......... 26 | 27 | func Seed2PEM(seed string) ([]byte, error) { 28 | privateKey, err := seed2PrivateKey(seed) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return privateKey2PEM(privateKey) 34 | } 35 | 36 | func seed2ChiselKey(seed string) ([]byte, error) { 37 | privateKey, err := seed2PrivateKey(seed) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return privateKey2ChiselKey(privateKey) 43 | } 44 | 45 | func seed2PrivateKey(seed string) (*ecdsa.PrivateKey, error) { 46 | if seed == "" { 47 | return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 48 | } else { 49 | return GenerateKeyGo119(elliptic.P256(), NewDetermRand([]byte(seed))) 50 | } 51 | } 52 | 53 | func privateKey2ChiselKey(privateKey *ecdsa.PrivateKey) ([]byte, error) { 54 | b, err := x509.MarshalECPrivateKey(privateKey) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | encodedPrivateKey := make([]byte, base64.RawStdEncoding.EncodedLen(len(b))) 60 | base64.RawStdEncoding.Encode(encodedPrivateKey, b) 61 | 62 | return append([]byte(ChiselKeyPrefix), encodedPrivateKey...), nil 63 | } 64 | 65 | func privateKey2PEM(privateKey *ecdsa.PrivateKey) ([]byte, error) { 66 | b, err := x509.MarshalECPrivateKey(privateKey) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil 72 | } 73 | 74 | func chiselKey2PrivateKey(chiselKey []byte) (*ecdsa.PrivateKey, error) { 75 | rawChiselKey := chiselKey[len(ChiselKeyPrefix):] 76 | 77 | decodedPrivateKey := make([]byte, base64.RawStdEncoding.DecodedLen(len(rawChiselKey))) 78 | _, err := base64.RawStdEncoding.Decode(decodedPrivateKey, rawChiselKey) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return x509.ParseECPrivateKey(decodedPrivateKey) 84 | } 85 | 86 | func ChiselKey2PEM(chiselKey []byte) ([]byte, error) { 87 | privateKey, err := chiselKey2PrivateKey(chiselKey) 88 | if err == nil { 89 | return privateKey2PEM(privateKey) 90 | } 91 | 92 | return nil, err 93 | } 94 | 95 | func IsChiselKey(chiselKey []byte) bool { 96 | return strings.HasPrefix(string(chiselKey), ChiselKeyPrefix) 97 | } 98 | -------------------------------------------------------------------------------- /share/cio/logger.go: -------------------------------------------------------------------------------- 1 | package cio 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | //Logger is pkg/log Logger with prefixing and 2 log levels 10 | type Logger struct { 11 | Info, Debug bool 12 | //internal 13 | prefix string 14 | logger *log.Logger 15 | info, debug *bool 16 | } 17 | 18 | func NewLogger(prefix string) *Logger { 19 | return NewLoggerFlag(prefix, log.Ldate|log.Ltime) 20 | } 21 | 22 | func NewLoggerFlag(prefix string, flag int) *Logger { 23 | l := &Logger{ 24 | prefix: prefix, 25 | logger: log.New(os.Stderr, "", flag), 26 | Info: false, 27 | Debug: false, 28 | } 29 | return l 30 | } 31 | 32 | func (l *Logger) Infof(f string, args ...interface{}) { 33 | if l.IsInfo() { 34 | l.logger.Printf(l.prefix+": "+f, args...) 35 | } 36 | } 37 | 38 | func (l *Logger) Debugf(f string, args ...interface{}) { 39 | if l.IsDebug() { 40 | l.logger.Printf(l.prefix+": "+f, args...) 41 | } 42 | } 43 | 44 | func (l *Logger) Errorf(f string, args ...interface{}) error { 45 | return fmt.Errorf(l.prefix+": "+f, args...) 46 | } 47 | 48 | func (l *Logger) Fork(prefix string, args ...interface{}) *Logger { 49 | //slip the parent prefix at the front 50 | args = append([]interface{}{l.prefix}, args...) 51 | ll := NewLogger(fmt.Sprintf("%s: "+prefix, args...)) 52 | //store link to parent settings too 53 | ll.Info = l.Info 54 | if l.info != nil { 55 | ll.info = l.info 56 | } else { 57 | ll.info = &l.Info 58 | } 59 | ll.Debug = l.Debug 60 | if l.debug != nil { 61 | ll.debug = l.debug 62 | } else { 63 | ll.debug = &l.Debug 64 | } 65 | return ll 66 | } 67 | 68 | func (l *Logger) Prefix() string { 69 | return l.prefix 70 | } 71 | 72 | func (l *Logger) IsInfo() bool { 73 | return l.Info || (l.info != nil && *l.info) 74 | } 75 | 76 | func (l *Logger) IsDebug() bool { 77 | return l.Debug || (l.debug != nil && *l.debug) 78 | } 79 | -------------------------------------------------------------------------------- /share/cio/pipe.go: -------------------------------------------------------------------------------- 1 | package cio 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | func Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) { 10 | var sent, received int64 11 | var wg sync.WaitGroup 12 | var o sync.Once 13 | close := func() { 14 | src.Close() 15 | dst.Close() 16 | } 17 | wg.Add(2) 18 | go func() { 19 | received, _ = io.Copy(src, dst) 20 | o.Do(close) 21 | wg.Done() 22 | }() 23 | go func() { 24 | sent, _ = io.Copy(dst, src) 25 | o.Do(close) 26 | wg.Done() 27 | }() 28 | wg.Wait() 29 | return sent, received 30 | } 31 | 32 | const vis = false 33 | 34 | type pipeVisPrinter struct { 35 | name string 36 | } 37 | 38 | func (p pipeVisPrinter) Write(b []byte) (int, error) { 39 | log.Printf(">>> %s: %x", p.name, b) 40 | return len(b), nil 41 | } 42 | 43 | func pipeVis(name string, r io.Reader) io.Reader { 44 | if vis { 45 | return io.TeeReader(r, pipeVisPrinter{name}) 46 | } 47 | return r 48 | } 49 | -------------------------------------------------------------------------------- /share/cio/stdio.go: -------------------------------------------------------------------------------- 1 | package cio 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | //Stdio as a ReadWriteCloser 9 | var Stdio = &struct { 10 | io.ReadCloser 11 | io.Writer 12 | }{ 13 | io.NopCloser(os.Stdin), 14 | os.Stdout, 15 | } 16 | -------------------------------------------------------------------------------- /share/cnet/conn_rwc.go: -------------------------------------------------------------------------------- 1 | package cnet 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type rwcConn struct { 10 | io.ReadWriteCloser 11 | buff []byte 12 | } 13 | 14 | //NewRWCConn converts a RWC into a net.Conn 15 | func NewRWCConn(rwc io.ReadWriteCloser) net.Conn { 16 | c := rwcConn{ 17 | ReadWriteCloser: rwc, 18 | } 19 | return &c 20 | } 21 | 22 | func (c *rwcConn) LocalAddr() net.Addr { 23 | return c 24 | } 25 | 26 | func (c *rwcConn) RemoteAddr() net.Addr { 27 | return c 28 | } 29 | 30 | func (c *rwcConn) Network() string { 31 | return "tcp" 32 | } 33 | 34 | func (c *rwcConn) String() string { 35 | return "" 36 | } 37 | 38 | func (c *rwcConn) SetDeadline(t time.Time) error { 39 | return nil //no-op 40 | } 41 | 42 | func (c *rwcConn) SetReadDeadline(t time.Time) error { 43 | return nil //no-op 44 | } 45 | 46 | func (c *rwcConn) SetWriteDeadline(t time.Time) error { 47 | return nil //no-op 48 | } 49 | -------------------------------------------------------------------------------- /share/cnet/conn_ws.go: -------------------------------------------------------------------------------- 1 | package cnet 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | type wsConn struct { 11 | *websocket.Conn 12 | buff []byte 13 | } 14 | 15 | //NewWebSocketConn converts a websocket.Conn into a net.Conn 16 | func NewWebSocketConn(websocketConn *websocket.Conn) net.Conn { 17 | c := wsConn{ 18 | Conn: websocketConn, 19 | } 20 | return &c 21 | } 22 | 23 | //Read is not threadsafe though thats okay since there 24 | //should never be more than one reader 25 | func (c *wsConn) Read(dst []byte) (int, error) { 26 | ldst := len(dst) 27 | //use buffer or read new message 28 | var src []byte 29 | if len(c.buff) > 0 { 30 | src = c.buff 31 | c.buff = nil 32 | } else if _, msg, err := c.Conn.ReadMessage(); err == nil { 33 | src = msg 34 | } else { 35 | return 0, err 36 | } 37 | //copy src->dest 38 | var n int 39 | if len(src) > ldst { 40 | //copy as much as possible of src into dst 41 | n = copy(dst, src[:ldst]) 42 | //copy remainder into buffer 43 | r := src[ldst:] 44 | lr := len(r) 45 | c.buff = make([]byte, lr) 46 | copy(c.buff, r) 47 | } else { 48 | //copy all of src into dst 49 | n = copy(dst, src) 50 | } 51 | //return bytes copied 52 | return n, nil 53 | } 54 | 55 | func (c *wsConn) Write(b []byte) (int, error) { 56 | if err := c.Conn.WriteMessage(websocket.BinaryMessage, b); err != nil { 57 | return 0, err 58 | } 59 | n := len(b) 60 | return n, nil 61 | } 62 | 63 | func (c *wsConn) SetDeadline(t time.Time) error { 64 | if err := c.Conn.SetReadDeadline(t); err != nil { 65 | return err 66 | } 67 | return c.Conn.SetWriteDeadline(t) 68 | } 69 | -------------------------------------------------------------------------------- /share/cnet/connstats.go: -------------------------------------------------------------------------------- 1 | package cnet 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | ) 7 | 8 | //ConnCount is a connection counter 9 | type ConnCount struct { 10 | count int32 11 | open int32 12 | } 13 | 14 | func (c *ConnCount) New() int32 { 15 | return atomic.AddInt32(&c.count, 1) 16 | } 17 | 18 | func (c *ConnCount) Open() { 19 | atomic.AddInt32(&c.open, 1) 20 | } 21 | 22 | func (c *ConnCount) Close() { 23 | atomic.AddInt32(&c.open, -1) 24 | } 25 | 26 | func (c *ConnCount) String() string { 27 | return fmt.Sprintf("[%d/%d]", atomic.LoadInt32(&c.open), atomic.LoadInt32(&c.count)) 28 | } 29 | -------------------------------------------------------------------------------- /share/cnet/http_server.go: -------------------------------------------------------------------------------- 1 | package cnet 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "sync" 9 | 10 | "golang.org/x/sync/errgroup" 11 | ) 12 | 13 | //HTTPServer extends net/http Server and 14 | //adds graceful shutdowns 15 | type HTTPServer struct { 16 | *http.Server 17 | waiterMux sync.Mutex 18 | waiter *errgroup.Group 19 | listenErr error 20 | } 21 | 22 | //NewHTTPServer creates a new HTTPServer 23 | func NewHTTPServer() *HTTPServer { 24 | return &HTTPServer{ 25 | Server: &http.Server{}, 26 | } 27 | 28 | } 29 | 30 | func (h *HTTPServer) GoListenAndServe(addr string, handler http.Handler) error { 31 | return h.GoListenAndServeContext(context.Background(), addr, handler) 32 | } 33 | 34 | func (h *HTTPServer) GoListenAndServeContext(ctx context.Context, addr string, handler http.Handler) error { 35 | if ctx == nil { 36 | return errors.New("ctx must be set") 37 | } 38 | l, err := net.Listen("tcp", addr) 39 | if err != nil { 40 | return err 41 | } 42 | return h.GoServe(ctx, l, handler) 43 | } 44 | 45 | func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.Handler) error { 46 | if ctx == nil { 47 | return errors.New("ctx must be set") 48 | } 49 | h.waiterMux.Lock() 50 | defer h.waiterMux.Unlock() 51 | h.Handler = handler 52 | h.waiter, ctx = errgroup.WithContext(ctx) 53 | h.waiter.Go(func() error { 54 | return h.Serve(l) 55 | }) 56 | go func() { 57 | <-ctx.Done() 58 | h.Close() 59 | }() 60 | return nil 61 | } 62 | 63 | func (h *HTTPServer) Close() error { 64 | h.waiterMux.Lock() 65 | defer h.waiterMux.Unlock() 66 | if h.waiter == nil { 67 | return errors.New("not started yet") 68 | } 69 | return h.Server.Close() 70 | } 71 | 72 | func (h *HTTPServer) Wait() error { 73 | h.waiterMux.Lock() 74 | unset := h.waiter == nil 75 | h.waiterMux.Unlock() 76 | if unset { 77 | return errors.New("not started yet") 78 | } 79 | h.waiterMux.Lock() 80 | wait := h.waiter.Wait 81 | h.waiterMux.Unlock() 82 | err := wait() 83 | if err == http.ErrServerClosed { 84 | err = nil //success 85 | } 86 | return err 87 | } 88 | -------------------------------------------------------------------------------- /share/cnet/meter.go: -------------------------------------------------------------------------------- 1 | package cnet 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/jpillora/chisel/share/cio" 10 | "github.com/jpillora/sizestr" 11 | ) 12 | 13 | //NewMeter to measure readers/writers 14 | func NewMeter(l *cio.Logger) *Meter { 15 | return &Meter{l: l} 16 | } 17 | 18 | //Meter can be inserted in the path or 19 | //of a reader or writer to measure the 20 | //throughput 21 | type Meter struct { 22 | //meter state 23 | sent, recv int64 24 | //print state 25 | l *cio.Logger 26 | printing uint32 27 | last int64 28 | lsent, lrecv int64 29 | } 30 | 31 | func (m *Meter) print() { 32 | //move out of the read/write path asap 33 | if atomic.CompareAndSwapUint32(&m.printing, 0, 1) { 34 | go m.goprint() 35 | } 36 | } 37 | 38 | func (m *Meter) goprint() { 39 | time.Sleep(time.Second) 40 | //snapshot 41 | s := atomic.LoadInt64(&m.sent) 42 | r := atomic.LoadInt64(&m.recv) 43 | //compute speed 44 | curr := time.Now().UnixNano() 45 | last := atomic.LoadInt64(&m.last) 46 | dt := time.Duration(curr-last) * time.Nanosecond 47 | ls := atomic.LoadInt64(&m.lsent) 48 | lr := atomic.LoadInt64(&m.lrecv) 49 | //DEBUG 50 | // m.l.Infof("%s = %d(%d-%d), %d(%d-%d)", dt, s-ls, s, ls, r-lr, r, lr) 51 | //scale to per second V=D/T 52 | sps := int64(float64(s-ls) / float64(dt) * float64(time.Second)) 53 | rps := int64(float64(r-lr) / float64(dt) * float64(time.Second)) 54 | if last > 0 && (sps != 0 || rps != 0) { 55 | m.l.Debugf("write %s/s read %s/s", sizestr.ToString(sps), sizestr.ToString(rps)) 56 | } 57 | //record last printed 58 | atomic.StoreInt64(&m.lsent, s) 59 | atomic.StoreInt64(&m.lrecv, r) 60 | //done 61 | atomic.StoreInt64(&m.last, curr) 62 | atomic.StoreUint32(&m.printing, 0) 63 | } 64 | 65 | //TeeReader inserts Meter into the read path 66 | //if the linked logger is in debug mode, 67 | //otherwise this is a no-op 68 | func (m *Meter) TeeReader(r io.Reader) io.Reader { 69 | if m.l.IsDebug() { 70 | return &meterReader{m, r} 71 | } 72 | return r 73 | } 74 | 75 | type meterReader struct { 76 | *Meter 77 | inner io.Reader 78 | } 79 | 80 | func (m *meterReader) Read(p []byte) (n int, err error) { 81 | n, err = m.inner.Read(p) 82 | atomic.AddInt64(&m.recv, int64(n)) 83 | m.Meter.print() 84 | return 85 | } 86 | 87 | //TeeWriter inserts Meter into the write path 88 | //if the linked logger is in debug mode, 89 | //otherwise this is a no-op 90 | func (m *Meter) TeeWriter(w io.Writer) io.Writer { 91 | if m.l.IsDebug() { 92 | return &meterWriter{m, w} 93 | } 94 | return w 95 | } 96 | 97 | type meterWriter struct { 98 | *Meter 99 | inner io.Writer 100 | } 101 | 102 | func (m *meterWriter) Write(p []byte) (n int, err error) { 103 | n, err = m.inner.Write(p) 104 | atomic.AddInt64(&m.sent, int64(n)) 105 | m.Meter.print() 106 | return 107 | } 108 | 109 | //MeterConn inserts Meter into the connection path 110 | //if the linked logger is in debug mode, 111 | //otherwise this is a no-op 112 | func MeterConn(l *cio.Logger, conn net.Conn) net.Conn { 113 | m := NewMeter(l) 114 | return &meterConn{ 115 | mread: m.TeeReader(conn), 116 | mwrite: m.TeeWriter(conn), 117 | Conn: conn, 118 | } 119 | } 120 | 121 | type meterConn struct { 122 | mread io.Reader 123 | mwrite io.Writer 124 | net.Conn 125 | } 126 | 127 | func (m *meterConn) Read(p []byte) (n int, err error) { 128 | return m.mread.Read(p) 129 | } 130 | 131 | func (m *meterConn) Write(p []byte) (n int, err error) { 132 | return m.mwrite.Write(p) 133 | } 134 | 135 | //MeterRWC inserts Meter into the RWC path 136 | //if the linked logger is in debug mode, 137 | //otherwise this is a no-op 138 | func MeterRWC(l *cio.Logger, rwc io.ReadWriteCloser) io.ReadWriteCloser { 139 | m := NewMeter(l) 140 | return &struct { 141 | io.Reader 142 | io.Writer 143 | io.Closer 144 | }{ 145 | Reader: m.TeeReader(rwc), 146 | Writer: m.TeeWriter(rwc), 147 | Closer: rwc, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /share/compat.go: -------------------------------------------------------------------------------- 1 | package chshare 2 | 3 | //this file exists to maintain backwards compatibility 4 | 5 | import ( 6 | "github.com/jpillora/chisel/share/ccrypto" 7 | "github.com/jpillora/chisel/share/cio" 8 | "github.com/jpillora/chisel/share/cnet" 9 | "github.com/jpillora/chisel/share/cos" 10 | "github.com/jpillora/chisel/share/settings" 11 | "github.com/jpillora/chisel/share/tunnel" 12 | ) 13 | 14 | const ( 15 | DetermRandIter = ccrypto.DetermRandIter 16 | ) 17 | 18 | type ( 19 | Config = settings.Config 20 | Remote = settings.Remote 21 | Remotes = settings.Remotes 22 | User = settings.User 23 | Users = settings.Users 24 | UserIndex = settings.UserIndex 25 | HTTPServer = cnet.HTTPServer 26 | ConnStats = cnet.ConnCount 27 | Logger = cio.Logger 28 | TCPProxy = tunnel.Proxy 29 | ) 30 | 31 | var ( 32 | NewDetermRand = ccrypto.NewDetermRand 33 | GenerateKey = ccrypto.GenerateKey 34 | FingerprintKey = ccrypto.FingerprintKey 35 | Pipe = cio.Pipe 36 | NewLoggerFlag = cio.NewLoggerFlag 37 | NewLogger = cio.NewLogger 38 | Stdio = cio.Stdio 39 | DecodeConfig = settings.DecodeConfig 40 | DecodeRemote = settings.DecodeRemote 41 | NewUsers = settings.NewUsers 42 | NewUserIndex = settings.NewUserIndex 43 | UserAllowAll = settings.UserAllowAll 44 | ParseAuth = settings.ParseAuth 45 | NewRWCConn = cnet.NewRWCConn 46 | NewWebSocketConn = cnet.NewWebSocketConn 47 | NewHTTPServer = cnet.NewHTTPServer 48 | GoStats = cos.GoStats 49 | SleepSignal = cos.SleepSignal 50 | NewTCPProxy = tunnel.NewProxy 51 | ) 52 | 53 | //EncodeConfig old version 54 | func EncodeConfig(c *settings.Config) ([]byte, error) { 55 | return settings.EncodeConfig(*c), nil 56 | } 57 | -------------------------------------------------------------------------------- /share/cos/common.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "time" 8 | ) 9 | 10 | //InterruptContext returns a context which is 11 | //cancelled on OS Interrupt 12 | func InterruptContext() context.Context { 13 | ctx, cancel := context.WithCancel(context.Background()) 14 | go func() { 15 | sig := make(chan os.Signal, 1) 16 | signal.Notify(sig, os.Interrupt) //windows compatible? 17 | <-sig 18 | signal.Stop(sig) 19 | cancel() 20 | }() 21 | return ctx 22 | } 23 | 24 | //SleepSignal sleeps for the given duration, 25 | //or until a SIGHUP is received 26 | func SleepSignal(d time.Duration) { 27 | <-AfterSignal(d) 28 | } 29 | -------------------------------------------------------------------------------- /share/cos/pprof.go: -------------------------------------------------------------------------------- 1 | // +build pprof 2 | 3 | package cos 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | _ "net/http/pprof" //import http profiler api 9 | ) 10 | 11 | func init() { 12 | go func() { 13 | log.Fatal(http.ListenAndServe("localhost:6060", nil)) 14 | }() 15 | log.Printf("[pprof] listening on 6060") 16 | } 17 | -------------------------------------------------------------------------------- /share/cos/signal.go: -------------------------------------------------------------------------------- 1 | //+build !windows 2 | 3 | package cos 4 | 5 | import ( 6 | "log" 7 | "os" 8 | "os/signal" 9 | "runtime" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/jpillora/sizestr" 14 | ) 15 | 16 | //GoStats prints statistics to 17 | //stdout on SIGUSR2 (posix-only) 18 | func GoStats() { 19 | //silence complaints from windows 20 | const SIGUSR2 = syscall.Signal(0x1f) 21 | time.Sleep(time.Second) 22 | c := make(chan os.Signal, 1) 23 | signal.Notify(c, SIGUSR2) 24 | for range c { 25 | memStats := runtime.MemStats{} 26 | runtime.ReadMemStats(&memStats) 27 | log.Printf("recieved SIGUSR2, go-routines: %d, go-memory-usage: %s", 28 | runtime.NumGoroutine(), 29 | sizestr.ToString(int64(memStats.Alloc))) 30 | } 31 | } 32 | 33 | //AfterSignal returns a channel which will be closed 34 | //after the given duration or until a SIGHUP is received 35 | func AfterSignal(d time.Duration) <-chan struct{} { 36 | ch := make(chan struct{}) 37 | go func() { 38 | sig := make(chan os.Signal, 1) 39 | signal.Notify(sig, syscall.SIGHUP) 40 | select { 41 | case <-time.After(d): 42 | case <-sig: 43 | } 44 | signal.Stop(sig) 45 | close(ch) 46 | }() 47 | return ch 48 | } 49 | -------------------------------------------------------------------------------- /share/cos/signal_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package cos 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | func GoStats() { 10 | //noop 11 | } 12 | 13 | func AfterSignal(d time.Duration) <-chan struct{} { 14 | ch := make(chan struct{}) 15 | go func() { 16 | <-time.After(d) 17 | close(ch) 18 | }() 19 | return ch 20 | } 21 | -------------------------------------------------------------------------------- /share/settings/config.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Config struct { 9 | Version string 10 | Remotes 11 | } 12 | 13 | func DecodeConfig(b []byte) (*Config, error) { 14 | c := &Config{} 15 | err := json.Unmarshal(b, c) 16 | if err != nil { 17 | return nil, fmt.Errorf("Invalid JSON config") 18 | } 19 | return c, nil 20 | } 21 | 22 | func EncodeConfig(c Config) []byte { 23 | //Config doesn't have types that can fail to marshal 24 | b, _ := json.Marshal(c) 25 | return b 26 | } 27 | -------------------------------------------------------------------------------- /share/settings/env.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Env returns a chisel environment variable 11 | func Env(name string) string { 12 | return os.Getenv("CHISEL_" + name) 13 | } 14 | 15 | // EnvInt returns an integer using an environment variable, with a default fallback 16 | func EnvInt(name string, def int) int { 17 | if n, err := strconv.Atoi(Env(name)); err == nil { 18 | return n 19 | } 20 | return def 21 | } 22 | 23 | // EnvDuration returns a duration using an environment variable, with a default fallback 24 | func EnvDuration(name string, def time.Duration) time.Duration { 25 | if n, err := time.ParseDuration(Env(name)); err == nil { 26 | return n 27 | } 28 | return def 29 | } 30 | 31 | // EnvBool returns a boolean using an environment variable 32 | func EnvBool(name string) bool { 33 | v := Env(name) 34 | return v == "1" || strings.ToLower(v) == "true" 35 | } 36 | -------------------------------------------------------------------------------- /share/settings/remote.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/url" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // short-hand conversions (see remote_test) 13 | // 3000 -> 14 | // local 127.0.0.1:3000 15 | // remote 127.0.0.1:3000 16 | // foobar.com:3000 -> 17 | // local 127.0.0.1:3000 18 | // remote foobar.com:3000 19 | // 3000:google.com:80 -> 20 | // local 127.0.0.1:3000 21 | // remote google.com:80 22 | // 192.168.0.1:3000:google.com:80 -> 23 | // local 192.168.0.1:3000 24 | // remote google.com:80 25 | // 127.0.0.1:1080:socks 26 | // local 127.0.0.1:1080 27 | // remote socks 28 | // stdio:example.com:22 29 | // local stdio 30 | // remote example.com:22 31 | // 1.1.1.1:53/udp 32 | // local 127.0.0.1:53/udp 33 | // remote 1.1.1.1:53/udp 34 | 35 | type Remote struct { 36 | LocalHost, LocalPort, LocalProto string 37 | RemoteHost, RemotePort, RemoteProto string 38 | Socks, Reverse, Stdio bool 39 | } 40 | 41 | const revPrefix = "R:" 42 | 43 | func DecodeRemote(s string) (*Remote, error) { 44 | reverse := false 45 | if strings.HasPrefix(s, revPrefix) { 46 | s = strings.TrimPrefix(s, revPrefix) 47 | reverse = true 48 | } 49 | parts := regexp.MustCompile(`(\[[^\[\]]+\]|[^\[\]:]+):?`).FindAllStringSubmatch(s, -1) 50 | if len(parts) <= 0 || len(parts) >= 5 { 51 | return nil, errors.New("Invalid remote") 52 | } 53 | r := &Remote{Reverse: reverse} 54 | //parse from back to front, to set 'remote' fields first, 55 | //then to set 'local' fields second (allows the 'remote' side 56 | //to provide the defaults) 57 | for i := len(parts) - 1; i >= 0; i-- { 58 | p := parts[i][1] 59 | //remote portion is socks? 60 | if i == len(parts)-1 && p == "socks" { 61 | r.Socks = true 62 | continue 63 | } 64 | //local portion is stdio? 65 | if i == 0 && p == "stdio" { 66 | r.Stdio = true 67 | continue 68 | } 69 | p, proto := L4Proto(p) 70 | if proto != "" { 71 | if r.RemotePort == "" { 72 | r.RemoteProto = proto 73 | } else if r.LocalProto == "" { 74 | r.LocalProto = proto 75 | } 76 | } 77 | if isPort(p) { 78 | if !r.Socks && r.RemotePort == "" { 79 | r.RemotePort = p 80 | } 81 | r.LocalPort = p 82 | continue 83 | } 84 | if !r.Socks && (r.RemotePort == "" && r.LocalPort == "") { 85 | return nil, errors.New("Missing ports") 86 | } 87 | if !isHost(p) { 88 | return nil, errors.New("Invalid host") 89 | } 90 | if !r.Socks && r.RemoteHost == "" { 91 | r.RemoteHost = p 92 | } else { 93 | r.LocalHost = p 94 | } 95 | } 96 | //remote string parsed, apply defaults... 97 | if r.Socks { 98 | //socks defaults 99 | if r.LocalHost == "" { 100 | r.LocalHost = "127.0.0.1" 101 | } 102 | if r.LocalPort == "" { 103 | r.LocalPort = "1080" 104 | } 105 | } else { 106 | //non-socks defaults 107 | if r.LocalHost == "" { 108 | r.LocalHost = "0.0.0.0" 109 | } 110 | if r.RemoteHost == "" { 111 | r.RemoteHost = "127.0.0.1" 112 | } 113 | } 114 | if r.RemoteProto == "" { 115 | r.RemoteProto = "tcp" 116 | } 117 | if r.LocalProto == "" { 118 | r.LocalProto = r.RemoteProto 119 | } 120 | if r.LocalProto != r.RemoteProto { 121 | //TODO support cross protocol 122 | //tcp <-> udp, is faily straight forward 123 | //udp <-> tcp, is trickier since udp is stateless and tcp is not 124 | return nil, errors.New("cross-protocol remotes are not supported yet") 125 | } 126 | if r.Socks && r.RemoteProto != "tcp" { 127 | return nil, errors.New("only TCP SOCKS is supported") 128 | } 129 | if r.Stdio && r.Reverse { 130 | return nil, errors.New("stdio cannot be reversed") 131 | } 132 | return r, nil 133 | } 134 | 135 | func isPort(s string) bool { 136 | n, err := strconv.Atoi(s) 137 | if err != nil { 138 | return false 139 | } 140 | if n <= 0 || n > 65535 { 141 | return false 142 | } 143 | return true 144 | } 145 | 146 | func isHost(s string) bool { 147 | _, err := url.Parse("//" + s) 148 | if err != nil { 149 | return false 150 | } 151 | return true 152 | } 153 | 154 | var l4Proto = regexp.MustCompile(`(?i)\/(tcp|udp)$`) 155 | 156 | //L4Proto extacts the layer-4 protocol from the given string 157 | func L4Proto(s string) (head, proto string) { 158 | if l4Proto.MatchString(s) { 159 | l := len(s) 160 | return strings.ToLower(s[:l-4]), s[l-3:] 161 | } 162 | return s, "" 163 | } 164 | 165 | //implement Stringer 166 | func (r Remote) String() string { 167 | sb := strings.Builder{} 168 | if r.Reverse { 169 | sb.WriteString(revPrefix) 170 | } 171 | sb.WriteString(strings.TrimPrefix(r.Local(), "0.0.0.0:")) 172 | sb.WriteString("=>") 173 | sb.WriteString(strings.TrimPrefix(r.Remote(), "127.0.0.1:")) 174 | if r.RemoteProto == "udp" { 175 | sb.WriteString("/udp") 176 | } 177 | return sb.String() 178 | } 179 | 180 | //Encode remote to a string 181 | func (r Remote) Encode() string { 182 | if r.LocalPort == "" { 183 | r.LocalPort = r.RemotePort 184 | } 185 | local := r.Local() 186 | remote := r.Remote() 187 | if r.RemoteProto == "udp" { 188 | remote += "/udp" 189 | } 190 | if r.Reverse { 191 | return "R:" + local + ":" + remote 192 | } 193 | return local + ":" + remote 194 | } 195 | 196 | //Local is the decodable local portion 197 | func (r Remote) Local() string { 198 | if r.Stdio { 199 | return "stdio" 200 | } 201 | if r.LocalHost == "" { 202 | r.LocalHost = "0.0.0.0" 203 | } 204 | return r.LocalHost + ":" + r.LocalPort 205 | } 206 | 207 | //Remote is the decodable remote portion 208 | func (r Remote) Remote() string { 209 | if r.Socks { 210 | return "socks" 211 | } 212 | if r.RemoteHost == "" { 213 | r.RemoteHost = "127.0.0.1" 214 | } 215 | return r.RemoteHost + ":" + r.RemotePort 216 | } 217 | 218 | //UserAddr is checked when checking if a 219 | //user has access to a given remote 220 | func (r Remote) UserAddr() string { 221 | if r.Reverse { 222 | return "R:" + r.LocalHost + ":" + r.LocalPort 223 | } 224 | return r.RemoteHost + ":" + r.RemotePort 225 | } 226 | 227 | //CanListen checks if the port can be listened on 228 | func (r Remote) CanListen() bool { 229 | //valid protocols 230 | switch r.LocalProto { 231 | case "tcp": 232 | conn, err := net.Listen("tcp", r.Local()) 233 | if err == nil { 234 | conn.Close() 235 | return true 236 | } 237 | return false 238 | case "udp": 239 | addr, err := net.ResolveUDPAddr("udp", r.Local()) 240 | if err != nil { 241 | return false 242 | } 243 | conn, err := net.ListenUDP(r.LocalProto, addr) 244 | if err == nil { 245 | conn.Close() 246 | return true 247 | } 248 | return false 249 | } 250 | //invalid 251 | return false 252 | } 253 | 254 | type Remotes []*Remote 255 | 256 | //Filter out forward reversed/non-reversed remotes 257 | func (rs Remotes) Reversed(reverse bool) Remotes { 258 | subset := Remotes{} 259 | for _, r := range rs { 260 | match := r.Reverse == reverse 261 | if match { 262 | subset = append(subset, r) 263 | } 264 | } 265 | return subset 266 | } 267 | 268 | //Encode back into strings 269 | func (rs Remotes) Encode() []string { 270 | s := make([]string, len(rs)) 271 | for i, r := range rs { 272 | s[i] = r.Encode() 273 | } 274 | return s 275 | } 276 | -------------------------------------------------------------------------------- /share/settings/remote_test.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRemoteDecode(t *testing.T) { 9 | //test table 10 | for i, test := range []struct { 11 | Input string 12 | Output Remote 13 | Encoded string 14 | }{ 15 | { 16 | "3000", 17 | Remote{ 18 | LocalPort: "3000", 19 | RemoteHost: "127.0.0.1", 20 | RemotePort: "3000", 21 | }, 22 | "0.0.0.0:3000:127.0.0.1:3000", 23 | }, 24 | { 25 | "google.com:80", 26 | Remote{ 27 | LocalPort: "80", 28 | RemoteHost: "google.com", 29 | RemotePort: "80", 30 | }, 31 | "0.0.0.0:80:google.com:80", 32 | }, 33 | { 34 | "R:google.com:80", 35 | Remote{ 36 | LocalPort: "80", 37 | RemoteHost: "google.com", 38 | RemotePort: "80", 39 | Reverse: true, 40 | }, 41 | "R:0.0.0.0:80:google.com:80", 42 | }, 43 | { 44 | "示例網站.com:80", 45 | Remote{ 46 | LocalPort: "80", 47 | RemoteHost: "示例網站.com", 48 | RemotePort: "80", 49 | }, 50 | "0.0.0.0:80:示例網站.com:80", 51 | }, 52 | { 53 | "socks", 54 | Remote{ 55 | LocalHost: "127.0.0.1", 56 | LocalPort: "1080", 57 | Socks: true, 58 | }, 59 | "127.0.0.1:1080:socks", 60 | }, 61 | { 62 | "127.0.0.1:1081:socks", 63 | Remote{ 64 | LocalHost: "127.0.0.1", 65 | LocalPort: "1081", 66 | Socks: true, 67 | }, 68 | "127.0.0.1:1081:socks", 69 | }, 70 | { 71 | "1.1.1.1:53/udp", 72 | Remote{ 73 | LocalPort: "53", 74 | LocalProto: "udp", 75 | RemoteHost: "1.1.1.1", 76 | RemotePort: "53", 77 | RemoteProto: "udp", 78 | }, 79 | "0.0.0.0:53:1.1.1.1:53/udp", 80 | }, 81 | { 82 | "localhost:5353:1.1.1.1:53/udp", 83 | Remote{ 84 | LocalHost: "localhost", 85 | LocalPort: "5353", 86 | LocalProto: "udp", 87 | RemoteHost: "1.1.1.1", 88 | RemotePort: "53", 89 | RemoteProto: "udp", 90 | }, 91 | "localhost:5353:1.1.1.1:53/udp", 92 | }, 93 | { 94 | "[::1]:8080:google.com:80", 95 | Remote{ 96 | LocalHost: "[::1]", 97 | LocalPort: "8080", 98 | RemoteHost: "google.com", 99 | RemotePort: "80", 100 | }, 101 | "[::1]:8080:google.com:80", 102 | }, 103 | { 104 | "R:[::]:3000:[::1]:3000", 105 | Remote{ 106 | LocalHost: "[::]", 107 | LocalPort: "3000", 108 | RemoteHost: "[::1]", 109 | RemotePort: "3000", 110 | Reverse: true, 111 | }, 112 | "R:[::]:3000:[::1]:3000", 113 | }, 114 | } { 115 | //expected defaults 116 | expected := test.Output 117 | if expected.LocalHost == "" { 118 | expected.LocalHost = "0.0.0.0" 119 | } 120 | if expected.RemoteProto == "" { 121 | expected.RemoteProto = "tcp" 122 | } 123 | if expected.LocalProto == "" { 124 | expected.LocalProto = "tcp" 125 | } 126 | //compare 127 | got, err := DecodeRemote(test.Input) 128 | if err != nil { 129 | t.Fatalf("decode #%d '%s' failed: %s", i+1, test.Input, err) 130 | } 131 | if !reflect.DeepEqual(got, &expected) { 132 | t.Fatalf("decode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, expected, got) 133 | } 134 | if e := got.Encode(); test.Encoded != e { 135 | t.Fatalf("encode #%d '%s' expected\n %#v\ngot\n %#v", i+1, test.Input, test.Encoded, e) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /share/settings/user.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var UserAllowAll = regexp.MustCompile("") 9 | 10 | func ParseAuth(auth string) (string, string) { 11 | if strings.Contains(auth, ":") { 12 | pair := strings.SplitN(auth, ":", 2) 13 | return pair[0], pair[1] 14 | } 15 | return "", "" 16 | } 17 | 18 | type User struct { 19 | Name string 20 | Pass string 21 | Addrs []*regexp.Regexp 22 | } 23 | 24 | func (u *User) HasAccess(addr string) bool { 25 | m := false 26 | for _, r := range u.Addrs { 27 | if r.MatchString(addr) { 28 | m = true 29 | break 30 | } 31 | } 32 | return m 33 | } 34 | -------------------------------------------------------------------------------- /share/settings/users.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "sync" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/jpillora/chisel/share/cio" 13 | ) 14 | 15 | type Users struct { 16 | sync.RWMutex 17 | inner map[string]*User 18 | } 19 | 20 | func NewUsers() *Users { 21 | return &Users{inner: map[string]*User{}} 22 | } 23 | 24 | // Len returns the numbers of users 25 | func (u *Users) Len() int { 26 | u.RLock() 27 | l := len(u.inner) 28 | u.RUnlock() 29 | return l 30 | } 31 | 32 | // Get user from the index by key 33 | func (u *Users) Get(key string) (*User, bool) { 34 | u.RLock() 35 | user, found := u.inner[key] 36 | u.RUnlock() 37 | return user, found 38 | } 39 | 40 | // Set a users into the list by specific key 41 | func (u *Users) Set(key string, user *User) { 42 | u.Lock() 43 | u.inner[key] = user 44 | u.Unlock() 45 | } 46 | 47 | // Del ete a users from the list 48 | func (u *Users) Del(key string) { 49 | u.Lock() 50 | delete(u.inner, key) 51 | u.Unlock() 52 | } 53 | 54 | // AddUser adds a users to the set 55 | func (u *Users) AddUser(user *User) { 56 | u.Set(user.Name, user) 57 | } 58 | 59 | // Reset all users to the given set, 60 | // Use nil to remove all. 61 | func (u *Users) Reset(users []*User) { 62 | m := map[string]*User{} 63 | for _, u := range users { 64 | m[u.Name] = u 65 | } 66 | u.Lock() 67 | u.inner = m 68 | u.Unlock() 69 | } 70 | 71 | // UserIndex is a reloadable user source 72 | type UserIndex struct { 73 | *cio.Logger 74 | *Users 75 | configFile string 76 | } 77 | 78 | // NewUserIndex creates a source for users 79 | func NewUserIndex(logger *cio.Logger) *UserIndex { 80 | return &UserIndex{ 81 | Logger: logger.Fork("users"), 82 | Users: NewUsers(), 83 | } 84 | } 85 | 86 | // LoadUsers is responsible for loading users from a file 87 | func (u *UserIndex) LoadUsers(configFile string) error { 88 | u.configFile = configFile 89 | u.Infof("Loading configuration file %s", configFile) 90 | if err := u.loadUserIndex(); err != nil { 91 | return err 92 | } 93 | if err := u.addWatchEvents(); err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | // watchEvents is responsible for watching for updates to the file and reloading 100 | func (u *UserIndex) addWatchEvents() error { 101 | watcher, err := fsnotify.NewWatcher() 102 | if err != nil { 103 | return err 104 | } 105 | if err := watcher.Add(u.configFile); err != nil { 106 | return err 107 | } 108 | go func() { 109 | for e := range watcher.Events { 110 | if e.Op&fsnotify.Write != fsnotify.Write { 111 | continue 112 | } 113 | if err := u.loadUserIndex(); err != nil { 114 | u.Infof("Failed to reload the users configuration: %s", err) 115 | } else { 116 | u.Debugf("Users configuration successfully reloaded from: %s", u.configFile) 117 | } 118 | } 119 | }() 120 | return nil 121 | } 122 | 123 | // loadUserIndex is responsible for loading the users configuration 124 | func (u *UserIndex) loadUserIndex() error { 125 | if u.configFile == "" { 126 | return errors.New("configuration file not set") 127 | } 128 | b, err := os.ReadFile(u.configFile) 129 | if err != nil { 130 | return fmt.Errorf("Failed to read auth file: %s, error: %s", u.configFile, err) 131 | } 132 | var raw map[string][]string 133 | if err := json.Unmarshal(b, &raw); err != nil { 134 | return errors.New("Invalid JSON: " + err.Error()) 135 | } 136 | users := []*User{} 137 | for auth, remotes := range raw { 138 | user := &User{} 139 | user.Name, user.Pass = ParseAuth(auth) 140 | if user.Name == "" { 141 | return errors.New("Invalid user:pass string") 142 | } 143 | for _, r := range remotes { 144 | if r == "" || r == "*" { 145 | user.Addrs = append(user.Addrs, UserAllowAll) 146 | } else { 147 | re, err := regexp.Compile(r) 148 | if err != nil { 149 | return errors.New("Invalid address regex") 150 | } 151 | user.Addrs = append(user.Addrs, re) 152 | } 153 | } 154 | users = append(users, user) 155 | } 156 | //swap 157 | u.Reset(users) 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /share/tunnel/tunnel.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io" 8 | "log" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/armon/go-socks5" 14 | "github.com/jpillora/chisel/share/cio" 15 | "github.com/jpillora/chisel/share/cnet" 16 | "github.com/jpillora/chisel/share/settings" 17 | "golang.org/x/crypto/ssh" 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | //Config a Tunnel 22 | type Config struct { 23 | *cio.Logger 24 | Inbound bool 25 | Outbound bool 26 | Socks bool 27 | KeepAlive time.Duration 28 | } 29 | 30 | //Tunnel represents an SSH tunnel with proxy capabilities. 31 | //Both chisel client and server are Tunnels. 32 | //chisel client has a single set of remotes, whereas 33 | //chisel server has multiple sets of remotes (one set per client). 34 | //Each remote has a 1:1 mapping to a proxy. 35 | //Proxies listen, send data over ssh, and the other end of the ssh connection 36 | //communicates with the endpoint and returns the response. 37 | type Tunnel struct { 38 | Config 39 | //ssh connection 40 | activeConnMut sync.RWMutex 41 | activatingConn waitGroup 42 | activeConn ssh.Conn 43 | //proxies 44 | proxyCount int 45 | //internals 46 | connStats cnet.ConnCount 47 | socksServer *socks5.Server 48 | } 49 | 50 | //New Tunnel from the given Config 51 | func New(c Config) *Tunnel { 52 | c.Logger = c.Logger.Fork("tun") 53 | t := &Tunnel{ 54 | Config: c, 55 | } 56 | t.activatingConn.Add(1) 57 | //setup socks server (not listening on any port!) 58 | extra := "" 59 | if c.Socks { 60 | sl := log.New(io.Discard, "", 0) 61 | if t.Logger.Debug { 62 | sl = log.New(os.Stdout, "[socks]", log.Ldate|log.Ltime) 63 | } 64 | t.socksServer, _ = socks5.New(&socks5.Config{Logger: sl}) 65 | extra += " (SOCKS enabled)" 66 | } 67 | t.Debugf("Created%s", extra) 68 | return t 69 | } 70 | 71 | //BindSSH provides an active SSH for use for tunnelling 72 | func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error { 73 | //link ctx to ssh-conn 74 | go func() { 75 | <-ctx.Done() 76 | if c.Close() == nil { 77 | t.Debugf("SSH cancelled") 78 | } 79 | t.activatingConn.DoneAll() 80 | }() 81 | //mark active and unblock 82 | t.activeConnMut.Lock() 83 | if t.activeConn != nil { 84 | panic("double bind ssh") 85 | } 86 | t.activeConn = c 87 | t.activeConnMut.Unlock() 88 | t.activatingConn.Done() 89 | //optional keepalive loop against this connection 90 | if t.Config.KeepAlive > 0 { 91 | go t.keepAliveLoop(c) 92 | } 93 | //block until closed 94 | go t.handleSSHRequests(reqs) 95 | go t.handleSSHChannels(chans) 96 | t.Debugf("SSH connected") 97 | err := c.Wait() 98 | t.Debugf("SSH disconnected") 99 | //mark inactive and block 100 | t.activatingConn.Add(1) 101 | t.activeConnMut.Lock() 102 | t.activeConn = nil 103 | t.activeConnMut.Unlock() 104 | return err 105 | } 106 | 107 | //getSSH blocks while connecting 108 | func (t *Tunnel) getSSH(ctx context.Context) ssh.Conn { 109 | //cancelled already? 110 | if isDone(ctx) { 111 | return nil 112 | } 113 | t.activeConnMut.RLock() 114 | c := t.activeConn 115 | t.activeConnMut.RUnlock() 116 | //connected already? 117 | if c != nil { 118 | return c 119 | } 120 | //connecting... 121 | select { 122 | case <-ctx.Done(): //cancelled 123 | return nil 124 | case <-time.After(settings.EnvDuration("SSH_WAIT", 35*time.Second)): 125 | return nil //a bit longer than ssh timeout 126 | case <-t.activatingConnWait(): 127 | t.activeConnMut.RLock() 128 | c := t.activeConn 129 | t.activeConnMut.RUnlock() 130 | return c 131 | } 132 | } 133 | 134 | func (t *Tunnel) activatingConnWait() <-chan struct{} { 135 | ch := make(chan struct{}) 136 | go func() { 137 | t.activatingConn.Wait() 138 | close(ch) 139 | }() 140 | return ch 141 | } 142 | 143 | //BindRemotes converts the given remotes into proxies, and blocks 144 | //until the caller cancels the context or there is a proxy error. 145 | func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error { 146 | if len(remotes) == 0 { 147 | return errors.New("no remotes") 148 | } 149 | if !t.Inbound { 150 | return errors.New("inbound connections blocked") 151 | } 152 | proxies := make([]*Proxy, len(remotes)) 153 | for i, remote := range remotes { 154 | p, err := NewProxy(t.Logger, t, t.proxyCount, remote) 155 | if err != nil { 156 | return err 157 | } 158 | proxies[i] = p 159 | t.proxyCount++ 160 | } 161 | //TODO: handle tunnel close 162 | eg, ctx := errgroup.WithContext(ctx) 163 | for _, proxy := range proxies { 164 | p := proxy 165 | eg.Go(func() error { 166 | return p.Run(ctx) 167 | }) 168 | } 169 | t.Debugf("Bound proxies") 170 | err := eg.Wait() 171 | t.Debugf("Unbound proxies") 172 | return err 173 | } 174 | 175 | func (t *Tunnel) keepAliveLoop(sshConn ssh.Conn) { 176 | //ping forever 177 | for { 178 | time.Sleep(t.Config.KeepAlive) 179 | _, b, err := sshConn.SendRequest("ping", true, nil) 180 | if err != nil { 181 | break 182 | } 183 | if len(b) > 0 && !bytes.Equal(b, []byte("pong")) { 184 | t.Debugf("strange ping response") 185 | break 186 | } 187 | } 188 | //close ssh connection on abnormal ping 189 | sshConn.Close() 190 | } 191 | -------------------------------------------------------------------------------- /share/tunnel/tunnel_in_proxy.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "sync" 8 | 9 | "github.com/jpillora/chisel/share/cio" 10 | "github.com/jpillora/chisel/share/settings" 11 | "github.com/jpillora/sizestr" 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | //sshTunnel exposes a subset of Tunnel to subtypes 16 | type sshTunnel interface { 17 | getSSH(ctx context.Context) ssh.Conn 18 | } 19 | 20 | //Proxy is the inbound portion of a Tunnel 21 | type Proxy struct { 22 | *cio.Logger 23 | sshTun sshTunnel 24 | id int 25 | count int 26 | remote *settings.Remote 27 | dialer net.Dialer 28 | tcp *net.TCPListener 29 | udp *udpListener 30 | mu sync.Mutex 31 | } 32 | 33 | //NewProxy creates a Proxy 34 | func NewProxy(logger *cio.Logger, sshTun sshTunnel, index int, remote *settings.Remote) (*Proxy, error) { 35 | id := index + 1 36 | p := &Proxy{ 37 | Logger: logger.Fork("proxy#%s", remote.String()), 38 | sshTun: sshTun, 39 | id: id, 40 | remote: remote, 41 | } 42 | return p, p.listen() 43 | } 44 | 45 | func (p *Proxy) listen() error { 46 | if p.remote.Stdio { 47 | //TODO check if pipes active? 48 | } else if p.remote.LocalProto == "tcp" { 49 | addr, err := net.ResolveTCPAddr("tcp", p.remote.LocalHost+":"+p.remote.LocalPort) 50 | if err != nil { 51 | return p.Errorf("resolve: %s", err) 52 | } 53 | l, err := net.ListenTCP("tcp", addr) 54 | if err != nil { 55 | return p.Errorf("tcp: %s", err) 56 | } 57 | p.Infof("Listening") 58 | p.tcp = l 59 | } else if p.remote.LocalProto == "udp" { 60 | l, err := listenUDP(p.Logger, p.sshTun, p.remote) 61 | if err != nil { 62 | return err 63 | } 64 | p.Infof("Listening") 65 | p.udp = l 66 | } else { 67 | return p.Errorf("unknown local proto") 68 | } 69 | return nil 70 | } 71 | 72 | //Run enables the proxy and blocks while its active, 73 | //close the proxy by cancelling the context. 74 | func (p *Proxy) Run(ctx context.Context) error { 75 | if p.remote.Stdio { 76 | return p.runStdio(ctx) 77 | } else if p.remote.LocalProto == "tcp" { 78 | return p.runTCP(ctx) 79 | } else if p.remote.LocalProto == "udp" { 80 | return p.udp.run(ctx) 81 | } 82 | panic("should not get here") 83 | } 84 | 85 | func (p *Proxy) runStdio(ctx context.Context) error { 86 | defer p.Infof("Closed") 87 | for { 88 | p.pipeRemote(ctx, cio.Stdio) 89 | select { 90 | case <-ctx.Done(): 91 | return nil 92 | default: 93 | // the connection is not ready yet, keep waiting 94 | } 95 | } 96 | } 97 | 98 | func (p *Proxy) runTCP(ctx context.Context) error { 99 | done := make(chan struct{}) 100 | //implements missing net.ListenContext 101 | go func() { 102 | select { 103 | case <-ctx.Done(): 104 | p.tcp.Close() 105 | case <-done: 106 | } 107 | }() 108 | for { 109 | src, err := p.tcp.Accept() 110 | if err != nil { 111 | select { 112 | case <-ctx.Done(): 113 | //listener closed 114 | err = nil 115 | default: 116 | p.Infof("Accept error: %s", err) 117 | } 118 | close(done) 119 | return err 120 | } 121 | go p.pipeRemote(ctx, src) 122 | } 123 | } 124 | 125 | func (p *Proxy) pipeRemote(ctx context.Context, src io.ReadWriteCloser) { 126 | defer src.Close() 127 | 128 | p.mu.Lock() 129 | p.count++ 130 | cid := p.count 131 | p.mu.Unlock() 132 | 133 | l := p.Fork("conn#%d", cid) 134 | l.Debugf("Open") 135 | sshConn := p.sshTun.getSSH(ctx) 136 | if sshConn == nil { 137 | l.Debugf("No remote connection") 138 | return 139 | } 140 | //ssh request for tcp connection for this proxy's remote 141 | dst, reqs, err := sshConn.OpenChannel("chisel", []byte(p.remote.Remote())) 142 | if err != nil { 143 | l.Infof("Stream error: %s", err) 144 | return 145 | } 146 | go ssh.DiscardRequests(reqs) 147 | //then pipe 148 | s, r := cio.Pipe(src, dst) 149 | l.Debugf("Close (sent %s received %s)", sizestr.ToString(s), sizestr.ToString(r)) 150 | } 151 | -------------------------------------------------------------------------------- /share/tunnel/tunnel_in_proxy_udp.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "context" 5 | "encoding/gob" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/jpillora/chisel/share/cio" 15 | "github.com/jpillora/chisel/share/settings" 16 | "github.com/jpillora/sizestr" 17 | "golang.org/x/crypto/ssh" 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | //listenUDP is a special listener which forwards packets via 22 | //the bound ssh connection. tricky part is multiplexing lots of 23 | //udp clients through the entry node. each will listen on its 24 | //own source-port for a response: 25 | // (random) 26 | // src-1 1111->... dst-1 6345->7777 27 | // src-2 2222->... <---> udp <---> udp <-> dst-1 7543->7777 28 | // src-3 3333->... listener handler dst-1 1444->7777 29 | // 30 | //we must store these mappings (1111-6345, etc) in memory for a length 31 | //of time, so that when the exit node receives a response on 6345, it 32 | //knows to return it to 1111. 33 | func listenUDP(l *cio.Logger, sshTun sshTunnel, remote *settings.Remote) (*udpListener, error) { 34 | a, err := net.ResolveUDPAddr("udp", remote.Local()) 35 | if err != nil { 36 | return nil, l.Errorf("resolve: %s", err) 37 | } 38 | conn, err := net.ListenUDP("udp", a) 39 | if err != nil { 40 | return nil, l.Errorf("listen: %s", err) 41 | } 42 | //ready 43 | u := &udpListener{ 44 | Logger: l, 45 | sshTun: sshTun, 46 | remote: remote, 47 | inbound: conn, 48 | maxMTU: settings.EnvInt("UDP_MAX_SIZE", 9012), 49 | } 50 | u.Debugf("UDP max size: %d bytes", u.maxMTU) 51 | return u, nil 52 | } 53 | 54 | type udpListener struct { 55 | *cio.Logger 56 | sshTun sshTunnel 57 | remote *settings.Remote 58 | inbound *net.UDPConn 59 | outboundMut sync.Mutex 60 | outbound *udpChannel 61 | sent, recv int64 62 | maxMTU int 63 | } 64 | 65 | func (u *udpListener) run(ctx context.Context) error { 66 | defer u.inbound.Close() 67 | //udp doesnt accept connections, 68 | //udp simply forwards packets 69 | //and therefore only needs to listen 70 | eg, ctx := errgroup.WithContext(ctx) 71 | eg.Go(func() error { 72 | return u.runInbound(ctx) 73 | }) 74 | eg.Go(func() error { 75 | return u.runOutbound(ctx) 76 | }) 77 | if err := eg.Wait(); err != nil { 78 | u.Debugf("listen: %s", err) 79 | return err 80 | } 81 | u.Debugf("Close (sent %s received %s)", sizestr.ToString(u.sent), sizestr.ToString(u.recv)) 82 | return nil 83 | } 84 | 85 | func (u *udpListener) runInbound(ctx context.Context) error { 86 | buff := make([]byte, u.maxMTU) 87 | for !isDone(ctx) { 88 | //read from inbound udp 89 | u.inbound.SetReadDeadline(time.Now().Add(time.Second)) 90 | n, addr, err := u.inbound.ReadFromUDP(buff) 91 | if e, ok := err.(net.Error); ok && (e.Timeout() || e.Temporary()) { 92 | continue 93 | } 94 | if err != nil { 95 | return u.Errorf("read error: %w", err) 96 | } 97 | //upsert ssh channel 98 | uc, err := u.getUDPChan(ctx) 99 | if err != nil { 100 | if strings.HasSuffix(err.Error(), "EOF") { 101 | continue 102 | } 103 | return u.Errorf("inbound-udpchan: %w", err) 104 | } 105 | //send over channel, including source address 106 | b := buff[:n] 107 | if err := uc.encode(addr.String(), b); err != nil { 108 | if strings.HasSuffix(err.Error(), "EOF") { 109 | continue //dropped packet... 110 | } 111 | return u.Errorf("encode error: %w", err) 112 | } 113 | //stats 114 | atomic.AddInt64(&u.sent, int64(n)) 115 | } 116 | return nil 117 | } 118 | 119 | func (u *udpListener) runOutbound(ctx context.Context) error { 120 | for !isDone(ctx) { 121 | //upsert ssh channel 122 | uc, err := u.getUDPChan(ctx) 123 | if err != nil { 124 | if strings.HasSuffix(err.Error(), "EOF") { 125 | continue 126 | } 127 | return u.Errorf("outbound-udpchan: %w", err) 128 | } 129 | //receive from channel, including source address 130 | p := udpPacket{} 131 | if err := uc.decode(&p); err == io.EOF { 132 | //outbound ssh disconnected, get new connection... 133 | continue 134 | } else if err != nil { 135 | return u.Errorf("decode error: %w", err) 136 | } 137 | //write back to inbound udp 138 | addr, err := net.ResolveUDPAddr("udp", p.Src) 139 | if err != nil { 140 | return u.Errorf("resolve error: %w", err) 141 | } 142 | n, err := u.inbound.WriteToUDP(p.Payload, addr) 143 | if err != nil { 144 | return u.Errorf("write error: %w", err) 145 | } 146 | //stats 147 | atomic.AddInt64(&u.recv, int64(n)) 148 | } 149 | return nil 150 | } 151 | 152 | func (u *udpListener) getUDPChan(ctx context.Context) (*udpChannel, error) { 153 | u.outboundMut.Lock() 154 | defer u.outboundMut.Unlock() 155 | //cached 156 | if u.outbound != nil { 157 | return u.outbound, nil 158 | } 159 | //not cached, bind 160 | sshConn := u.sshTun.getSSH(ctx) 161 | if sshConn == nil { 162 | return nil, fmt.Errorf("ssh-conn nil") 163 | } 164 | //ssh request for udp packets for this proxy's remote, 165 | //just "udp" since the remote address is sent with each packet 166 | dstAddr := u.remote.Remote() + "/udp" 167 | rwc, reqs, err := sshConn.OpenChannel("chisel", []byte(dstAddr)) 168 | if err != nil { 169 | return nil, fmt.Errorf("ssh-chan error: %s", err) 170 | } 171 | go ssh.DiscardRequests(reqs) 172 | //remove on disconnect 173 | go u.unsetUDPChan(sshConn) 174 | //ready 175 | o := &udpChannel{ 176 | r: gob.NewDecoder(rwc), 177 | w: gob.NewEncoder(rwc), 178 | c: rwc, 179 | } 180 | u.outbound = o 181 | u.Debugf("aquired channel") 182 | return o, nil 183 | } 184 | 185 | func (u *udpListener) unsetUDPChan(sshConn ssh.Conn) { 186 | sshConn.Wait() 187 | u.Debugf("lost channel") 188 | u.outboundMut.Lock() 189 | u.outbound = nil 190 | u.outboundMut.Unlock() 191 | } 192 | -------------------------------------------------------------------------------- /share/tunnel/tunnel_out_ssh.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "strings" 8 | 9 | "github.com/jpillora/chisel/share/cio" 10 | "github.com/jpillora/chisel/share/cnet" 11 | "github.com/jpillora/chisel/share/settings" 12 | "github.com/jpillora/sizestr" 13 | "golang.org/x/crypto/ssh" 14 | ) 15 | 16 | func (t *Tunnel) handleSSHRequests(reqs <-chan *ssh.Request) { 17 | for r := range reqs { 18 | switch r.Type { 19 | case "ping": 20 | r.Reply(true, []byte("pong")) 21 | default: 22 | t.Debugf("Unknown request: %s", r.Type) 23 | } 24 | } 25 | } 26 | 27 | func (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) { 28 | for ch := range chans { 29 | go t.handleSSHChannel(ch) 30 | } 31 | } 32 | 33 | func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) { 34 | if !t.Config.Outbound { 35 | t.Debugf("Denied outbound connection") 36 | ch.Reject(ssh.Prohibited, "Denied outbound connection") 37 | return 38 | } 39 | remote := string(ch.ExtraData()) 40 | //extract protocol 41 | hostPort, proto := settings.L4Proto(remote) 42 | udp := proto == "udp" 43 | socks := hostPort == "socks" 44 | if socks && t.socksServer == nil { 45 | t.Debugf("Denied socks request, please enable socks") 46 | ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled") 47 | return 48 | } 49 | sshChan, reqs, err := ch.Accept() 50 | if err != nil { 51 | t.Debugf("Failed to accept stream: %s", err) 52 | return 53 | } 54 | stream := io.ReadWriteCloser(sshChan) 55 | //cnet.MeterRWC(t.Logger.Fork("sshchan"), sshChan) 56 | defer stream.Close() 57 | go ssh.DiscardRequests(reqs) 58 | l := t.Logger.Fork("conn#%d", t.connStats.New()) 59 | //ready to handle 60 | t.connStats.Open() 61 | l.Debugf("Open %s", t.connStats.String()) 62 | if socks { 63 | err = t.handleSocks(stream) 64 | } else if udp { 65 | err = t.handleUDP(l, stream, hostPort) 66 | } else { 67 | err = t.handleTCP(l, stream, hostPort) 68 | } 69 | t.connStats.Close() 70 | errmsg := "" 71 | if err != nil && !strings.HasSuffix(err.Error(), "EOF") { 72 | errmsg = fmt.Sprintf(" (error %s)", err) 73 | } 74 | l.Debugf("Close %s%s", t.connStats.String(), errmsg) 75 | } 76 | 77 | func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error { 78 | return t.socksServer.ServeConn(cnet.NewRWCConn(src)) 79 | } 80 | 81 | func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error { 82 | dst, err := net.Dial("tcp", hostPort) 83 | if err != nil { 84 | return err 85 | } 86 | s, r := cio.Pipe(src, dst) 87 | l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r)) 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /share/tunnel/tunnel_out_ssh_udp.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "encoding/gob" 5 | "io" 6 | "net" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/jpillora/chisel/share/cio" 12 | "github.com/jpillora/chisel/share/settings" 13 | ) 14 | 15 | func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error { 16 | conns := &udpConns{ 17 | Logger: l, 18 | m: map[string]*udpConn{}, 19 | } 20 | defer conns.closeAll() 21 | h := &udpHandler{ 22 | Logger: l, 23 | hostPort: hostPort, 24 | udpChannel: &udpChannel{ 25 | r: gob.NewDecoder(rwc), 26 | w: gob.NewEncoder(rwc), 27 | c: rwc, 28 | }, 29 | udpConns: conns, 30 | maxMTU: settings.EnvInt("UDP_MAX_SIZE", 9012), 31 | } 32 | h.Debugf("UDP max size: %d bytes", h.maxMTU) 33 | for { 34 | p := udpPacket{} 35 | if err := h.handleWrite(&p); err != nil { 36 | return err 37 | } 38 | } 39 | } 40 | 41 | type udpHandler struct { 42 | *cio.Logger 43 | hostPort string 44 | *udpChannel 45 | *udpConns 46 | maxMTU int 47 | } 48 | 49 | func (h *udpHandler) handleWrite(p *udpPacket) error { 50 | if err := h.r.Decode(&p); err != nil { 51 | return err 52 | } 53 | //dial now, we know we must write 54 | conn, exists, err := h.udpConns.dial(p.Src, h.hostPort) 55 | if err != nil { 56 | return err 57 | } 58 | //however, we dont know if we must read... 59 | //spawn up to go-routines to wait 60 | //for a reply. 61 | //TODO configurable 62 | //TODO++ dont use go-routines, switch to pollable 63 | // array of listeners where all listeners are 64 | // sweeped periodically, removing the idle ones 65 | const maxConns = 100 66 | if !exists { 67 | if h.udpConns.len() <= maxConns { 68 | go h.handleRead(p, conn) 69 | } else { 70 | h.Debugf("exceeded max udp connections (%d)", maxConns) 71 | } 72 | } 73 | _, err = conn.Write(p.Payload) 74 | if err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | func (h *udpHandler) handleRead(p *udpPacket, conn *udpConn) { 81 | //ensure connection is cleaned up 82 | defer h.udpConns.remove(conn.id) 83 | buff := make([]byte, h.maxMTU) 84 | for { 85 | //response must arrive within 15 seconds 86 | deadline := settings.EnvDuration("UDP_DEADLINE", 15*time.Second) 87 | conn.SetReadDeadline(time.Now().Add(deadline)) 88 | //read response 89 | n, err := conn.Read(buff) 90 | if err != nil { 91 | if !os.IsTimeout(err) && err != io.EOF { 92 | h.Debugf("read error: %s", err) 93 | } 94 | break 95 | } 96 | b := buff[:n] 97 | //encode back over ssh connection 98 | err = h.udpChannel.encode(p.Src, b) 99 | if err != nil { 100 | h.Debugf("encode error: %s", err) 101 | return 102 | } 103 | } 104 | } 105 | 106 | type udpConns struct { 107 | *cio.Logger 108 | sync.Mutex 109 | m map[string]*udpConn 110 | } 111 | 112 | func (cs *udpConns) dial(id, addr string) (*udpConn, bool, error) { 113 | cs.Lock() 114 | defer cs.Unlock() 115 | conn, ok := cs.m[id] 116 | if !ok { 117 | c, err := net.Dial("udp", addr) 118 | if err != nil { 119 | return nil, false, err 120 | } 121 | conn = &udpConn{ 122 | id: id, 123 | Conn: c, // cnet.MeterConn(cs.Logger.Fork(addr), c), 124 | } 125 | cs.m[id] = conn 126 | } 127 | return conn, ok, nil 128 | } 129 | 130 | func (cs *udpConns) len() int { 131 | cs.Lock() 132 | l := len(cs.m) 133 | cs.Unlock() 134 | return l 135 | } 136 | 137 | func (cs *udpConns) remove(id string) { 138 | cs.Lock() 139 | delete(cs.m, id) 140 | cs.Unlock() 141 | } 142 | 143 | func (cs *udpConns) closeAll() { 144 | cs.Lock() 145 | for id, conn := range cs.m { 146 | conn.Close() 147 | delete(cs.m, id) 148 | } 149 | cs.Unlock() 150 | } 151 | 152 | type udpConn struct { 153 | id string 154 | net.Conn 155 | } 156 | -------------------------------------------------------------------------------- /share/tunnel/udp.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "context" 5 | "encoding/gob" 6 | "io" 7 | ) 8 | 9 | type udpPacket struct { 10 | Src string 11 | Payload []byte 12 | } 13 | 14 | func init() { 15 | gob.Register(&udpPacket{}) 16 | } 17 | 18 | //udpChannel encodes/decodes udp payloads over a stream 19 | type udpChannel struct { 20 | r *gob.Decoder 21 | w *gob.Encoder 22 | c io.Closer 23 | } 24 | 25 | func (o *udpChannel) encode(src string, b []byte) error { 26 | return o.w.Encode(udpPacket{ 27 | Src: src, 28 | Payload: b, 29 | }) 30 | } 31 | 32 | func (o *udpChannel) decode(p *udpPacket) error { 33 | return o.r.Decode(p) 34 | } 35 | 36 | func isDone(ctx context.Context) bool { 37 | select { 38 | case <-ctx.Done(): 39 | return true 40 | default: 41 | return false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /share/tunnel/wg.go: -------------------------------------------------------------------------------- 1 | package tunnel 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type waitGroup struct { 9 | inner sync.WaitGroup 10 | n int32 11 | } 12 | 13 | func (w *waitGroup) Add(n int) { 14 | atomic.AddInt32(&w.n, int32(n)) 15 | w.inner.Add(n) 16 | } 17 | 18 | func (w *waitGroup) Done() { 19 | if n := atomic.LoadInt32(&w.n); n > 0 && atomic.CompareAndSwapInt32(&w.n, n, n-1) { 20 | w.inner.Done() 21 | } 22 | } 23 | 24 | func (w *waitGroup) DoneAll() { 25 | for atomic.LoadInt32(&w.n) > 0 { 26 | w.Done() 27 | } 28 | } 29 | 30 | func (w *waitGroup) Wait() { 31 | w.inner.Wait() 32 | } 33 | -------------------------------------------------------------------------------- /share/version.go: -------------------------------------------------------------------------------- 1 | package chshare 2 | 3 | //ProtocolVersion of chisel. When backwards 4 | //incompatible changes are made, this will 5 | //be incremented to signify a protocol 6 | //mismatch. 7 | var ProtocolVersion = "chisel-v3" 8 | 9 | var BuildVersion = "0.0.0-src" 10 | -------------------------------------------------------------------------------- /test/bench/main.go: -------------------------------------------------------------------------------- 1 | //chisel end-to-end test 2 | //====================== 3 | // 4 | // (direct) 5 | // .--------------->----------------. 6 | // / chisel chisel \ 7 | // request--->client:2001--->server:2002---->fileserver:3000 8 | // \ / 9 | // '--> crowbar:4001--->crowbar:4002' 10 | // client server 11 | // 12 | // crowbar and chisel binaries should be in your PATH 13 | 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "io" 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(io.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 | } 212 | -------------------------------------------------------------------------------- /test/bench/perf.md: -------------------------------------------------------------------------------- 1 | 2 | ### Performance 3 | 4 | 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. 5 | 6 | In this simple benchmark, we have: 7 | 8 | ``` 9 | (direct) 10 | .--------------->----------------. 11 | / chisel chisel \ 12 | request--->client:2001--->server:2002---->fileserver:3000 13 | \ / 14 | '--> crowbar:4001--->crowbar:4002' 15 | client server 16 | ``` 17 | 18 | Note, we're using an in-memory "file" server on localhost for these tests 19 | 20 | _direct_ 21 | 22 | ``` 23 | :3000 => 1 bytes in 1.291417ms 24 | :3000 => 10 bytes in 713.525µs 25 | :3000 => 100 bytes in 562.48µs 26 | :3000 => 1000 bytes in 595.445µs 27 | :3000 => 10000 bytes in 1.053298ms 28 | :3000 => 100000 bytes in 741.351µs 29 | :3000 => 1000000 bytes in 1.367143ms 30 | :3000 => 10000000 bytes in 8.601549ms 31 | :3000 => 100000000 bytes in 76.3939ms 32 | ``` 33 | 34 | `chisel` 35 | 36 | ``` 37 | :2001 => 1 bytes in 1.351976ms 38 | :2001 => 10 bytes in 1.106086ms 39 | :2001 => 100 bytes in 1.005729ms 40 | :2001 => 1000 bytes in 1.254396ms 41 | :2001 => 10000 bytes in 1.139777ms 42 | :2001 => 100000 bytes in 2.35437ms 43 | :2001 => 1000000 bytes in 11.502673ms 44 | :2001 => 10000000 bytes in 123.130246ms 45 | :2001 => 100000000 bytes in 966.48636ms 46 | ``` 47 | 48 | ~100MB in **~1 second** 49 | 50 | `crowbar` 51 | 52 | ``` 53 | :4001 => 1 bytes in 3.335797ms 54 | :4001 => 10 bytes in 1.453007ms 55 | :4001 => 100 bytes in 1.811727ms 56 | :4001 => 1000 bytes in 1.621525ms 57 | :4001 => 10000 bytes in 5.20729ms 58 | :4001 => 100000 bytes in 38.461926ms 59 | :4001 => 1000000 bytes in 358.784864ms 60 | :4001 => 10000000 bytes in 3.603206487s 61 | :4001 => 100000000 bytes in 36.332395213s 62 | ``` 63 | 64 | ~100MB in **36 seconds** 65 | 66 | See `test/bench/main.go` -------------------------------------------------------------------------------- /test/bench/userfile: -------------------------------------------------------------------------------- 1 | foo:bar -------------------------------------------------------------------------------- /test/e2e/auth_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "testing" 5 | 6 | chclient "github.com/jpillora/chisel/client" 7 | chserver "github.com/jpillora/chisel/server" 8 | ) 9 | 10 | //TODO tests for: 11 | // - failed auth 12 | // - dynamic auth (server add/remove user) 13 | // - watch auth file 14 | 15 | func TestAuth(t *testing.T) { 16 | tmpPort1 := availablePort() 17 | tmpPort2 := availablePort() 18 | //setup server, client, fileserver 19 | teardown := simpleSetup(t, 20 | &chserver.Config{ 21 | KeySeed: "foobar", 22 | Auth: "../bench/userfile", 23 | }, 24 | &chclient.Config{ 25 | Remotes: []string{ 26 | "0.0.0.0:" + tmpPort1 + ":127.0.0.1:$FILEPORT", 27 | "0.0.0.0:" + tmpPort2 + ":localhost:$FILEPORT", 28 | }, 29 | Auth: "foo:bar", 30 | }) 31 | defer teardown() 32 | //test first remote 33 | result, err := post("http://localhost:"+tmpPort1, "foo") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if result != "foo!" { 38 | t.Fatalf("expected exclamation mark added") 39 | } 40 | //test second remote 41 | result, err = post("http://localhost:"+tmpPort2, "bar") 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if result != "bar!" { 46 | t.Fatalf("expected exclamation mark added again") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/e2e/base_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "testing" 5 | 6 | chclient "github.com/jpillora/chisel/client" 7 | chserver "github.com/jpillora/chisel/server" 8 | ) 9 | 10 | func TestBase(t *testing.T) { 11 | tmpPort := availablePort() 12 | //setup server, client, fileserver 13 | teardown := simpleSetup(t, 14 | &chserver.Config{}, 15 | &chclient.Config{ 16 | Remotes: []string{tmpPort + ":$FILEPORT"}, 17 | }) 18 | defer teardown() 19 | //test remote 20 | result, err := post("http://localhost:"+tmpPort, "foo") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if result != "foo!" { 25 | t.Fatalf("expected exclamation mark added") 26 | } 27 | } 28 | 29 | func TestReverse(t *testing.T) { 30 | tmpPort := availablePort() 31 | //setup server, client, fileserver 32 | teardown := simpleSetup(t, 33 | &chserver.Config{ 34 | Reverse: true, 35 | }, 36 | &chclient.Config{ 37 | Remotes: []string{"R:" + tmpPort + ":$FILEPORT"}, 38 | }) 39 | defer teardown() 40 | //test remote (this goes through the server and out the client) 41 | result, err := post("http://localhost:"+tmpPort, "foo") 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if result != "foo!" { 46 | t.Fatalf("expected exclamation mark added") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/e2e/cert_utils_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/ed25519" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/x509" 11 | "crypto/x509/pkix" 12 | "encoding/pem" 13 | "fmt" 14 | "math/big" 15 | "net" 16 | "os" 17 | "path" 18 | "time" 19 | 20 | chclient "github.com/jpillora/chisel/client" 21 | chserver "github.com/jpillora/chisel/server" 22 | ) 23 | 24 | type tlsConfig struct { 25 | serverTLS *chserver.TLSConfig 26 | clientTLS *chclient.TLSConfig 27 | tmpDir string 28 | } 29 | 30 | func (t *tlsConfig) Close() { 31 | if t.tmpDir != "" { 32 | os.RemoveAll(t.tmpDir) 33 | } 34 | } 35 | 36 | func newTestTLSConfig() (*tlsConfig, error) { 37 | tlsConfig := &tlsConfig{} 38 | _, serverCertPEM, serverKeyPEM, err := certGetCertificate(&certConfig{ 39 | hosts: []string{ 40 | "0.0.0.0", 41 | "localhost", 42 | }, 43 | extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 44 | }) 45 | if err != nil { 46 | return nil, err 47 | } 48 | _, clientCertPEM, clientKeyPEM, err := certGetCertificate(&certConfig{ 49 | extKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 50 | }) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | tlsConfig.tmpDir, err = os.MkdirTemp("", "") 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | dirServerCA := path.Join(tlsConfig.tmpDir, "server-ca") 61 | if err := os.Mkdir(dirServerCA, 0777); err != nil { 62 | return nil, err 63 | } 64 | pathServerCACrt := path.Join(dirServerCA, "client.crt") 65 | if err := os.WriteFile(pathServerCACrt, clientCertPEM, 0666); err != nil { 66 | return nil, err 67 | } 68 | 69 | dirClientCA := path.Join(tlsConfig.tmpDir, "client-ca") 70 | if err := os.Mkdir(dirClientCA, 0777); err != nil { 71 | return nil, err 72 | } 73 | pathClientCACrt := path.Join(dirClientCA, "server.crt") 74 | if err := os.WriteFile(pathClientCACrt, serverCertPEM, 0666); err != nil { 75 | return nil, err 76 | } 77 | 78 | dirServerCrt := path.Join(tlsConfig.tmpDir, "server-crt") 79 | if err := os.Mkdir(dirServerCrt, 0777); err != nil { 80 | return nil, err 81 | } 82 | pathServerCrtCrt := path.Join(dirServerCrt, "server.crt") 83 | if err := os.WriteFile(pathServerCrtCrt, serverCertPEM, 0666); err != nil { 84 | return nil, err 85 | } 86 | pathServerCrtKey := path.Join(dirServerCrt, "server.key") 87 | if err := os.WriteFile(pathServerCrtKey, serverKeyPEM, 0666); err != nil { 88 | return nil, err 89 | } 90 | 91 | dirClientCrt := path.Join(tlsConfig.tmpDir, "client-crt") 92 | if err := os.Mkdir(dirClientCrt, 0777); err != nil { 93 | return nil, err 94 | } 95 | pathClientCrtCrt := path.Join(dirClientCrt, "client.crt") 96 | if err := os.WriteFile(pathClientCrtCrt, clientCertPEM, 0666); err != nil { 97 | return nil, err 98 | } 99 | pathClientCrtKey := path.Join(dirClientCrt, "client.key") 100 | if err := os.WriteFile(pathClientCrtKey, clientKeyPEM, 0666); err != nil { 101 | return nil, err 102 | } 103 | 104 | // for self signed cert, it needs the server cert, for real cert, this need to be the trusted CA cert 105 | tlsConfig.serverTLS = &chserver.TLSConfig{ 106 | CA: pathServerCACrt, 107 | Cert: pathServerCrtCrt, 108 | Key: pathServerCrtKey, 109 | } 110 | tlsConfig.clientTLS = &chclient.TLSConfig{ 111 | CA: pathClientCACrt, 112 | Cert: pathClientCrtCrt, 113 | Key: pathClientCrtKey, 114 | } 115 | return tlsConfig, nil 116 | } 117 | 118 | type certConfig struct { 119 | signCA *x509.Certificate 120 | isCA bool 121 | hosts []string 122 | validFrom *time.Time 123 | validFor *time.Time 124 | extKeyUsage []x509.ExtKeyUsage 125 | rsaBits int 126 | ecdsaCurve string 127 | ed25519Key bool 128 | } 129 | 130 | func certGetCertificate(c *certConfig) (*x509.Certificate, []byte, []byte, error) { 131 | var err error 132 | var priv interface{} 133 | switch c.ecdsaCurve { 134 | case "": 135 | if c.ed25519Key { 136 | _, priv, err = ed25519.GenerateKey(rand.Reader) 137 | } else { 138 | rsaBits := c.rsaBits 139 | if rsaBits == 0 { 140 | rsaBits = 2048 141 | } 142 | priv, err = rsa.GenerateKey(rand.Reader, rsaBits) 143 | } 144 | case "P224": 145 | priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) 146 | case "P256": 147 | priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 148 | case "P384": 149 | priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 150 | case "P521": 151 | priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 152 | default: 153 | return nil, nil, nil, fmt.Errorf("Unrecognized elliptic curve: %q", c.ecdsaCurve) 154 | } 155 | if err != nil { 156 | return nil, nil, nil, fmt.Errorf("Failed to generate private key: %v", err) 157 | } 158 | 159 | // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature 160 | // KeyUsage bits set in the x509.Certificate template 161 | keyUsage := x509.KeyUsageDigitalSignature 162 | // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In 163 | // the context of TLS this KeyUsage is particular to RSA key exchange and 164 | // authentication. 165 | if _, isRSA := priv.(*rsa.PrivateKey); isRSA { 166 | keyUsage |= x509.KeyUsageKeyEncipherment 167 | } 168 | 169 | notBefore := time.Now() 170 | if c.validFrom != nil { 171 | notBefore = *c.validFrom 172 | } 173 | 174 | notAfter := time.Now().Add(24 * time.Hour) 175 | if c.validFor != nil { 176 | notAfter = *c.validFor 177 | } 178 | 179 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 180 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 181 | if err != nil { 182 | return nil, nil, nil, fmt.Errorf("Failed to generate serial number: %v", err) 183 | } 184 | 185 | cert := &x509.Certificate{ 186 | SerialNumber: serialNumber, 187 | Subject: pkix.Name{ 188 | OrganizationalUnit: []string{"test"}, 189 | Organization: []string{"Chisel"}, 190 | Country: []string{"us"}, 191 | Province: []string{"ma"}, 192 | Locality: []string{"Boston"}, 193 | CommonName: "localhost", 194 | }, 195 | NotBefore: notBefore, 196 | NotAfter: notAfter, 197 | 198 | KeyUsage: keyUsage, 199 | ExtKeyUsage: c.extKeyUsage, 200 | BasicConstraintsValid: true, 201 | } 202 | 203 | for _, h := range c.hosts { 204 | if ip := net.ParseIP(h); ip != nil { 205 | cert.IPAddresses = append(cert.IPAddresses, ip) 206 | } else { 207 | cert.DNSNames = append(cert.DNSNames, h) 208 | } 209 | } 210 | 211 | if c.isCA { 212 | cert.IsCA = true 213 | cert.KeyUsage |= x509.KeyUsageCertSign 214 | } 215 | 216 | ca := cert 217 | if c.signCA != nil { 218 | ca = c.signCA 219 | } 220 | 221 | certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, certGetPublicKey(priv), priv) 222 | if err != nil { 223 | return nil, nil, nil, fmt.Errorf("Failed to create certificate: %v", err) 224 | } 225 | 226 | certPEM := new(bytes.Buffer) 227 | pem.Encode(certPEM, &pem.Block{ 228 | Type: "CERTIFICATE", 229 | Bytes: certBytes, 230 | }) 231 | 232 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 233 | if err != nil { 234 | return nil, nil, nil, fmt.Errorf("Unable to marshal private key: %v", err) 235 | } 236 | certPrivKeyPEM := new(bytes.Buffer) 237 | pem.Encode(certPrivKeyPEM, &pem.Block{ 238 | Type: "PRIVATE KEY", 239 | Bytes: privBytes, 240 | }) 241 | 242 | return cert, certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil 243 | } 244 | 245 | func certGetPublicKey(priv interface{}) interface{} { 246 | switch k := priv.(type) { 247 | case *rsa.PrivateKey: 248 | return &k.PublicKey 249 | case *ecdsa.PrivateKey: 250 | return &k.PublicKey 251 | case ed25519.PrivateKey: 252 | return k.Public().(ed25519.PublicKey) 253 | default: 254 | return nil 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /test/e2e/proxy_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | //TODO tests for: 4 | // client -> CONNECT proxy -> server -> endpoint 5 | // client -> SOCKS proxy -> server -> endpoint 6 | -------------------------------------------------------------------------------- /test/e2e/setup_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | chclient "github.com/jpillora/chisel/client" 14 | chserver "github.com/jpillora/chisel/server" 15 | ) 16 | 17 | const debug = true 18 | 19 | // test layout configuration 20 | type testLayout struct { 21 | server *chserver.Config 22 | client *chclient.Config 23 | fileServer bool 24 | udpEcho bool 25 | udpServer bool 26 | } 27 | 28 | func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chclient.Client, teardown func()) { 29 | //start of the world 30 | // goroutines := runtime.NumGoroutine() 31 | //root cancel 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | //fileserver (fake endpoint) 34 | filePort := availablePort() 35 | if tl.fileServer { 36 | fileAddr := "127.0.0.1:" + filePort 37 | f := http.Server{ 38 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | b, _ := io.ReadAll(r.Body) 40 | w.Write(append(b, '!')) 41 | }), 42 | } 43 | fl, err := net.Listen("tcp", fileAddr) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | log.Printf("fileserver: listening on %s", fileAddr) 48 | go func() { 49 | f.Serve(fl) 50 | cancel() 51 | }() 52 | go func() { 53 | <-ctx.Done() 54 | f.Close() 55 | }() 56 | } 57 | //server 58 | server, err := chserver.NewServer(tl.server) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | server.Debug = debug 63 | port := availablePort() 64 | if err := server.StartContext(ctx, "127.0.0.1", port); err != nil { 65 | t.Fatal(err) 66 | } 67 | go func() { 68 | server.Wait() 69 | server.Infof("Closed") 70 | cancel() 71 | }() 72 | //client (with defaults) 73 | tl.client.Fingerprint = server.GetFingerprint() 74 | if tl.server.TLS.Key != "" { 75 | //the domain name has to be localhost to match the ssl cert 76 | tl.client.Server = "https://localhost:" + port 77 | } else { 78 | tl.client.Server = "http://127.0.0.1:" + port 79 | } 80 | for i, r := range tl.client.Remotes { 81 | //convert $FILEPORT into the allocated port for this test case 82 | if tl.fileServer { 83 | tl.client.Remotes[i] = strings.Replace(r, "$FILEPORT", filePort, 1) 84 | } 85 | } 86 | client, err = chclient.NewClient(tl.client) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | client.Debug = debug 91 | if err := client.Start(ctx); err != nil { 92 | t.Fatal(err) 93 | } 94 | go func() { 95 | client.Wait() 96 | client.Infof("Closed") 97 | cancel() 98 | }() 99 | //cancel context tree, and wait for both client and server to stop 100 | teardown = func() { 101 | cancel() 102 | server.Wait() 103 | client.Wait() 104 | //confirm goroutines have been cleaned up 105 | // time.Sleep(500 * time.Millisecond) 106 | // TODO remove sleep 107 | // d := runtime.NumGoroutine() - goroutines 108 | // if d != 0 { 109 | // pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 110 | // t.Fatalf("goroutines left %d", d) 111 | // } 112 | } 113 | //wait a bit... 114 | //TODO: client signal API, similar to os.Notify(signal) 115 | // wait for client setup 116 | time.Sleep(50 * time.Millisecond) 117 | //ready 118 | return server, client, teardown 119 | } 120 | 121 | func simpleSetup(t *testing.T, s *chserver.Config, c *chclient.Config) context.CancelFunc { 122 | conf := testLayout{ 123 | server: s, 124 | client: c, 125 | fileServer: true, 126 | } 127 | _, _, teardown := conf.setup(t) 128 | return teardown 129 | } 130 | 131 | func post(url, body string) (string, error) { 132 | resp, err := http.Post(url, "text/plain", strings.NewReader(body)) 133 | if err != nil { 134 | return "", err 135 | } 136 | b, err := io.ReadAll(resp.Body) 137 | if err != nil { 138 | return "", err 139 | } 140 | return string(b), nil 141 | } 142 | 143 | func availablePort() string { 144 | l, err := net.Listen("tcp", "127.0.0.1:0") 145 | if err != nil { 146 | log.Panic(err) 147 | } 148 | l.Close() 149 | _, port, err := net.SplitHostPort(l.Addr().String()) 150 | if err != nil { 151 | log.Panic(err) 152 | } 153 | return port 154 | } 155 | -------------------------------------------------------------------------------- /test/e2e/socks_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | //TODO tests for: 4 | // - SOCKS-client -> [client -> server SOCKS] -> endpoint 5 | // - SOCKS-client -> [server -> client SOCKS] -> endpoint 6 | -------------------------------------------------------------------------------- /test/e2e/tls_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | chclient "github.com/jpillora/chisel/client" 8 | chserver "github.com/jpillora/chisel/server" 9 | ) 10 | 11 | func TestTLS(t *testing.T) { 12 | tlsConfig, err := newTestTLSConfig() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer tlsConfig.Close() 17 | 18 | tmpPort := availablePort() 19 | //setup server, client, fileserver 20 | teardown := simpleSetup(t, 21 | &chserver.Config{ 22 | TLS: *tlsConfig.serverTLS, 23 | }, 24 | &chclient.Config{ 25 | Remotes: []string{tmpPort + ":$FILEPORT"}, 26 | TLS: *tlsConfig.clientTLS, 27 | Server: "https://localhost:" + tmpPort, 28 | }) 29 | defer teardown() 30 | //test remote 31 | result, err := post("http://localhost:"+tmpPort, "foo") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | if result != "foo!" { 36 | t.Fatalf("expected exclamation mark added") 37 | } 38 | } 39 | 40 | func TestMTLS(t *testing.T) { 41 | tlsConfig, err := newTestTLSConfig() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | defer tlsConfig.Close() 46 | //provide no client cert, server should reject the client request 47 | tlsConfig.serverTLS.CA = path.Dir(tlsConfig.serverTLS.CA) 48 | 49 | tmpPort := availablePort() 50 | //setup server, client, fileserver 51 | teardown := simpleSetup(t, 52 | &chserver.Config{ 53 | TLS: *tlsConfig.serverTLS, 54 | }, 55 | &chclient.Config{ 56 | Remotes: []string{tmpPort + ":$FILEPORT"}, 57 | TLS: *tlsConfig.clientTLS, 58 | Server: "https://localhost:" + tmpPort, 59 | }) 60 | defer teardown() 61 | //test remote 62 | result, err := post("http://localhost:"+tmpPort, "foo") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | if result != "foo!" { 67 | t.Fatalf("expected exclamation mark added") 68 | } 69 | } 70 | 71 | func TestTLSMissingClientCert(t *testing.T) { 72 | tlsConfig, err := newTestTLSConfig() 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | defer tlsConfig.Close() 77 | //provide no client cert, server should reject the client request 78 | tlsConfig.clientTLS.Cert = "" 79 | tlsConfig.clientTLS.Key = "" 80 | 81 | tmpPort := availablePort() 82 | //setup server, client, fileserver 83 | teardown := simpleSetup(t, 84 | &chserver.Config{ 85 | TLS: *tlsConfig.serverTLS, 86 | }, 87 | &chclient.Config{ 88 | Remotes: []string{tmpPort + ":$FILEPORT"}, 89 | TLS: *tlsConfig.clientTLS, 90 | Server: "https://localhost:" + tmpPort, 91 | }) 92 | defer teardown() 93 | //test remote 94 | _, err = post("http://localhost:"+tmpPort, "foo") 95 | if err == nil { 96 | t.Fatal(err) 97 | } 98 | } 99 | 100 | func TestTLSMissingClientCA(t *testing.T) { 101 | tlsConfig, err := newTestTLSConfig() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | defer tlsConfig.Close() 106 | //specify a CA which does not match the client cert 107 | //server should reject the client request 108 | //provide no client cert, server should reject the client request 109 | tlsConfig.serverTLS.CA = tlsConfig.clientTLS.CA 110 | 111 | tmpPort := availablePort() 112 | //setup server, client, fileserver 113 | teardown := simpleSetup(t, 114 | &chserver.Config{ 115 | TLS: *tlsConfig.serverTLS, 116 | }, 117 | &chclient.Config{ 118 | Remotes: []string{tmpPort + ":$FILEPORT"}, 119 | TLS: *tlsConfig.clientTLS, 120 | Server: "https://localhost:" + tmpPort, 121 | }) 122 | defer teardown() 123 | //test remote 124 | _, err = post("http://localhost:"+tmpPort, "foo") 125 | if err == nil { 126 | t.Fatal(err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/e2e/udp_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | chclient "github.com/jpillora/chisel/client" 10 | chserver "github.com/jpillora/chisel/server" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | func TestUDP(t *testing.T) { 15 | //listen on random udp port 16 | echoPort := availableUDPPort() 17 | a, _ := net.ResolveUDPAddr("udp", ":"+echoPort) 18 | l, err := net.ListenUDP("udp", a) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | //chisel client+server 23 | inboundPort := availableUDPPort() 24 | teardown := simpleSetup(t, 25 | &chserver.Config{}, 26 | &chclient.Config{ 27 | Remotes: []string{ 28 | inboundPort + ":" + echoPort + "/udp", 29 | }, 30 | }, 31 | ) 32 | defer teardown() 33 | //fake udp server, read and echo back duplicated, close 34 | eg := errgroup.Group{} 35 | eg.Go(func() error { 36 | defer l.Close() 37 | b := make([]byte, 128) 38 | n, a, err := l.ReadFrom(b) 39 | if err != nil { 40 | return err 41 | } 42 | if _, err = l.WriteTo(append(b[:n], b[:n]...), a); err != nil { 43 | return err 44 | } 45 | return nil 46 | }) 47 | //fake udp client 48 | conn, err := net.Dial("udp4", "localhost:"+inboundPort) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | //write bazz through the tunnel 53 | if _, err := conn.Write([]byte("bazz")); err != nil { 54 | t.Fatal(err) 55 | } 56 | //receive bazzbazz back 57 | b := make([]byte, 128) 58 | conn.SetReadDeadline(time.Now().Add(2 * time.Second)) 59 | n, err := conn.Read(b) 60 | if err != nil { 61 | t.Fatal(err) 62 | return 63 | } 64 | //udp server should close correctly 65 | if err := eg.Wait(); err != nil { 66 | t.Fatal(err) 67 | return 68 | } 69 | //ensure expected 70 | s := string(b[:n]) 71 | if s != "bazzbazz" { 72 | t.Fatalf("expected double bazz") 73 | } 74 | } 75 | 76 | func availableUDPPort() string { 77 | a, _ := net.ResolveUDPAddr("udp", ":0") 78 | l, err := net.ListenUDP("udp", a) 79 | if err != nil { 80 | log.Panicf("availability listen: %s", err) 81 | } 82 | l.Close() 83 | _, port, err := net.SplitHostPort(l.LocalAddr().String()) 84 | if err != nil { 85 | log.Panic(err) 86 | } 87 | return port 88 | } 89 | --------------------------------------------------------------------------------