[
  {
    "path": ".github/Dockerfile",
    "content": "# build stage\nFROM golang:alpine AS build\nRUN apk update && apk add git\nADD . /src\nWORKDIR /src\nENV CGO_ENABLED=0\nRUN go build \\\n    -ldflags \"-X github.com/jpillora/chisel/share.BuildVersion=$(git describe --abbrev=0 --tags)\" \\\n    -o /tmp/bin\n# run stage\nFROM scratch\nLABEL maintainer=\"dev@jpillora.com\"\nCOPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nWORKDIR /app\nCOPY --from=build /tmp/bin /app/bin\nENTRYPOINT [\"/app/bin\"]"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n\n# Dependencies listed in go.mod\n  - package-ecosystem: \"gomod\"\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"monthly\"\n"
  },
  {
    "path": ".github/goreleaser.yml",
    "content": "# test this file with\n#   goreleaser release --config goreleaser.yml --clean --snapshot\nversion: 2\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    ldflags:\n      - -s -w -X github.com/jpillora/chisel/share.BuildVersion={{.Version}}\n    flags:\n      - -trimpath\n    goos:\n      - linux\n      - darwin\n      - windows\n      - openbsd\n    goarch:\n      - \"386\"\n      - amd64\n      - arm\n      - arm64\n      - ppc64\n      - ppc64le\n      - mips\n      - mipsle\n      - mips64\n      - mips64le\n      - s390x\n    goarm:\n      - \"5\"\n      - \"6\"\n      - \"7\"\n    gomips:\n      - hardfloat\n      - softfloat\n    ignore:\n      # https://github.com/golang/go/issues/70705\n      - goos: windows\n        goarch: arm\nnfpms:\n  - maintainer: \"https://github.com/jpillora\"\n    formats:\n      - deb\n      - rpm\n      - apk\narchives:\n  - formats:\n      - gz\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n    files:\n      - none*\nrelease:\n  draft: true\n  prerelease: auto\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non:\n  pull_request: {}\n  push: {}\npermissions: write-all\njobs:\n  # ================\n  # BUILD AND TEST JOB\n  # ================\n  test:\n    name: Build & Test\n    strategy:\n      matrix:\n        go-version: [\"stable\"]\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go-version }}\n          check-latest: true\n      - name: Build\n        run: go build -v -o /dev/null .\n      - name: Test\n        run: go test -v ./...\n  # ================\n  # RELEASE BINARIES (on push \"v*\" tag)\n  # ================\n  release_binaries:\n    name: Release Binaries\n    needs: test\n    if: startsWith(github.ref, 'refs/tags/v')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n      - name: goreleaser\n        if: success()\n        uses: docker://goreleaser/goreleaser:latest\n        env:\n          GITHUB_USER: ${{ github.repository_owner }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GOTOOLCHAIN: auto\n        with:\n          args: release --config .github/goreleaser.yml\n  # ================\n  # RELEASE DOCKER IMAGES (on push \"v*\" tag)\n  # ================\n  release_docker:\n    name: Release Docker Images\n    needs: test\n    if: startsWith(github.ref, 'refs/tags/v')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code\n        uses: actions/checkout@v5\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Login to DockerHub\n        uses: docker/login-action@v3\n        with:\n          username: jpillora\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: jpillora/chisel\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: .github/Dockerfile\n          platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/386,linux/arm/v7,linux/arm/v6\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".gitignore",
    "content": "dist/\n*.swp\n.idea/\nchisel\nbin/\nrelease/\ntmp/\n*.orig\ndebug\n\n# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Jaime Pillora <dev@jpillora.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "VERSION=$(shell git describe --abbrev=0 --tags)\nBUILD=$(shell git rev-parse HEAD)\nDIRBASE=./build\nDIR=${DIRBASE}/${VERSION}/${BUILD}/bin\n\nLDFLAGS=-ldflags \"-s -w ${XBUILD} -buildid=${BUILD} -X github.com/jpillora/chisel/share.BuildVersion=${VERSION}\"\n\nGOFILES=`go list ./...`\nGOFILESNOTEST=`go list ./... | grep -v test`\n\n# Make Directory to store executables\n$(shell mkdir -p ${DIR})\n\nall:\n\t@goreleaser build --skip-validate --single-target --config .github/goreleaser.yml\n\nfreebsd: lint\n\tenv CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-freebsd_amd64 .\n\nlinux: lint\n\tenv CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-linux_amd64 .\n\nwindows: lint\n\tenv CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-windows_amd64 .\n\ndarwin:\n\tenv CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/chisel-darwin_amd64 .\n\ndocker:\n\t@docker build .\n\ndep: ## Get the dependencies\n\t@go get -u github.com/goreleaser/goreleaser\n\t@go get -u github.com/boumenot/gocover-cobertura\n\t@go get -v -d ./...\n\t@go get -u all\n\t@go mod tidy\n\nlint: ## Lint the files\n\t@go fmt ${GOFILES}\n\t@go vet ${GOFILESNOTEST}\n\ntest: ## Run unit tests\n\t@go test -coverprofile=${DIR}/coverage.out -race -short ${GOFILESNOTEST}\n\t@go tool cover -html=${DIR}/coverage.out -o ${DIR}/coverage.html\n\t@gocover-cobertura < ${DIR}/coverage.out > ${DIR}/coverage.xml\n\nrelease: lint test\n\tgoreleaser release --config .github/goreleaser.yml\n\nclean:\n\trm -rf ${DIRBASE}/*\n\n.PHONY: all freebsd linux windows docker dep lint test release clean"
  },
  {
    "path": "README.md",
    "content": "# Chisel\n\n[![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)\n\nChisel 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.\n\n![overview](https://docs.google.com/drawings/d/1p53VWxzGNfy8rjr-mW8pvisJmhkoLl82vAgctO_6f1w/pub?w=960&h=720)\n\n## Table of Contents\n\n- [Features](#features)\n- [Install](#install)\n- [Demo](#demo)\n- [Usage](#usage)\n- [Contributing](#contributing)\n- [Changelog](#changelog)\n- [License](#license)\n\n## Features\n\n- Easy to use\n- [Performant](./test/bench/perf.md)\\*\n- [Encrypted connections](#security) using the SSH protocol (via `crypto/ssh`)\n- [Authenticated connections](#authentication); authenticated client connections with a users config file, authenticated server connections with fingerprint matching.\n- Client auto-reconnects with [exponential backoff](https://github.com/jpillora/backoff)\n- Clients can create multiple tunnel endpoints over one TCP connection\n- Clients can optionally pass through SOCKS or HTTP CONNECT proxies\n- Reverse port forwarding (Connections go through the server and out the client)\n- Server optionally doubles as a [reverse proxy](http://golang.org/pkg/net/http/httputil/#NewSingleHostReverseProxy)\n- Server optionally allows [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections (See [guide below](#socks5-guide))\n- Clients optionally allow [SOCKS5](https://en.wikipedia.org/wiki/SOCKS) connections from a reversed port forward\n- Client connections over stdio which supports `ssh -o ProxyCommand` providing SSH over HTTP\n\n## Install\n\n### Binaries\n\n[![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)\n\nSee [the latest release](https://github.com/jpillora/chisel/releases/latest) or download and install it now with `curl https://i.jpillora.com/chisel! | bash`\n\n### Docker\n\n[![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)\n\n```sh\ndocker run --rm -it jpillora/chisel --help\n```\n\n### Fedora\n\nThe 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).\n\n```sh\nsudo dnf -y install chisel\n```\n\n### Source\n\n```sh\n$ go install github.com/jpillora/chisel@latest\n```\n\n## Demo\n\nA [demo app](https://chisel-demo.herokuapp.com) on Heroku is running this `chisel server`:\n\n```sh\n$ chisel server --port $PORT --proxy http://example.com\n# listens on $PORT, proxy web requests to http://example.com\n```\n\nThis 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:\n\n```sh\n$ chisel client https://chisel-demo.herokuapp.com 3000\n# connects to chisel server at https://chisel-demo.herokuapp.com,\n# tunnels your localhost:3000 to the server's localhost:3000\n```\n\nand 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).\n\n## Usage\n\n<!-- render these help texts by hand,\n  or use https://github.com/jpillora/md-tmpl\n    with $ md-tmpl -w README.md -->\n\n<!--tmpl,code=plain:echo \"$ chisel --help\" && go run main.go --help | sed 's#0.0.0-src (go1\\..*)#X.Y.Z#' -->\n``` plain \n$ chisel --help\n\n  Usage: chisel [command] [--help]\n\n  Version: X.Y.Z\n\n  Commands:\n    server - runs chisel in server mode\n    client - runs chisel in client mode\n\n  Read more:\n    https://github.com/jpillora/chisel\n\n```\n<!--/tmpl-->\n\n\n<!--tmpl,code=plain:echo \"$ chisel server --help\" && go run main.go server --help | cat | sed 's#0.0.0-src (go1\\..*)#X.Y.Z#' -->\n``` plain \n$ chisel server --help\n\n  Usage: chisel server [options]\n\n  Options:\n\n    --host, Defines the HTTP listening host – the network interface\n    (defaults the environment variable HOST and falls back to 0.0.0.0).\n\n    --port, -p, Defines the HTTP listening port (defaults to the environment\n    variable PORT and fallsback to port 8080).\n\n    --key, (deprecated use --keygen and --keyfile instead)\n    An optional string to seed the generation of a ECDSA public\n    and private key pair. All communications will be secured using this\n    key pair. Share the subsequent fingerprint with clients to enable detection\n    of man-in-the-middle attacks (defaults to the CHISEL_KEY environment\n    variable, otherwise a new key is generate each run).\n\n    --keygen, A path to write a newly generated PEM-encoded SSH private key file.\n    If users depend on your --key fingerprint, you may also include your --key to\n    output your existing key. Use - (dash) to output the generated key to stdout.\n\n    --keyfile, An optional path to a PEM-encoded SSH private key. When\n    this flag is set, the --key option is ignored, and the provided private key\n    is used to secure all communications. (defaults to the CHISEL_KEY_FILE\n    environment variable). Since ECDSA keys are short, you may also set keyfile\n    to an inline base64 private key (e.g. chisel server --keygen - | base64).\n\n    --authfile, An optional path to a users.json file. This file should\n    be an object with users defined like:\n      {\n        \"<user:pass>\": [\"<addr-regex>\",\"<addr-regex>\"]\n      }\n    when <user> connects, their <pass> will be verified and then\n    each of the remote addresses will be compared against the list\n    of address regular expressions for a match. Addresses will\n    always come in the form \"<remote-host>:<remote-port>\" for normal remotes\n    and \"R:<local-interface>:<local-port>\" for reverse port forwarding\n    remotes. This file will be automatically reloaded on change.\n\n    --auth, An optional string representing a single user with full\n    access, in the form of <user:pass>. It is equivalent to creating an\n    authfile with {\"<user:pass>\": [\"\"]}. If unset, it will use the\n    environment variable AUTH.\n\n    --keepalive, An optional keepalive interval. Since the underlying\n    transport is HTTP, in many instances we'll be traversing through\n    proxies, often these proxies will close idle connections. You must\n    specify a time with a unit, for example '5s' or '2m'. Defaults\n    to '25s' (set to 0s to disable).\n\n    --backend, Specifies another HTTP server to proxy requests to when\n    chisel receives a normal HTTP request. Useful for hiding chisel in\n    plain sight.\n\n    --socks5, Allow clients to access the internal SOCKS5 proxy. See\n    chisel client --help for more information.\n\n    --reverse, Allow clients to specify reverse port forwarding remotes\n    in addition to normal remotes.\n\n    --tls-key, Enables TLS and provides optional path to a PEM-encoded\n    TLS private key. When this flag is set, you must also set --tls-cert,\n    and you cannot set --tls-domain.\n\n    --tls-cert, Enables TLS and provides optional path to a PEM-encoded\n    TLS certificate. When this flag is set, you must also set --tls-key,\n    and you cannot set --tls-domain.\n\n    --tls-domain, Enables TLS and automatically acquires a TLS key and\n    certificate using LetsEncrypt. Setting --tls-domain requires port 443.\n    You may specify multiple --tls-domain flags to serve multiple domains.\n    The resulting files are cached in the \"$HOME/.cache/chisel\" directory.\n    You can modify this path by setting the CHISEL_LE_CACHE variable,\n    or disable caching by setting this variable to \"-\". You can optionally\n    provide a certificate notification email by setting CHISEL_LE_EMAIL.\n\n    --tls-ca, a path to a PEM encoded CA certificate bundle or a directory\n    holding multiple PEM encode CA certificate bundle files, which is used to \n    validate client connections. The provided CA certificates will be used \n    instead of the system roots. This is commonly used to implement mutual-TLS. \n\n    --pid Generate pid file in current working directory\n\n    -v, Enable verbose logging\n\n    --help, This help text\n\n  Signals:\n    The chisel process is listening for:\n      a SIGUSR2 to print process stats, and\n      a SIGHUP to short-circuit the client reconnect timer\n\n  Version:\n    X.Y.Z\n\n  Read more:\n    https://github.com/jpillora/chisel\n\n```\n<!--/tmpl-->\n\n\n<!--tmpl,code=plain:echo \"$ chisel client --help\" && go run main.go client --help | sed 's#0.0.0-src (go1\\..*)#X.Y.Z#' -->\n``` plain \n$ chisel client --help\n\n  Usage: chisel client [options] <server> <remote> [remote] [remote] ...\n\n  <server> is the URL to the chisel server.\n\n  <remote>s are remote connections tunneled through the server, each of\n  which come in the form:\n\n    <local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>\n\n    ■ local-host defaults to 0.0.0.0 (all interfaces).\n    ■ local-port defaults to remote-port.\n    ■ remote-port is required*.\n    ■ remote-host defaults to 0.0.0.0 (server localhost).\n    ■ protocol defaults to tcp.\n\n  which shares <remote-host>:<remote-port> from the server to the client\n  as <local-host>:<local-port>, or:\n\n    R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>\n\n  which does reverse port forwarding, sharing <remote-host>:<remote-port>\n  from the client to the server's <local-interface>:<local-port>.\n\n    example remotes\n\n      3000\n      example.com:3000\n      3000:google.com:80\n      192.168.0.5:3000:google.com:80\n      socks\n      5000:socks\n      R:2222:localhost:22\n      R:socks\n      R:5000:socks\n      stdio:example.com:22\n      1.1.1.1:53/udp\n\n    When the chisel server has --socks5 enabled, remotes can\n    specify \"socks\" in place of remote-host and remote-port.\n    The default local host and port for a \"socks\" remote is\n    127.0.0.1:1080. Connections to this remote will terminate\n    at the server's internal SOCKS5 proxy.\n\n    When the chisel server has --reverse enabled, remotes can\n    be prefixed with R to denote that they are reversed. That\n    is, the server will listen and accept connections, and they\n    will be proxied through the client which specified the remote.\n    Reverse remotes specifying \"R:socks\" will listen on the server's\n    default socks port (1080) and terminate the connection at the\n    client's internal SOCKS5 proxy.\n\n    When stdio is used as local-host, the tunnel will connect standard\n    input/output of this program with the remote. This is useful when \n    combined with ssh ProxyCommand. You can use\n      ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \\\n          user@example.com\n    to connect to an SSH server through the tunnel.\n\n  Options:\n\n    --fingerprint, A *strongly recommended* fingerprint string\n    to perform host-key validation against the server's public key.\n\tFingerprint mismatches will close the connection.\n\tFingerprints are generated by hashing the ECDSA public key using\n\tSHA256 and encoding the result in base64.\n\tFingerprints must be 44 characters containing a trailing equals (=).\n\n    --auth, An optional username and password (client authentication)\n    in the form: \"<user>:<pass>\". These credentials are compared to\n    the credentials inside the server's --authfile. defaults to the\n    AUTH environment variable.\n\n    --keepalive, An optional keepalive interval. Since the underlying\n    transport is HTTP, in many instances we'll be traversing through\n    proxies, often these proxies will close idle connections. You must\n    specify a time with a unit, for example '5s' or '2m'. Defaults\n    to '25s' (set to 0s to disable).\n\n    --max-retry-count, Maximum number of times to retry before exiting.\n    Defaults to unlimited.\n\n    --max-retry-interval, Maximum wait time before retrying after a\n    disconnection. Defaults to 5 minutes.\n\n    --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be\n    used to reach the chisel server. Authentication can be specified\n    inside the URL.\n    For example, http://admin:password@my-server.com:8081\n            or: socks://admin:password@my-server.com:1080\n\n    --header, Set a custom header in the form \"HeaderName: HeaderContent\".\n    Can be used multiple times. (e.g --header \"Foo: Bar\" --header \"Hello: World\")\n\n    --hostname, Optionally set the 'Host' header (defaults to the host\n    found in the server url).\n\n    --sni, Override the ServerName when using TLS (defaults to the \n    hostname).\n\n    --tls-ca, An optional root certificate bundle used to verify the\n    chisel server. Only valid when connecting to the server with\n    \"https\" or \"wss\". By default, the operating system CAs will be used.\n\n    --tls-skip-verify, Skip server TLS certificate verification of\n    chain and host name (if TLS is used for transport connections to\n    server). If set, client accepts any TLS certificate presented by\n    the server and any host name in that certificate. This only affects\n    transport https (wss) connection. Chisel server's public key\n    may be still verified (see --fingerprint) after inner connection\n    is established.\n\n    --tls-key, a path to a PEM encoded private key used for client \n    authentication (mutual-TLS).\n\n    --tls-cert, a path to a PEM encoded certificate matching the provided \n    private key. The certificate must have client authentication \n    enabled (mutual-TLS).\n\n    --pid Generate pid file in current working directory\n\n    -v, Enable verbose logging\n\n    --help, This help text\n\n  Signals:\n    The chisel process is listening for:\n      a SIGUSR2 to print process stats, and\n      a SIGHUP to short-circuit the client reconnect timer\n\n  Version:\n    X.Y.Z\n\n  Read more:\n    https://github.com/jpillora/chisel\n\n```\n<!--/tmpl-->\n\n### Security\n\nEncryption 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.\n\n### Authentication\n\nUsing 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.\n\nInternally, this is done using the _Password_ authentication method provided by SSH. Learn more about `crypto/ssh` here http://blog.gopheracademy.com/go-and-ssh/.\n\n### SOCKS5 Guide with Docker\n\n1. Print a new private key to the terminal\n\n    ```sh\n    chisel server --keygen -\n    # or save it to disk --keygen /path/to/mykey\n    ```\n\n1. Start your chisel server\n\n    ```sh\n    jpillora/chisel server --keyfile '<ck-base64 string or file path>' -p 9312 --socks5\n    ```\n\n1. Connect your chisel client (using server's fingerprint)\n\n    ```sh\n    chisel client --fingerprint '<see server output>' <server-address>:9312 socks\n    ```\n\n1. Point your SOCKS5 clients (e.g. OS/Browser) to:\n\n    ```\n    <client-address>:1080\n    ```\n\n1. Now you have an encrypted, authenticated SOCKS5 connection over HTTP\n\n\n#### Caveats\n\nSince WebSockets support is required:\n\n- 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)\n- PaaS providers vary in their support for WebSockets\n  - Heroku has full support\n  - Openshift has full support though connections are only accepted on ports 8443 and 8080\n  - Google App Engine has **no** support (Track this on [their repo](https://code.google.com/p/googleappengine/issues/detail?id=2535))\n\n## Contributing\n\n- http://golang.org/doc/code.html\n- http://golang.org/doc/effective_go.html\n- `github.com/jpillora/chisel/share` contains the shared package\n- `github.com/jpillora/chisel/server` contains the server package\n- `github.com/jpillora/chisel/client` contains the client package\n\n## Changelog\n\n- `1.0` - Initial release\n- `1.1` - Replaced simple symmetric encryption for ECDSA SSH\n- `1.2` - Added SOCKS5 (server) and HTTP CONNECT (client) support\n- `1.3` - Added reverse tunnelling support\n- `1.4` - Added arbitrary HTTP header support\n- `1.5` - Added reverse SOCKS support (by @aus)\n- `1.6` - Added client stdio support (by @BoleynSu)\n- `1.7` - Added UDP support\n- `1.8` - Move to a `scratch`Docker image\n- `1.9` - Bump to Go 1.21. Switch from `--key` seed to P256 key strings with `--key{gen,file}` (by @cmenginnz)\n- `1.10` - Bump to Go 1.22. Add `.rpm` `.deb` and `.akp` to releases. Fix bad version comparison.\n- `1.11` - Bump to Go 1.25.1. Update all dependencies.\n\n## License\n\n[MIT](https://github.com/jpillora/chisel/blob/master/LICENSE) © Jaime Pillora\n"
  },
  {
    "path": "client/client.go",
    "content": "package chclient\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\tchshare \"github.com/jpillora/chisel/share\"\n\t\"github.com/jpillora/chisel/share/ccrypto\"\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/chisel/share/tunnel\"\n\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/net/proxy\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// Config represents a client configuration\ntype Config struct {\n\tFingerprint      string\n\tAuth             string\n\tKeepAlive        time.Duration\n\tMaxRetryCount    int\n\tMaxRetryInterval time.Duration\n\tServer           string\n\tProxy            string\n\tRemotes          []string\n\tHeaders          http.Header\n\tTLS              TLSConfig\n\tDialContext      func(ctx context.Context, network, addr string) (net.Conn, error)\n\tVerbose          bool\n}\n\n// TLSConfig for a Client\ntype TLSConfig struct {\n\tSkipVerify bool\n\tCA         string\n\tCert       string\n\tKey        string\n\tServerName string\n}\n\n// Client represents a client instance\ntype Client struct {\n\t*cio.Logger\n\tconfig    *Config\n\tcomputed  settings.Config\n\tsshConfig *ssh.ClientConfig\n\ttlsConfig *tls.Config\n\tproxyURL  *url.URL\n\tserver    string\n\tconnCount cnet.ConnCount\n\tstop      func()\n\teg        *errgroup.Group\n\ttunnel    *tunnel.Tunnel\n}\n\n// NewClient creates a new client instance\nfunc NewClient(c *Config) (*Client, error) {\n\t//apply default scheme\n\tif !strings.HasPrefix(c.Server, \"http\") {\n\t\tc.Server = \"http://\" + c.Server\n\t}\n\tif c.MaxRetryInterval < time.Second {\n\t\tc.MaxRetryInterval = 5 * time.Minute\n\t}\n\tu, err := url.Parse(c.Server)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//swap to websockets scheme\n\tu.Scheme = strings.Replace(u.Scheme, \"http\", \"ws\", 1)\n\t//apply default port\n\tif !regexp.MustCompile(`:\\d+$`).MatchString(u.Host) {\n\t\tif u.Scheme == \"wss\" {\n\t\t\tu.Host = u.Host + \":443\"\n\t\t} else {\n\t\t\tu.Host = u.Host + \":80\"\n\t\t}\n\t}\n\thasReverse := false\n\thasSocks := false\n\thasStdio := false\n\tclient := &Client{\n\t\tLogger: cio.NewLogger(\"client\"),\n\t\tconfig: c,\n\t\tcomputed: settings.Config{\n\t\t\tVersion: chshare.BuildVersion,\n\t\t},\n\t\tserver:    u.String(),\n\t\ttlsConfig: nil,\n\t}\n\t//set default log level\n\tclient.Logger.Info = true\n\t//configure tls\n\tif u.Scheme == \"wss\" {\n\t\ttc := &tls.Config{}\n\t\tif c.TLS.ServerName != \"\" {\n\t\t\ttc.ServerName = c.TLS.ServerName\n\t\t}\n\t\t//certificate verification config\n\t\tif c.TLS.SkipVerify {\n\t\t\tclient.Infof(\"TLS verification disabled\")\n\t\t\ttc.InsecureSkipVerify = true\n\t\t} else if c.TLS.CA != \"\" {\n\t\t\trootCAs := x509.NewCertPool()\n\t\t\tif b, err := os.ReadFile(c.TLS.CA); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"Failed to load file: %s\", c.TLS.CA)\n\t\t\t} else if ok := rootCAs.AppendCertsFromPEM(b); !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"Failed to decode PEM: %s\", c.TLS.CA)\n\t\t\t} else {\n\t\t\t\tclient.Infof(\"TLS verification using CA %s\", c.TLS.CA)\n\t\t\t\ttc.RootCAs = rootCAs\n\t\t\t}\n\t\t}\n\t\t//provide client cert and key pair for mtls\n\t\tif c.TLS.Cert != \"\" && c.TLS.Key != \"\" {\n\t\t\tc, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"Error loading client cert and key pair: %v\", err)\n\t\t\t}\n\t\t\ttc.Certificates = []tls.Certificate{c}\n\t\t} else if c.TLS.Cert != \"\" || c.TLS.Key != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"Please specify client BOTH cert and key\")\n\t\t}\n\t\tclient.tlsConfig = tc\n\t}\n\t//validate remotes\n\tfor _, s := range c.Remotes {\n\t\tr, err := settings.DecodeRemote(s)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Failed to decode remote '%s': %s\", s, err)\n\t\t}\n\t\tif r.Socks {\n\t\t\thasSocks = true\n\t\t}\n\t\tif r.Reverse {\n\t\t\thasReverse = true\n\t\t}\n\t\tif r.Stdio {\n\t\t\tif hasStdio {\n\t\t\t\treturn nil, errors.New(\"Only one stdio is allowed\")\n\t\t\t}\n\t\t\thasStdio = true\n\t\t}\n\t\t//confirm non-reverse tunnel is available\n\t\tif !r.Reverse && !r.Stdio && !r.CanListen() {\n\t\t\treturn nil, fmt.Errorf(\"Client cannot listen on %s\", r.String())\n\t\t}\n\t\tclient.computed.Remotes = append(client.computed.Remotes, r)\n\t}\n\t//outbound proxy\n\tif p := c.Proxy; p != \"\" {\n\t\tclient.proxyURL, err = url.Parse(p)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Invalid proxy URL (%s)\", err)\n\t\t}\n\t}\n\t//ssh auth and config\n\tuser, pass := settings.ParseAuth(c.Auth)\n\tclient.sshConfig = &ssh.ClientConfig{\n\t\tUser:            user,\n\t\tAuth:            []ssh.AuthMethod{ssh.Password(pass)},\n\t\tClientVersion:   \"SSH-\" + chshare.ProtocolVersion + \"-client\",\n\t\tHostKeyCallback: client.verifyServer,\n\t\tTimeout:         settings.EnvDuration(\"SSH_TIMEOUT\", 30*time.Second),\n\t}\n\t//prepare client tunnel\n\tclient.tunnel = tunnel.New(tunnel.Config{\n\t\tLogger:    client.Logger,\n\t\tInbound:   true, //client always accepts inbound\n\t\tOutbound:  hasReverse,\n\t\tSocks:     hasReverse && hasSocks,\n\t\tKeepAlive: client.config.KeepAlive,\n\t})\n\treturn client, nil\n}\n\n// Run starts client and blocks while connected\nfunc (c *Client) Run() error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tif err := c.Start(ctx); err != nil {\n\t\treturn err\n\t}\n\treturn c.Wait()\n}\n\nfunc (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error {\n\texpect := c.config.Fingerprint\n\tif expect == \"\" {\n\t\treturn nil\n\t}\n\tgot := ccrypto.FingerprintKey(key)\n\t_, err := base64.StdEncoding.DecodeString(expect)\n\tif _, ok := err.(base64.CorruptInputError); ok {\n\t\tc.Logger.Infof(\"Specified deprecated MD5 fingerprint (%s), please update to the new SHA256 fingerprint: %s\", expect, got)\n\t\treturn c.verifyLegacyFingerprint(key)\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"Error decoding fingerprint: %w\", err)\n\t}\n\tif got != expect {\n\t\treturn fmt.Errorf(\"Invalid fingerprint (%s)\", got)\n\t}\n\t//overwrite with complete fingerprint\n\tc.Infof(\"Fingerprint %s\", got)\n\treturn nil\n}\n\n// verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints\nfunc (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error {\n\tbytes := md5.Sum(key.Marshal())\n\tstrbytes := make([]string, len(bytes))\n\tfor i, b := range bytes {\n\t\tstrbytes[i] = fmt.Sprintf(\"%02x\", b)\n\t}\n\tgot := strings.Join(strbytes, \":\")\n\texpect := c.config.Fingerprint\n\tif !strings.HasPrefix(got, expect) {\n\t\treturn fmt.Errorf(\"Invalid fingerprint (%s)\", got)\n\t}\n\treturn nil\n}\n\n// Start client and does not block\nfunc (c *Client) Start(ctx context.Context) error {\n\tctx, cancel := context.WithCancel(ctx)\n\tc.stop = cancel\n\teg, ctx := errgroup.WithContext(ctx)\n\tc.eg = eg\n\tvia := \"\"\n\tif c.proxyURL != nil {\n\t\tvia = \" via \" + c.proxyURL.String()\n\t}\n\tc.Infof(\"Connecting to %s%s\\n\", c.server, via)\n\t//connect to chisel server\n\teg.Go(func() error {\n\t\treturn c.connectionLoop(ctx)\n\t})\n\t//listen sockets\n\teg.Go(func() error {\n\t\tclientInbound := c.computed.Remotes.Reversed(false)\n\t\tif len(clientInbound) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\treturn c.tunnel.BindRemotes(ctx, clientInbound)\n\t})\n\treturn nil\n}\n\nfunc (c *Client) setProxy(u *url.URL, d *websocket.Dialer) error {\n\t// CONNECT proxy\n\tif !strings.HasPrefix(u.Scheme, \"socks\") {\n\t\td.Proxy = func(*http.Request) (*url.URL, error) {\n\t\t\treturn u, nil\n\t\t}\n\t\treturn nil\n\t}\n\t// SOCKS5 proxy\n\tif u.Scheme != \"socks\" && u.Scheme != \"socks5h\" {\n\t\treturn fmt.Errorf(\n\t\t\t\"unsupported socks proxy type: %s:// (only socks5h:// or socks:// is supported)\",\n\t\t\tu.Scheme,\n\t\t)\n\t}\n\tvar auth *proxy.Auth\n\tif u.User != nil {\n\t\tpass, _ := u.User.Password()\n\t\tauth = &proxy.Auth{\n\t\t\tUser:     u.User.Username(),\n\t\t\tPassword: pass,\n\t\t}\n\t}\n\tsocksDialer, err := proxy.SOCKS5(\"tcp\", u.Host, auth, proxy.Direct)\n\tif err != nil {\n\t\treturn err\n\t}\n\td.NetDial = socksDialer.Dial\n\treturn nil\n}\n\n// Wait blocks while the client is running.\nfunc (c *Client) Wait() error {\n\treturn c.eg.Wait()\n}\n\n// Close manually stops the client\nfunc (c *Client) Close() error {\n\tif c.stop != nil {\n\t\tc.stop()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "client/client_connect.go",
    "content": "package chclient\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/jpillora/backoff\"\n\tchshare \"github.com/jpillora/chisel/share\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/cos\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc (c *Client) connectionLoop(ctx context.Context) error {\n\t//connection loop!\n\tb := &backoff.Backoff{Max: c.config.MaxRetryInterval}\n\tfor {\n\t\tconnected, err := c.connectionOnce(ctx)\n\t\t//reset backoff after successful connections\n\t\tif connected {\n\t\t\tb.Reset()\n\t\t}\n\t\t//connection error\n\t\tattempt := int(b.Attempt())\n\t\tmaxAttempt := c.config.MaxRetryCount\n\t\t//dont print closed-connection errors\n\t\tif strings.HasSuffix(err.Error(), \"use of closed network connection\") {\n\t\t\terr = io.EOF\n\t\t}\n\t\t//show error message and attempt counts (excluding disconnects)\n\t\tif err != nil && err != io.EOF {\n\t\t\tmsg := fmt.Sprintf(\"Connection error: %s\", err)\n\t\t\tif attempt > 0 {\n\t\t\t\tmaxAttemptVal := fmt.Sprint(maxAttempt)\n\t\t\t\tif maxAttempt < 0 {\n\t\t\t\t\tmaxAttemptVal = \"unlimited\"\n\t\t\t\t}\n\t\t\t\tmsg += fmt.Sprintf(\" (Attempt: %d/%s)\", attempt, maxAttemptVal)\n\t\t\t}\n\t\t\tc.Infof(msg)\n\t\t}\n\t\t//give up?\n\t\tif maxAttempt >= 0 && attempt >= maxAttempt {\n\t\t\tc.Infof(\"Give up\")\n\t\t\tbreak\n\t\t}\n\t\td := b.Duration()\n\t\tc.Infof(\"Retrying in %s...\", d)\n\t\tselect {\n\t\tcase <-cos.AfterSignal(d):\n\t\t\tcontinue //retry now\n\t\tcase <-ctx.Done():\n\t\t\tc.Infof(\"Cancelled\")\n\t\t\treturn nil\n\t\t}\n\t}\n\tc.Close()\n\treturn nil\n}\n\n// connectionOnce connects to the chisel server and blocks\nfunc (c *Client) connectionOnce(ctx context.Context) (connected bool, err error) {\n\t//already closed?\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn false, errors.New(\"Cancelled\")\n\tdefault:\n\t\t//still open\n\t}\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\t//prepare dialer\n\td := websocket.Dialer{\n\t\tHandshakeTimeout: settings.EnvDuration(\"WS_TIMEOUT\", 45*time.Second),\n\t\tSubprotocols:     []string{chshare.ProtocolVersion},\n\t\tTLSClientConfig:  c.tlsConfig,\n\t\tReadBufferSize:   settings.EnvInt(\"WS_BUFF_SIZE\", 0),\n\t\tWriteBufferSize:  settings.EnvInt(\"WS_BUFF_SIZE\", 0),\n\t\tNetDialContext:   c.config.DialContext,\n\t}\n\t//optional proxy\n\tif p := c.proxyURL; p != nil {\n\t\tif err := c.setProxy(p, &d); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\twsConn, _, err := d.DialContext(ctx, c.server, c.config.Headers)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tconn := cnet.NewWebSocketConn(wsConn)\n\t// perform SSH handshake on net.Conn\n\tc.Debugf(\"Handshaking...\")\n\tsshConn, chans, reqs, err := ssh.NewClientConn(conn, \"\", c.sshConfig)\n\tif err != nil {\n\t\te := err.Error()\n\t\tif strings.Contains(e, \"unable to authenticate\") {\n\t\t\tc.Infof(\"Authentication failed\")\n\t\t\tc.Debugf(e)\n\t\t} else {\n\t\t\tc.Infof(e)\n\t\t}\n\t\treturn false, err\n\t}\n\tdefer sshConn.Close()\n\t// chisel client handshake (reverse of server handshake)\n\t// send configuration\n\tc.Debugf(\"Sending config\")\n\tt0 := time.Now()\n\t_, configerr, err := sshConn.SendRequest(\n\t\t\"config\",\n\t\ttrue,\n\t\tsettings.EncodeConfig(c.computed),\n\t)\n\tif err != nil {\n\t\tc.Infof(\"Config verification failed\")\n\t\treturn false, err\n\t}\n\tif len(configerr) > 0 {\n\t\treturn false, errors.New(string(configerr))\n\t}\n\tc.Infof(\"Connected (Latency %s)\", time.Since(t0))\n\t//connected, handover ssh connection for tunnel to use, and block\n\terr = c.tunnel.BindSSH(ctx, sshConn, reqs, chans)\n\tc.Infof(\"Disconnected\")\n\tconnected = time.Since(t0) > 5*time.Second\n\treturn connected, err\n}\n"
  },
  {
    "path": "client/client_test.go",
    "content": "package chclient\n\nimport (\n\t\"crypto/elliptic\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/jpillora/chisel/share/ccrypto\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestCustomHeaders(t *testing.T) {\n\t//fake server\n\twg := sync.WaitGroup{}\n\twg.Add(1)\n\tserver := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {\n\t\tif req.Header.Get(\"Foo\") != \"Bar\" {\n\t\t\tt.Fatal(\"expected header Foo to be 'Bar'\")\n\t\t}\n\t\twg.Done()\n\t}))\n\tdefer server.Close()\n\t//client\n\theaders := http.Header{}\n\theaders.Set(\"Foo\", \"Bar\")\n\tconfig := Config{\n\t\tKeepAlive:        time.Second,\n\t\tMaxRetryInterval: time.Second,\n\t\tServer:           server.URL,\n\t\tRemotes:          []string{\"9000\"},\n\t\tHeaders:          headers,\n\t}\n\tc, err := NewClient(&config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tgo c.Run()\n\t//wait for test to complete\n\twg.Wait()\n\tc.Close()\n}\n\nfunc TestFallbackLegacyFingerprint(t *testing.T) {\n\tconfig := Config{\n\t\tFingerprint: \"a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44\",\n\t}\n\tc, err := NewClient(&config)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr := ccrypto.NewDetermRand([]byte(\"test123\"))\n\tpriv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpub, err := ssh.NewPublicKey(&priv.PublicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = c.verifyServer(\"\", nil, pub)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestVerifyLegacyFingerprint(t *testing.T) {\n\tconfig := Config{\n\t\tFingerprint: \"a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44\",\n\t}\n\tc, err := NewClient(&config)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr := ccrypto.NewDetermRand([]byte(\"test123\"))\n\tpriv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpub, err := ssh.NewPublicKey(&priv.PublicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = c.verifyLegacyFingerprint(pub)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestVerifyFingerprint(t *testing.T) {\n\tconfig := Config{\n\t\tFingerprint: \"qmrRoo8MIqePv3jC8+wv49gU6uaFgD3FASQx9V8KdmY=\",\n\t}\n\tc, err := NewClient(&config)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr := ccrypto.NewDetermRand([]byte(\"test123\"))\n\tpriv, err := ccrypto.GenerateKeyGo119(elliptic.P256(), r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpub, err := ssh.NewPublicKey(&priv.PublicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = c.verifyServer(\"\", nil, pub)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "example/Flyfile",
    "content": "FROM jpillora/chisel\nENTRYPOINT [\"/app/bin\", \"server\", \"--port\", \"443\", \"--tls-domain\", \"chisel.jpillora.com\"]"
  },
  {
    "path": "example/fly.toml",
    "content": "app = \"jp-chisel\"\nkill_signal = \"SIGINT\"\nkill_timeout = 5\nprocesses = []\n\n[build]\n  dockerfile = \"Flyfile\"\n\n[[services]]\n  internal_port = 443\n  protocol = \"tcp\"\n  [[services.ports]]\n    port = \"443\""
  },
  {
    "path": "example/reverse-tunneling-authenticated.md",
    "content": "# Reverse Tunneling\n\n> **Use Case**: Host a website on your Raspberry Pi without opening ports on your router.\n\nThis 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.\n\n## Chisel CLI\n\n### Server\n\nSetup a relay server on the VPS to bounce down TCP traffic on port 80:\n\n```bash\n#!/bin/bash\n\n# ⬇️ Start Chisel server in Reverse mode\nchisel server --reverse \\\n\n# ⬇️ Use the include users.json as an authfile\n--authfile=\"./users.json\" \\\n```\n\nThe corresponding `authfile` might look like this:\n\n```json\n{\n  \"foo:bar\": [\"0.0.0.0:80\"]\n}\n```\n\n### Client\n\nSetup a chisel client to receive bounced-down traffic and forward it to the webserver running on the Pi:\n\n```bash\n#!/bin/bash\n\nchisel client \\\n\n# ⬇️ Authenticates user \"foo\" with password \"bar\"\n--auth=\"foo:bar\" \\\n\n# ⬇️ Connects to chisel relay server example.com\n# listening on the default (\"fallback\") port, 8080\nexample.com \\\n\n# ⬇️ Reverse tunnels port 80 on the relay server to\n# port 80 on your Pi.\nR:80:localhost:80\n```\n\n---\n\n## Chisel Container\n\nThis guide makes use of Docker and Docker compose to accomplish the same task as the above guide.\n### Server\n\nSetup a relay server on the VPS to bounce down TCP traffic on port 80:\n\n```yaml\nversion: '3'\n\nservices:\n  chisel:\n    image: jpillora/chisel\n    restart: unless-stopped\n    container_name: chisel\n    # ⬇️ Pass CLI arguments one at a time in an array, as required by Docker compose.\n    command:\n      - 'server'\n      # ⬇️ Use the --key=value syntax, since Docker compose doesn't parse whitespace well.\n      - '--authfile=/users.json'\n      - '--reverse'\n    # ⬇️ Mount the authfile as a Docker volume\n    volumes:\n      - './users.json:/users.json'\n    # ⬇️ Give the container unrestricted access to the Docker host's network\n    network_mode: host\n```\n\nThe `authfile` (`users.json`) remains the same as in the non-containerized version - shown again with the username `foo` and password `bar`.\n\n```json\n{\n  \"foo:bar\": [\"0.0.0.0:80\"]\n}\n```\n\n### Client\n\nSetup an instance of the Chisel client on the Pi to receive relayed TCP traffic and feed it to the web server:\n\n```yaml\nversion: '3'\n\nservices:\n  chisel:\n    # ⬇️ Delay starting Chisel server until the web server container is started.\n    depends_on:\n      - webserver\n    image: jpillora/chisel\n    restart: unless-stopped\n    container_name: 'chisel'\n    command:\n      - 'client'\n      # ⬇️ Use username `foo` and password `bar` to authenticate with Chisel server.\n      - '--auth=foo:bar'\n      # ⬇️ Domain & port of Chisel server. Port defaults to 8080 on server, but must be manually set on client.\n      - 'proxy.example.com:8080'\n      # ⬇️ Reverse tunnel traffic from the chisel server to the web server container, identified in Docker using DNS by its service name `webserver`.\n      - 'R:80:webserver:80'\n    networks:\n      - internal\n  # ⬇️ Basic Nginx webserver for demo purposes.\n  webserver:\n    image: nginx\n    restart: unless-stopped\n    container_name: nginx\n    networks:\n      - internal\n\n# ⬇️ Make use of a Docker network called `internal`.\nnetworks:\n  internal:\n```\n"
  },
  {
    "path": "example/users.json",
    "content": "{\n\t\"root:toor\": [\n\t\t\"\"\n\t],\n\t\"foo:bar\": [\n\t\t\"^0.0.0.0:3000$\"\n\t],\n\t\"ping:pong\": [\n\t\t\"^0.0.0.0:[45]000$\",\n\t\t\"^example.com:80$\",\n\t\t\"^R:0.0.0.0:7000$\"\n\t]\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/jpillora/chisel\n\ngo 1.25\n\ntoolchain go1.25.7\n\nrequire (\n\tgithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/jpillora/backoff v1.0.0\n\tgithub.com/jpillora/requestlog v1.0.0\n\tgithub.com/jpillora/sizestr v1.0.0\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/net v0.50.0\n\tgolang.org/x/sync v0.19.0\n)\n\nrequire (\n\tgithub.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect\n\tgithub.com/jpillora/ansi v1.0.3 // indirect\n\tgithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=\ngithub.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/jpillora/ansi v1.0.3 h1:nn4Jzti0EmRfDxm7JtEs5LzCbNwd5sv+0aE+LdS9/ZQ=\ngithub.com/jpillora/ansi v1.0.3/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY=\ngithub.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA=\ngithub.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8=\ngithub.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw=\ngithub.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0=\ngithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=\ngithub.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n\tchshare \"github.com/jpillora/chisel/share\"\n\t\"github.com/jpillora/chisel/share/ccrypto\"\n\t\"github.com/jpillora/chisel/share/cos\"\n\t\"github.com/jpillora/chisel/share/settings\"\n)\n\nvar help = `\n  Usage: chisel [command] [--help]\n\n  Version: ` + chshare.BuildVersion + ` (` + runtime.Version() + `)\n\n  Commands:\n    server - runs chisel in server mode\n    client - runs chisel in client mode\n\n  Read more:\n    https://github.com/jpillora/chisel\n\n`\n\nfunc main() {\n\n\tversion := flag.Bool(\"version\", false, \"\")\n\tv := flag.Bool(\"v\", false, \"\")\n\tflag.Bool(\"help\", false, \"\")\n\tflag.Bool(\"h\", false, \"\")\n\tflag.Usage = func() {}\n\tflag.Parse()\n\n\tif *version || *v {\n\t\tfmt.Println(chshare.BuildVersion)\n\t\tos.Exit(0)\n\t}\n\n\targs := flag.Args()\n\n\tsubcmd := \"\"\n\tif len(args) > 0 {\n\t\tsubcmd = args[0]\n\t\targs = args[1:]\n\t}\n\n\tswitch subcmd {\n\tcase \"server\":\n\t\tserver(args)\n\tcase \"client\":\n\t\tclient(args)\n\tdefault:\n\t\tfmt.Print(help)\n\t\tos.Exit(0)\n\t}\n}\n\nvar commonHelp = `\n    --pid Generate pid file in current working directory\n\n    -v, Enable verbose logging\n\n    --help, This help text\n\n  Signals:\n    The chisel process is listening for:\n      a SIGUSR2 to print process stats, and\n      a SIGHUP to short-circuit the client reconnect timer\n\n  Version:\n    ` + chshare.BuildVersion + ` (` + runtime.Version() + `)\n\n  Read more:\n    https://github.com/jpillora/chisel\n\n`\n\nfunc generatePidFile() {\n\tpid := []byte(strconv.Itoa(os.Getpid()))\n\tif err := os.WriteFile(\"chisel.pid\", pid, 0644); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nvar serverHelp = `\n  Usage: chisel server [options]\n\n  Options:\n\n    --host, Defines the HTTP listening host – the network interface\n    (defaults the environment variable HOST and falls back to 0.0.0.0).\n\n    --port, -p, Defines the HTTP listening port (defaults to the environment\n    variable PORT and fallsback to port 8080).\n\n    --key, (deprecated use --keygen and --keyfile instead)\n    An optional string to seed the generation of a ECDSA public\n    and private key pair. All communications will be secured using this\n    key pair. Share the subsequent fingerprint with clients to enable detection\n    of man-in-the-middle attacks (defaults to the CHISEL_KEY environment\n    variable, otherwise a new key is generate each run).\n\n    --keygen, A path to write a newly generated PEM-encoded SSH private key file.\n    If users depend on your --key fingerprint, you may also include your --key to\n    output your existing key. Use - (dash) to output the generated key to stdout.\n\n    --keyfile, An optional path to a PEM-encoded SSH private key. When\n    this flag is set, the --key option is ignored, and the provided private key\n    is used to secure all communications. (defaults to the CHISEL_KEY_FILE\n    environment variable). Since ECDSA keys are short, you may also set keyfile\n    to an inline base64 private key (e.g. chisel server --keygen - | base64).\n\n    --authfile, An optional path to a users.json file. This file should\n    be an object with users defined like:\n      {\n        \"<user:pass>\": [\"<addr-regex>\",\"<addr-regex>\"]\n      }\n    when <user> connects, their <pass> will be verified and then\n    each of the remote addresses will be compared against the list\n    of address regular expressions for a match. Addresses will\n    always come in the form \"<remote-host>:<remote-port>\" for normal remotes\n    and \"R:<local-interface>:<local-port>\" for reverse port forwarding\n    remotes. This file will be automatically reloaded on change.\n\n    --auth, An optional string representing a single user with full\n    access, in the form of <user:pass>. It is equivalent to creating an\n    authfile with {\"<user:pass>\": [\"\"]}. If unset, it will use the\n    environment variable AUTH.\n\n    --keepalive, An optional keepalive interval. Since the underlying\n    transport is HTTP, in many instances we'll be traversing through\n    proxies, often these proxies will close idle connections. You must\n    specify a time with a unit, for example '5s' or '2m'. Defaults\n    to '25s' (set to 0s to disable).\n\n    --backend, Specifies another HTTP server to proxy requests to when\n    chisel receives a normal HTTP request. Useful for hiding chisel in\n    plain sight.\n\n    --socks5, Allow clients to access the internal SOCKS5 proxy. See\n    chisel client --help for more information.\n\n    --reverse, Allow clients to specify reverse port forwarding remotes\n    in addition to normal remotes.\n\n    --tls-key, Enables TLS and provides optional path to a PEM-encoded\n    TLS private key. When this flag is set, you must also set --tls-cert,\n    and you cannot set --tls-domain.\n\n    --tls-cert, Enables TLS and provides optional path to a PEM-encoded\n    TLS certificate. When this flag is set, you must also set --tls-key,\n    and you cannot set --tls-domain.\n\n    --tls-domain, Enables TLS and automatically acquires a TLS key and\n    certificate using LetsEncrypt. Setting --tls-domain requires port 443.\n    You may specify multiple --tls-domain flags to serve multiple domains.\n    The resulting files are cached in the \"$HOME/.cache/chisel\" directory.\n    You can modify this path by setting the CHISEL_LE_CACHE variable,\n    or disable caching by setting this variable to \"-\". You can optionally\n    provide a certificate notification email by setting CHISEL_LE_EMAIL.\n\n    --tls-ca, a path to a PEM encoded CA certificate bundle or a directory\n    holding multiple PEM encode CA certificate bundle files, which is used to \n    validate client connections. The provided CA certificates will be used \n    instead of the system roots. This is commonly used to implement mutual-TLS. \n` + commonHelp\n\nfunc server(args []string) {\n\n\tflags := flag.NewFlagSet(\"server\", flag.ContinueOnError)\n\n\tconfig := &chserver.Config{}\n\tflags.StringVar(&config.KeySeed, \"key\", \"\", \"\")\n\tflags.StringVar(&config.KeyFile, \"keyfile\", \"\", \"\")\n\tflags.StringVar(&config.AuthFile, \"authfile\", \"\", \"\")\n\tflags.StringVar(&config.Auth, \"auth\", \"\", \"\")\n\tflags.DurationVar(&config.KeepAlive, \"keepalive\", 25*time.Second, \"\")\n\tflags.StringVar(&config.Proxy, \"proxy\", \"\", \"\")\n\tflags.StringVar(&config.Proxy, \"backend\", \"\", \"\")\n\tflags.BoolVar(&config.Socks5, \"socks5\", false, \"\")\n\tflags.BoolVar(&config.Reverse, \"reverse\", false, \"\")\n\tflags.StringVar(&config.TLS.Key, \"tls-key\", \"\", \"\")\n\tflags.StringVar(&config.TLS.Cert, \"tls-cert\", \"\", \"\")\n\tflags.Var(multiFlag{&config.TLS.Domains}, \"tls-domain\", \"\")\n\tflags.StringVar(&config.TLS.CA, \"tls-ca\", \"\", \"\")\n\n\thost := flags.String(\"host\", \"\", \"\")\n\tp := flags.String(\"p\", \"\", \"\")\n\tport := flags.String(\"port\", \"\", \"\")\n\tpid := flags.Bool(\"pid\", false, \"\")\n\tverbose := flags.Bool(\"v\", false, \"\")\n\tkeyGen := flags.String(\"keygen\", \"\", \"\")\n\n\tflags.Usage = func() {\n\t\tfmt.Print(serverHelp)\n\t\tos.Exit(0)\n\t}\n\tflags.Parse(args)\n\n\tif *keyGen != \"\" {\n\t\tif err := ccrypto.GenerateKeyFile(*keyGen, config.KeySeed); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\treturn\n\t}\n\n\tif config.KeySeed != \"\" {\n\t\tlog.Print(\"Option `--key` is deprecated and will be removed in a future version of chisel.\")\n\t\tlog.Print(\"Please use `chisel server --keygen /file/path`, followed by `chisel server --keyfile /file/path` to specify the SSH private key\")\n\t}\n\n\tif *host == \"\" {\n\t\t*host = os.Getenv(\"HOST\")\n\t}\n\tif *host == \"\" {\n\t\t*host = \"0.0.0.0\"\n\t}\n\tif *port == \"\" {\n\t\t*port = *p\n\t}\n\tif *port == \"\" {\n\t\t*port = os.Getenv(\"PORT\")\n\t}\n\tif *port == \"\" {\n\t\t*port = \"8080\"\n\t}\n\tif config.KeyFile == \"\" {\n\t\tconfig.KeyFile = settings.Env(\"KEY_FILE\")\n\t}\n\tif config.KeySeed == \"\" {\n\t\tconfig.KeySeed = settings.Env(\"KEY\")\n\t}\n\tif config.Auth == \"\" {\n\t\tconfig.Auth = os.Getenv(\"AUTH\")\n\t}\n\ts, err := chserver.NewServer(config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\ts.Debug = *verbose\n\tif *pid {\n\t\tgeneratePidFile()\n\t}\n\tgo cos.GoStats()\n\tctx := cos.InterruptContext()\n\tif err := s.StartContext(ctx, *host, *port); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := s.Wait(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\ntype multiFlag struct {\n\tvalues *[]string\n}\n\nfunc (flag multiFlag) String() string {\n\treturn strings.Join(*flag.values, \", \")\n}\n\nfunc (flag multiFlag) Set(arg string) error {\n\t*flag.values = append(*flag.values, arg)\n\treturn nil\n}\n\ntype headerFlags struct {\n\thttp.Header\n}\n\nfunc (flag *headerFlags) String() string {\n\tout := \"\"\n\tfor k, v := range flag.Header {\n\t\tout += fmt.Sprintf(\"%s: %s\\n\", k, v)\n\t}\n\treturn out\n}\n\nfunc (flag *headerFlags) Set(arg string) error {\n\tindex := strings.Index(arg, \":\")\n\tif index < 0 {\n\t\treturn fmt.Errorf(`Invalid header (%s). Should be in the format \"HeaderName: HeaderContent\"`, arg)\n\t}\n\tif flag.Header == nil {\n\t\tflag.Header = http.Header{}\n\t}\n\tkey := arg[0:index]\n\tvalue := arg[index+1:]\n\tflag.Header.Set(key, strings.TrimSpace(value))\n\treturn nil\n}\n\nvar clientHelp = `\n  Usage: chisel client [options] <server> <remote> [remote] [remote] ...\n\n  <server> is the URL to the chisel server.\n\n  <remote>s are remote connections tunneled through the server, each of\n  which come in the form:\n\n    <local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>\n\n    ■ local-host defaults to 0.0.0.0 (all interfaces).\n    ■ local-port defaults to remote-port.\n    ■ remote-port is required*.\n    ■ remote-host defaults to 0.0.0.0 (server localhost).\n    ■ protocol defaults to tcp.\n\n  which shares <remote-host>:<remote-port> from the server to the client\n  as <local-host>:<local-port>, or:\n\n    R:<local-interface>:<local-port>:<remote-host>:<remote-port>/<protocol>\n\n  which does reverse port forwarding, sharing <remote-host>:<remote-port>\n  from the client to the server's <local-interface>:<local-port>.\n\n    example remotes\n\n      3000\n      example.com:3000\n      3000:google.com:80\n      192.168.0.5:3000:google.com:80\n      socks\n      5000:socks\n      R:2222:localhost:22\n      R:socks\n      R:5000:socks\n      stdio:example.com:22\n      1.1.1.1:53/udp\n\n    When the chisel server has --socks5 enabled, remotes can\n    specify \"socks\" in place of remote-host and remote-port.\n    The default local host and port for a \"socks\" remote is\n    127.0.0.1:1080. Connections to this remote will terminate\n    at the server's internal SOCKS5 proxy.\n\n    When the chisel server has --reverse enabled, remotes can\n    be prefixed with R to denote that they are reversed. That\n    is, the server will listen and accept connections, and they\n    will be proxied through the client which specified the remote.\n    Reverse remotes specifying \"R:socks\" will listen on the server's\n    default socks port (1080) and terminate the connection at the\n    client's internal SOCKS5 proxy.\n\n    When stdio is used as local-host, the tunnel will connect standard\n    input/output of this program with the remote. This is useful when \n    combined with ssh ProxyCommand. You can use\n      ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \\\n          user@example.com\n    to connect to an SSH server through the tunnel.\n\n  Options:\n\n    --fingerprint, A *strongly recommended* fingerprint string\n    to perform host-key validation against the server's public key.\n\tFingerprint mismatches will close the connection.\n\tFingerprints are generated by hashing the ECDSA public key using\n\tSHA256 and encoding the result in base64.\n\tFingerprints must be 44 characters containing a trailing equals (=).\n\n    --auth, An optional username and password (client authentication)\n    in the form: \"<user>:<pass>\". These credentials are compared to\n    the credentials inside the server's --authfile. defaults to the\n    AUTH environment variable.\n\n    --keepalive, An optional keepalive interval. Since the underlying\n    transport is HTTP, in many instances we'll be traversing through\n    proxies, often these proxies will close idle connections. You must\n    specify a time with a unit, for example '5s' or '2m'. Defaults\n    to '25s' (set to 0s to disable).\n\n    --max-retry-count, Maximum number of times to retry before exiting.\n    Defaults to unlimited.\n\n    --max-retry-interval, Maximum wait time before retrying after a\n    disconnection. Defaults to 5 minutes.\n\n    --proxy, An optional HTTP CONNECT or SOCKS5 proxy which will be\n    used to reach the chisel server. Authentication can be specified\n    inside the URL.\n    For example, http://admin:password@my-server.com:8081\n            or: socks://admin:password@my-server.com:1080\n\n    --header, Set a custom header in the form \"HeaderName: HeaderContent\".\n    Can be used multiple times. (e.g --header \"Foo: Bar\" --header \"Hello: World\")\n\n    --hostname, Optionally set the 'Host' header (defaults to the host\n    found in the server url).\n\n    --sni, Override the ServerName when using TLS (defaults to the \n    hostname).\n\n    --tls-ca, An optional root certificate bundle used to verify the\n    chisel server. Only valid when connecting to the server with\n    \"https\" or \"wss\". By default, the operating system CAs will be used.\n\n    --tls-skip-verify, Skip server TLS certificate verification of\n    chain and host name (if TLS is used for transport connections to\n    server). If set, client accepts any TLS certificate presented by\n    the server and any host name in that certificate. This only affects\n    transport https (wss) connection. Chisel server's public key\n    may be still verified (see --fingerprint) after inner connection\n    is established.\n\n    --tls-key, a path to a PEM encoded private key used for client \n    authentication (mutual-TLS).\n\n    --tls-cert, a path to a PEM encoded certificate matching the provided \n    private key. The certificate must have client authentication \n    enabled (mutual-TLS).\n` + commonHelp\n\nfunc client(args []string) {\n\tflags := flag.NewFlagSet(\"client\", flag.ContinueOnError)\n\tconfig := chclient.Config{Headers: http.Header{}}\n\tflags.StringVar(&config.Fingerprint, \"fingerprint\", \"\", \"\")\n\tflags.StringVar(&config.Auth, \"auth\", \"\", \"\")\n\tflags.DurationVar(&config.KeepAlive, \"keepalive\", 25*time.Second, \"\")\n\tflags.IntVar(&config.MaxRetryCount, \"max-retry-count\", -1, \"\")\n\tflags.DurationVar(&config.MaxRetryInterval, \"max-retry-interval\", 0, \"\")\n\tflags.StringVar(&config.Proxy, \"proxy\", \"\", \"\")\n\tflags.StringVar(&config.TLS.CA, \"tls-ca\", \"\", \"\")\n\tflags.BoolVar(&config.TLS.SkipVerify, \"tls-skip-verify\", false, \"\")\n\tflags.StringVar(&config.TLS.Cert, \"tls-cert\", \"\", \"\")\n\tflags.StringVar(&config.TLS.Key, \"tls-key\", \"\", \"\")\n\tflags.Var(&headerFlags{config.Headers}, \"header\", \"\")\n\thostname := flags.String(\"hostname\", \"\", \"\")\n\tsni := flags.String(\"sni\", \"\", \"\")\n\tpid := flags.Bool(\"pid\", false, \"\")\n\tverbose := flags.Bool(\"v\", false, \"\")\n\tflags.Usage = func() {\n\t\tfmt.Print(clientHelp)\n\t\tos.Exit(0)\n\t}\n\tflags.Parse(args)\n\t//pull out options, put back remaining args\n\targs = flags.Args()\n\tif len(args) < 2 {\n\t\tlog.Fatalf(\"A server and least one remote is required\")\n\t}\n\tconfig.Server = args[0]\n\tconfig.Remotes = args[1:]\n\t//default auth\n\tif config.Auth == \"\" {\n\t\tconfig.Auth = os.Getenv(\"AUTH\")\n\t}\n\t//move hostname onto headers\n\tif *hostname != \"\" {\n\t\tconfig.Headers.Set(\"Host\", *hostname)\n\t\tconfig.TLS.ServerName = *hostname\n\t}\n\n\tif *sni != \"\" {\n\t\tconfig.TLS.ServerName = *sni\n\t}\n\n\t//ready\n\tc, err := chclient.NewClient(&config)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tc.Debug = *verbose\n\tif *pid {\n\t\tgeneratePidFile()\n\t}\n\tgo cos.GoStats()\n\tctx := cos.InterruptContext()\n\tif err := c.Start(ctx); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif err := c.Wait(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "package chserver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\tchshare \"github.com/jpillora/chisel/share\"\n\t\"github.com/jpillora/chisel/share/ccrypto\"\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/requestlog\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Config is the configuration for the chisel service\ntype Config struct {\n\tKeySeed   string\n\tKeyFile   string\n\tAuthFile  string\n\tAuth      string\n\tProxy     string\n\tSocks5    bool\n\tReverse   bool\n\tKeepAlive time.Duration\n\tTLS       TLSConfig\n}\n\n// Server respresent a chisel service\ntype Server struct {\n\t*cio.Logger\n\tconfig       *Config\n\tfingerprint  string\n\thttpServer   *cnet.HTTPServer\n\treverseProxy *httputil.ReverseProxy\n\tsessCount    int32\n\tsessions     *settings.Users\n\tsshConfig    *ssh.ServerConfig\n\tusers        *settings.UserIndex\n}\n\nvar upgrader = websocket.Upgrader{\n\tCheckOrigin:     func(r *http.Request) bool { return true },\n\tReadBufferSize:  settings.EnvInt(\"WS_BUFF_SIZE\", 0),\n\tWriteBufferSize: settings.EnvInt(\"WS_BUFF_SIZE\", 0),\n}\n\n// NewServer creates and returns a new chisel server\nfunc NewServer(c *Config) (*Server, error) {\n\tserver := &Server{\n\t\tconfig:     c,\n\t\thttpServer: cnet.NewHTTPServer(),\n\t\tLogger:     cio.NewLogger(\"server\"),\n\t\tsessions:   settings.NewUsers(),\n\t}\n\tserver.Info = true\n\tserver.users = settings.NewUserIndex(server.Logger)\n\tif c.AuthFile != \"\" {\n\t\tif err := server.users.LoadUsers(c.AuthFile); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif c.Auth != \"\" {\n\t\tu := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}\n\t\tu.Name, u.Pass = settings.ParseAuth(c.Auth)\n\t\tif u.Name != \"\" {\n\t\t\tserver.users.AddUser(u)\n\t\t}\n\t}\n\n\tvar pemBytes []byte\n\tvar err error\n\tif c.KeyFile != \"\" {\n\t\tvar key []byte\n\n\t\tif ccrypto.IsChiselKey([]byte(c.KeyFile)) {\n\t\t\tkey = []byte(c.KeyFile)\n\t\t} else {\n\t\t\tkey, err = os.ReadFile(c.KeyFile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Failed to read key file %s\", c.KeyFile)\n\t\t\t}\n\t\t}\n\n\t\tpemBytes = key\n\t\tif ccrypto.IsChiselKey(key) {\n\t\t\tpemBytes, err = ccrypto.ChiselKey2PEM(key)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Invalid key %s\", string(key))\n\t\t\t}\n\t\t}\n\t} else {\n\t\t//generate private key (optionally using seed)\n\t\tpemBytes, err = ccrypto.Seed2PEM(c.KeySeed)\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"Failed to generate key\")\n\t\t}\n\t}\n\n\t//convert into ssh.PrivateKey\n\tprivate, err := ssh.ParsePrivateKey(pemBytes)\n\tif err != nil {\n\t\tlog.Fatal(\"Failed to parse key\")\n\t}\n\t//fingerprint this key\n\tserver.fingerprint = ccrypto.FingerprintKey(private.PublicKey())\n\t//create ssh config\n\tserver.sshConfig = &ssh.ServerConfig{\n\t\tServerVersion:    \"SSH-\" + chshare.ProtocolVersion + \"-server\",\n\t\tPasswordCallback: server.authUser,\n\t}\n\tserver.sshConfig.AddHostKey(private)\n\t//setup reverse proxy\n\tif c.Proxy != \"\" {\n\t\tu, err := url.Parse(c.Proxy)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif u.Host == \"\" {\n\t\t\treturn nil, server.Errorf(\"Missing protocol (%s)\", u)\n\t\t}\n\t\tserver.reverseProxy = httputil.NewSingleHostReverseProxy(u)\n\t\t//always use proxy host\n\t\tserver.reverseProxy.Director = func(r *http.Request) {\n\t\t\t//enforce origin, keep path\n\t\t\tr.URL.Scheme = u.Scheme\n\t\t\tr.URL.Host = u.Host\n\t\t\tr.Host = u.Host\n\t\t}\n\t}\n\t//print when reverse tunnelling is enabled\n\tif c.Reverse {\n\t\tserver.Infof(\"Reverse tunnelling enabled\")\n\t}\n\treturn server, nil\n}\n\n// Run is responsible for starting the chisel service.\n// Internally this calls Start then Wait.\nfunc (s *Server) Run(host, port string) error {\n\tif err := s.Start(host, port); err != nil {\n\t\treturn err\n\t}\n\treturn s.Wait()\n}\n\n// Start is responsible for kicking off the http server\nfunc (s *Server) Start(host, port string) error {\n\treturn s.StartContext(context.Background(), host, port)\n}\n\n// StartContext is responsible for kicking off the http server,\n// and can be closed by cancelling the provided context\nfunc (s *Server) StartContext(ctx context.Context, host, port string) error {\n\ts.Infof(\"Fingerprint %s\", s.fingerprint)\n\tif s.users.Len() > 0 {\n\t\ts.Infof(\"User authentication enabled\")\n\t}\n\tif s.reverseProxy != nil {\n\t\ts.Infof(\"Reverse proxy enabled\")\n\t}\n\tl, err := s.listener(host, port)\n\tif err != nil {\n\t\treturn err\n\t}\n\th := http.Handler(http.HandlerFunc(s.handleClientHandler))\n\tif s.Debug {\n\t\to := requestlog.DefaultOptions\n\t\to.TrustProxy = true\n\t\th = requestlog.WrapWith(h, o)\n\t}\n\treturn s.httpServer.GoServe(ctx, l, h)\n}\n\n// Wait waits for the http server to close\nfunc (s *Server) Wait() error {\n\treturn s.httpServer.Wait()\n}\n\n// Close forcibly closes the http server\nfunc (s *Server) Close() error {\n\treturn s.httpServer.Close()\n}\n\n// GetFingerprint is used to access the server fingerprint\nfunc (s *Server) GetFingerprint() string {\n\treturn s.fingerprint\n}\n\n// authUser is responsible for validating the ssh user / password combination\nfunc (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {\n\t// check if user authentication is enabled and if not, allow all\n\tif s.users.Len() == 0 {\n\t\treturn nil, nil\n\t}\n\t// check the user exists and has matching password\n\tn := c.User()\n\tuser, found := s.users.Get(n)\n\tif !found || user.Pass != string(password) {\n\t\ts.Debugf(\"Login failed for user: %s\", n)\n\t\treturn nil, errors.New(\"Invalid authentication for username: %s\")\n\t}\n\t// insert the user session map\n\t// TODO this should probably have a lock on it given the map isn't thread-safe\n\ts.sessions.Set(string(c.SessionID()), user)\n\treturn nil, nil\n}\n\n// AddUser adds a new user into the server user index\nfunc (s *Server) AddUser(user, pass string, addrs ...string) error {\n\tauthorizedAddrs := []*regexp.Regexp{}\n\tfor _, addr := range addrs {\n\t\tauthorizedAddr, err := regexp.Compile(addr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tauthorizedAddrs = append(authorizedAddrs, authorizedAddr)\n\t}\n\ts.users.AddUser(&settings.User{\n\t\tName:  user,\n\t\tPass:  pass,\n\t\tAddrs: authorizedAddrs,\n\t})\n\treturn nil\n}\n\n// DeleteUser removes a user from the server user index\nfunc (s *Server) DeleteUser(user string) {\n\ts.users.Del(user)\n}\n\n// ResetUsers in the server user index.\n// Use nil to remove all.\nfunc (s *Server) ResetUsers(users []*settings.User) {\n\ts.users.Reset(users)\n}\n"
  },
  {
    "path": "server/server_handler.go",
    "content": "package chserver\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tchshare \"github.com/jpillora/chisel/share\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/chisel/share/tunnel\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// handleClientHandler is the main http websocket handler for the chisel server\nfunc (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {\n\t//websockets upgrade AND has chisel prefix\n\tupgrade := strings.ToLower(r.Header.Get(\"Upgrade\"))\n\tprotocol := r.Header.Get(\"Sec-WebSocket-Protocol\")\n\tif upgrade == \"websocket\" {\n\t\tif protocol == chshare.ProtocolVersion {\n\t\t\ts.handleWebsocket(w, r)\n\t\t\treturn\n\t\t}\n\t\t//print into server logs and silently fall-through\n\t\ts.Infof(\"ignored client connection using protocol '%s', expected '%s'\",\n\t\t\tprotocol, chshare.ProtocolVersion)\n\t}\n\t//proxy target was provided\n\tif s.reverseProxy != nil {\n\t\ts.reverseProxy.ServeHTTP(w, r)\n\t\treturn\n\t}\n\t//no proxy defined, provide access to health/version checks\n\tswitch r.URL.Path {\n\tcase \"/health\":\n\t\tw.Write([]byte(\"OK\\n\"))\n\t\treturn\n\tcase \"/version\":\n\t\tw.Write([]byte(chshare.BuildVersion))\n\t\treturn\n\t}\n\t//missing :O\n\tw.WriteHeader(404)\n\tw.Write([]byte(\"Not found\"))\n}\n\n// handleWebsocket is responsible for handling the websocket connection\nfunc (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {\n\tid := atomic.AddInt32(&s.sessCount, 1)\n\tl := s.Fork(\"session#%d\", id)\n\twsConn, err := upgrader.Upgrade(w, req, nil)\n\tif err != nil {\n\t\tl.Debugf(\"Failed to upgrade (%s)\", err)\n\t\treturn\n\t}\n\tconn := cnet.NewWebSocketConn(wsConn)\n\t// perform SSH handshake on net.Conn\n\tl.Debugf(\"Handshaking with %s...\", req.RemoteAddr)\n\tsshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)\n\tif err != nil {\n\t\ts.Debugf(\"Failed to handshake (%s)\", err)\n\t\treturn\n\t}\n\t// pull the users from the session map\n\tvar user *settings.User\n\tif s.users.Len() > 0 {\n\t\tsid := string(sshConn.SessionID())\n\t\tu, ok := s.sessions.Get(sid)\n\t\tif !ok {\n\t\t\tpanic(\"bug in ssh auth handler\")\n\t\t}\n\t\tuser = u\n\t\ts.sessions.Del(sid)\n\t}\n\t// chisel server handshake (reverse of client handshake)\n\t// verify configuration\n\tl.Debugf(\"Verifying configuration\")\n\t// wait for request, with timeout\n\tvar r *ssh.Request\n\tselect {\n\tcase r = <-reqs:\n\tcase <-time.After(settings.EnvDuration(\"CONFIG_TIMEOUT\", 10*time.Second)):\n\t\tl.Debugf(\"Timeout waiting for configuration\")\n\t\tsshConn.Close()\n\t\treturn\n\t}\n\tfailed := func(err error) {\n\t\tl.Debugf(\"Failed: %s\", err)\n\t\tr.Reply(false, []byte(err.Error()))\n\t}\n\tif r.Type != \"config\" {\n\t\tfailed(s.Errorf(\"expecting config request\"))\n\t\treturn\n\t}\n\tc, err := settings.DecodeConfig(r.Payload)\n\tif err != nil {\n\t\tfailed(s.Errorf(\"invalid config\"))\n\t\treturn\n\t}\n\t//print if client and server  versions dont match\n\tcv := strings.TrimPrefix(c.Version, \"v\")\n\tif cv == \"\" {\n\t\tcv = \"<unknown>\"\n\t}\n\tsv := strings.TrimPrefix(chshare.BuildVersion, \"v\")\n\tif cv != sv {\n\t\tl.Infof(\"Client version (%s) differs from server version (%s)\", cv, sv)\n\t}\n\t//validate remotes\n\tfor _, r := range c.Remotes {\n\t\t//if user is provided, ensure they have\n\t\t//access to the desired remotes\n\t\tif user != nil {\n\t\t\taddr := r.UserAddr()\n\t\t\tif !user.HasAccess(addr) {\n\t\t\t\tfailed(s.Errorf(\"access to '%s' denied\", addr))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t//confirm reverse tunnels are allowed\n\t\tif r.Reverse && !s.config.Reverse {\n\t\t\tl.Debugf(\"Denied reverse port forwarding request, please enable --reverse\")\n\t\t\tfailed(s.Errorf(\"Reverse port forwaring not enabled on server\"))\n\t\t\treturn\n\t\t}\n\t\t//confirm reverse tunnel is available\n\t\tif r.Reverse && !r.CanListen() {\n\t\t\tfailed(s.Errorf(\"Server cannot listen on %s\", r.String()))\n\t\t\treturn\n\t\t}\n\t}\n\t//successfuly validated config!\n\tr.Reply(true, nil)\n\t//tunnel per ssh connection\n\ttunnelConfig := tunnel.Config{\n\t\tLogger:    l,\n\t\tInbound:   s.config.Reverse,\n\t\tOutbound:  true, //server always accepts outbound\n\t\tSocks:     s.config.Socks5,\n\t\tKeepAlive: s.config.KeepAlive,\n\t}\n\t//enforce ACL on every channel, not just the initial config\n\tif user != nil {\n\t\ttunnelConfig.ACL = user.HasAccess\n\t}\n\ttunnel := tunnel.New(tunnelConfig)\n\t//bind\n\teg, ctx := errgroup.WithContext(req.Context())\n\teg.Go(func() error {\n\t\t//connected, handover ssh connection for tunnel to use, and block\n\t\treturn tunnel.BindSSH(ctx, sshConn, reqs, chans)\n\t})\n\teg.Go(func() error {\n\t\t//connected, setup reversed-remotes?\n\t\tserverInbound := c.Remotes.Reversed(true)\n\t\tif len(serverInbound) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\t//block\n\t\treturn tunnel.BindRemotes(ctx, serverInbound)\n\t})\n\terr = eg.Wait()\n\tif err != nil && !strings.HasSuffix(err.Error(), \"EOF\") {\n\t\tl.Debugf(\"Closed connection (%s)\", err)\n\t} else {\n\t\tl.Debugf(\"Closed connection\")\n\t}\n}\n"
  },
  {
    "path": "server/server_listen.go",
    "content": "package chserver\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"net\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"golang.org/x/crypto/acme/autocert\"\n)\n\n//TLSConfig enables configures TLS\ntype TLSConfig struct {\n\tKey     string\n\tCert    string\n\tDomains []string\n\tCA      string\n}\n\nfunc (s *Server) listener(host, port string) (net.Listener, error) {\n\thasDomains := len(s.config.TLS.Domains) > 0\n\thasKeyCert := s.config.TLS.Key != \"\" && s.config.TLS.Cert != \"\"\n\tif hasDomains && hasKeyCert {\n\t\treturn nil, errors.New(\"cannot use key/cert and domains\")\n\t}\n\tvar tlsConf *tls.Config\n\tif hasDomains {\n\t\ttlsConf = s.tlsLetsEncrypt(s.config.TLS.Domains)\n\t}\n\textra := \"\"\n\tif hasKeyCert {\n\t\tc, err := s.tlsKeyCert(s.config.TLS.Key, s.config.TLS.Cert, s.config.TLS.CA)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttlsConf = c\n\t\tif port != \"443\" && hasDomains {\n\t\t\textra = \" (WARNING: LetsEncrypt will attempt to connect to your domain on port 443)\"\n\t\t}\n\t}\n\t//tcp listen\n\tl, err := net.Listen(\"tcp\", host+\":\"+port)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//optionally wrap in tls\n\tproto := \"http\"\n\tif tlsConf != nil {\n\t\tproto += \"s\"\n\t\tl = tls.NewListener(l, tlsConf)\n\t}\n\tif err == nil {\n\t\ts.Infof(\"Listening on %s://%s:%s%s\", proto, host, port, extra)\n\t}\n\treturn l, nil\n}\n\nfunc (s *Server) tlsLetsEncrypt(domains []string) *tls.Config {\n\t//prepare cert manager\n\tm := &autocert.Manager{\n\t\tPrompt: func(tosURL string) bool {\n\t\t\ts.Infof(\"Accepting LetsEncrypt TOS and fetching certificate...\")\n\t\t\treturn true\n\t\t},\n\t\tEmail:      settings.Env(\"LE_EMAIL\"),\n\t\tHostPolicy: autocert.HostWhitelist(domains...),\n\t}\n\t//configure file cache\n\tc := settings.Env(\"LE_CACHE\")\n\tif c == \"\" {\n\t\th := os.Getenv(\"HOME\")\n\t\tif h == \"\" {\n\t\t\tif u, err := user.Current(); err == nil {\n\t\t\t\th = u.HomeDir\n\t\t\t}\n\t\t}\n\t\tc = filepath.Join(h, \".cache\", \"chisel\")\n\t}\n\tif c != \"-\" {\n\t\ts.Infof(\"LetsEncrypt cache directory %s\", c)\n\t\tm.Cache = autocert.DirCache(c)\n\t}\n\t//return lets-encrypt tls config\n\treturn m.TLSConfig()\n}\n\nfunc (s *Server) tlsKeyCert(key, cert string, ca string) (*tls.Config, error) {\n\tkeypair, err := tls.LoadX509KeyPair(cert, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t//file based tls config using tls defaults\n\tc := &tls.Config{\n\t\tCertificates: []tls.Certificate{keypair},\n\t}\n\t//mTLS requires server's CA\n\tif ca != \"\" {\n\t\tif err := addCA(ca, c); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ts.Infof(\"Loaded CA path: %s\", ca)\n\t}\n\treturn c, nil\n}\n\nfunc addCA(ca string, c *tls.Config) error {\n\tfileInfo, err := os.Stat(ca)\n\tif err != nil {\n\t\treturn err\n\t}\n\tclientCAPool := x509.NewCertPool()\n\tif fileInfo.IsDir() {\n\t\t//this is a directory holding CA bundle files\n\t\tfiles, err := os.ReadDir(ca)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t//add all cert files from path\n\t\tfor _, file := range files {\n\t\t\tf := file.Name()\n\t\t\tif err := addPEMFile(filepath.Join(ca, f), clientCAPool); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\t//this is a CA bundle file\n\t\tif err := addPEMFile(ca, clientCAPool); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t//set client CAs and enable cert verification\n\tc.ClientCAs = clientCAPool\n\tc.ClientAuth = tls.RequireAndVerifyClientCert\n\treturn nil\n}\n\nfunc addPEMFile(path string, pool *x509.CertPool) error {\n\tcontent, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !pool.AppendCertsFromPEM(content) {\n\t\treturn errors.New(\"Fail to load certificates from : \" + path)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "share/ccrypto/determ_rand.go",
    "content": "package ccrypto\n\n// Deterministic crypto.Reader\n// overview: half the result is used as the output\n// [a|...] -> sha512(a) -> [b|output] -> sha512(b)\n\nimport (\n\t\"crypto/sha512\"\n\t\"io\"\n)\n\nconst DetermRandIter = 2048\n\nfunc NewDetermRand(seed []byte) io.Reader {\n\tvar out []byte\n\t//strengthen seed\n\tvar next = seed\n\tfor i := 0; i < DetermRandIter; i++ {\n\t\tnext, out = hash(next)\n\t}\n\treturn &determRand{\n\t\tnext: next,\n\t\tout:  out,\n\t}\n}\n\ntype determRand struct {\n\tnext, out []byte\n}\n\nfunc (d *determRand) Read(b []byte) (int, error) {\n\tn := 0\n\tl := len(b)\n\tfor n < l {\n\t\tnext, out := hash(d.next)\n\t\tn += copy(b[n:], out)\n\t\td.next = next\n\t}\n\treturn n, nil\n}\n\nfunc hash(input []byte) (next []byte, output []byte) {\n\tnextout := sha512.Sum512(input)\n\treturn nextout[:sha512.Size/2], nextout[sha512.Size/2:]\n}\n"
  },
  {
    "path": "share/ccrypto/generate_key_go119.go",
    "content": "package ccrypto\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"io\"\n\t\"math/big\"\n)\n\nvar one = new(big.Int).SetInt64(1)\n\n// This function is copied from ecdsa.GenerateKey() of Go 1.19\nfunc GenerateKeyGo119(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) {\n\tk, err := randFieldElement(c, rand)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpriv := new(ecdsa.PrivateKey)\n\tpriv.PublicKey.Curve = c\n\tpriv.D = k\n\tpriv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes())\n\treturn priv, nil\n}\n\n// This function is copied from Go 1.19\nfunc randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) {\n\tparams := c.Params()\n\t// Note that for P-521 this will actually be 63 bits more than the order, as\n\t// division rounds down, but the extra bit is inconsequential.\n\tb := make([]byte, params.N.BitLen()/8+8)\n\t_, err = io.ReadFull(rand, b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tk = new(big.Int).SetBytes(b)\n\tn := new(big.Int).Sub(params.N, one)\n\tk.Mod(k, n)\n\tk.Add(k, one)\n\treturn\n}\n"
  },
  {
    "path": "share/ccrypto/keys.go",
    "content": "package ccrypto\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// GenerateKey generates a PEM key\nfunc GenerateKey(seed string) ([]byte, error) {\n\treturn Seed2PEM(seed)\n}\n\n// GenerateKeyFile generates an ChiselKey\nfunc GenerateKeyFile(keyFilePath, seed string) error {\n\tchiselKey, err := seed2ChiselKey(seed)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif keyFilePath == \"-\" {\n\t\tfmt.Print(string(chiselKey))\n\t\treturn nil\n\t}\n\treturn os.WriteFile(keyFilePath, chiselKey, 0600)\n}\n\n// FingerprintKey calculates the SHA256 hash of an SSH public key\nfunc FingerprintKey(k ssh.PublicKey) string {\n\tbytes := sha256.Sum256(k.Marshal())\n\treturn base64.StdEncoding.EncodeToString(bytes[:])\n}\n"
  },
  {
    "path": "share/ccrypto/keys_helpers.go",
    "content": "package ccrypto\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"strings\"\n)\n\nconst ChiselKeyPrefix = \"ck-\"\n\n//  Relations between entities:\n//\n//   .............> PEM <...........\n//   .               ^             .\n//   .               |             .\n//   .               |             .\n// Seed -------> PrivateKey        .\n//   .               ^             .\n//   .               |             .\n//   .               V             .\n//   ..........> ChiselKey .........\n\nfunc Seed2PEM(seed string) ([]byte, error) {\n\tprivateKey, err := seed2PrivateKey(seed)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn privateKey2PEM(privateKey)\n}\n\nfunc seed2ChiselKey(seed string) ([]byte, error) {\n\tprivateKey, err := seed2PrivateKey(seed)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn privateKey2ChiselKey(privateKey)\n}\n\nfunc seed2PrivateKey(seed string) (*ecdsa.PrivateKey, error) {\n\tif seed == \"\" {\n\t\treturn ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\t} else {\n\t\treturn GenerateKeyGo119(elliptic.P256(), NewDetermRand([]byte(seed)))\n\t}\n}\n\nfunc privateKey2ChiselKey(privateKey *ecdsa.PrivateKey) ([]byte, error) {\n\tb, err := x509.MarshalECPrivateKey(privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tencodedPrivateKey := make([]byte, base64.RawStdEncoding.EncodedLen(len(b)))\n\tbase64.RawStdEncoding.Encode(encodedPrivateKey, b)\n\n\treturn append([]byte(ChiselKeyPrefix), encodedPrivateKey...), nil\n}\n\nfunc privateKey2PEM(privateKey *ecdsa.PrivateKey) ([]byte, error) {\n\tb, err := x509.MarshalECPrivateKey(privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pem.EncodeToMemory(&pem.Block{Type: \"EC PRIVATE KEY\", Bytes: b}), nil\n}\n\nfunc chiselKey2PrivateKey(chiselKey []byte) (*ecdsa.PrivateKey, error) {\n\trawChiselKey := chiselKey[len(ChiselKeyPrefix):]\n\n\tdecodedPrivateKey := make([]byte, base64.RawStdEncoding.DecodedLen(len(rawChiselKey)))\n\t_, err := base64.RawStdEncoding.Decode(decodedPrivateKey, rawChiselKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn x509.ParseECPrivateKey(decodedPrivateKey)\n}\n\nfunc ChiselKey2PEM(chiselKey []byte) ([]byte, error) {\n\tprivateKey, err := chiselKey2PrivateKey(chiselKey)\n\tif err == nil {\n\t\treturn privateKey2PEM(privateKey)\n\t}\n\n\treturn nil, err\n}\n\nfunc IsChiselKey(chiselKey []byte) bool {\n\treturn strings.HasPrefix(string(chiselKey), ChiselKeyPrefix)\n}\n"
  },
  {
    "path": "share/cio/logger.go",
    "content": "package cio\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n)\n\n//Logger is pkg/log Logger with prefixing and 2 log levels\ntype Logger struct {\n\tInfo, Debug bool\n\t//internal\n\tprefix      string\n\tlogger      *log.Logger\n\tinfo, debug *bool\n}\n\nfunc NewLogger(prefix string) *Logger {\n\treturn NewLoggerFlag(prefix, log.Ldate|log.Ltime)\n}\n\nfunc NewLoggerFlag(prefix string, flag int) *Logger {\n\tl := &Logger{\n\t\tprefix: prefix,\n\t\tlogger: log.New(os.Stderr, \"\", flag),\n\t\tInfo:   false,\n\t\tDebug:  false,\n\t}\n\treturn l\n}\n\nfunc (l *Logger) Infof(f string, args ...interface{}) {\n\tif l.IsInfo() {\n\t\tl.logger.Printf(l.prefix+\": \"+f, args...)\n\t}\n}\n\nfunc (l *Logger) Debugf(f string, args ...interface{}) {\n\tif l.IsDebug() {\n\t\tl.logger.Printf(l.prefix+\": \"+f, args...)\n\t}\n}\n\nfunc (l *Logger) Errorf(f string, args ...interface{}) error {\n\treturn fmt.Errorf(l.prefix+\": \"+f, args...)\n}\n\nfunc (l *Logger) Fork(prefix string, args ...interface{}) *Logger {\n\t//slip the parent prefix at the front\n\targs = append([]interface{}{l.prefix}, args...)\n\tll := NewLogger(fmt.Sprintf(\"%s: \"+prefix, args...))\n\t//store link to parent settings too\n\tll.Info = l.Info\n\tif l.info != nil {\n\t\tll.info = l.info\n\t} else {\n\t\tll.info = &l.Info\n\t}\n\tll.Debug = l.Debug\n\tif l.debug != nil {\n\t\tll.debug = l.debug\n\t} else {\n\t\tll.debug = &l.Debug\n\t}\n\treturn ll\n}\n\nfunc (l *Logger) Prefix() string {\n\treturn l.prefix\n}\n\nfunc (l *Logger) IsInfo() bool {\n\treturn l.Info || (l.info != nil && *l.info)\n}\n\nfunc (l *Logger) IsDebug() bool {\n\treturn l.Debug || (l.debug != nil && *l.debug)\n}\n"
  },
  {
    "path": "share/cio/pipe.go",
    "content": "package cio\n\nimport (\n\t\"io\"\n\t\"log\"\n\t\"sync\"\n)\n\nfunc Pipe(src io.ReadWriteCloser, dst io.ReadWriteCloser) (int64, int64) {\n\tvar sent, received int64\n\tvar wg sync.WaitGroup\n\tvar o sync.Once\n\tclose := func() {\n\t\tsrc.Close()\n\t\tdst.Close()\n\t}\n\twg.Add(2)\n\tgo func() {\n\t\treceived, _ = io.Copy(src, dst)\n\t\to.Do(close)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\tsent, _ = io.Copy(dst, src)\n\t\to.Do(close)\n\t\twg.Done()\n\t}()\n\twg.Wait()\n\treturn sent, received\n}\n\nconst vis = false\n\ntype pipeVisPrinter struct {\n\tname string\n}\n\nfunc (p pipeVisPrinter) Write(b []byte) (int, error) {\n\tlog.Printf(\">>> %s: %x\", p.name, b)\n\treturn len(b), nil\n}\n\nfunc pipeVis(name string, r io.Reader) io.Reader {\n\tif vis {\n\t\treturn io.TeeReader(r, pipeVisPrinter{name})\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "share/cio/stdio.go",
    "content": "package cio\n\nimport (\n\t\"io\"\n\t\"os\"\n)\n\n//Stdio as a ReadWriteCloser\nvar Stdio = &struct {\n\tio.ReadCloser\n\tio.Writer\n}{\n\tio.NopCloser(os.Stdin),\n\tos.Stdout,\n}\n"
  },
  {
    "path": "share/cnet/conn_rwc.go",
    "content": "package cnet\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"time\"\n)\n\ntype rwcConn struct {\n\tio.ReadWriteCloser\n\tbuff []byte\n}\n\n//NewRWCConn converts a RWC into a net.Conn\nfunc NewRWCConn(rwc io.ReadWriteCloser) net.Conn {\n\tc := rwcConn{\n\t\tReadWriteCloser: rwc,\n\t}\n\treturn &c\n}\n\nfunc (c *rwcConn) LocalAddr() net.Addr {\n\treturn c\n}\n\nfunc (c *rwcConn) RemoteAddr() net.Addr {\n\treturn c\n}\n\nfunc (c *rwcConn) Network() string {\n\treturn \"tcp\"\n}\n\nfunc (c *rwcConn) String() string {\n\treturn \"\"\n}\n\nfunc (c *rwcConn) SetDeadline(t time.Time) error {\n\treturn nil //no-op\n}\n\nfunc (c *rwcConn) SetReadDeadline(t time.Time) error {\n\treturn nil //no-op\n}\n\nfunc (c *rwcConn) SetWriteDeadline(t time.Time) error {\n\treturn nil //no-op\n}\n"
  },
  {
    "path": "share/cnet/conn_ws.go",
    "content": "package cnet\n\nimport (\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\ntype wsConn struct {\n\t*websocket.Conn\n\tbuff []byte\n}\n\n//NewWebSocketConn converts a websocket.Conn into a net.Conn\nfunc NewWebSocketConn(websocketConn *websocket.Conn) net.Conn {\n\tc := wsConn{\n\t\tConn: websocketConn,\n\t}\n\treturn &c\n}\n\n//Read is not threadsafe though thats okay since there\n//should never be more than one reader\nfunc (c *wsConn) Read(dst []byte) (int, error) {\n\tldst := len(dst)\n\t//use buffer or read new message\n\tvar src []byte\n\tif len(c.buff) > 0 {\n\t\tsrc = c.buff\n\t\tc.buff = nil\n\t} else if _, msg, err := c.Conn.ReadMessage(); err == nil {\n\t\tsrc = msg\n\t} else {\n\t\treturn 0, err\n\t}\n\t//copy src->dest\n\tvar n int\n\tif len(src) > ldst {\n\t\t//copy as much as possible of src into dst\n\t\tn = copy(dst, src[:ldst])\n\t\t//copy remainder into buffer\n\t\tr := src[ldst:]\n\t\tlr := len(r)\n\t\tc.buff = make([]byte, lr)\n\t\tcopy(c.buff, r)\n\t} else {\n\t\t//copy all of src into dst\n\t\tn = copy(dst, src)\n\t}\n\t//return bytes copied\n\treturn n, nil\n}\n\nfunc (c *wsConn) Write(b []byte) (int, error) {\n\tif err := c.Conn.WriteMessage(websocket.BinaryMessage, b); err != nil {\n\t\treturn 0, err\n\t}\n\tn := len(b)\n\treturn n, nil\n}\n\nfunc (c *wsConn) SetDeadline(t time.Time) error {\n\tif err := c.Conn.SetReadDeadline(t); err != nil {\n\t\treturn err\n\t}\n\treturn c.Conn.SetWriteDeadline(t)\n}\n"
  },
  {
    "path": "share/cnet/connstats.go",
    "content": "package cnet\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n)\n\n//ConnCount is a connection counter\ntype ConnCount struct {\n\tcount int32\n\topen  int32\n}\n\nfunc (c *ConnCount) New() int32 {\n\treturn atomic.AddInt32(&c.count, 1)\n}\n\nfunc (c *ConnCount) Open() {\n\tatomic.AddInt32(&c.open, 1)\n}\n\nfunc (c *ConnCount) Close() {\n\tatomic.AddInt32(&c.open, -1)\n}\n\nfunc (c *ConnCount) String() string {\n\treturn fmt.Sprintf(\"[%d/%d]\", atomic.LoadInt32(&c.open), atomic.LoadInt32(&c.count))\n}\n"
  },
  {
    "path": "share/cnet/http_server.go",
    "content": "package cnet\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"golang.org/x/sync/errgroup\"\n)\n\n//HTTPServer extends net/http Server and\n//adds graceful shutdowns\ntype HTTPServer struct {\n\t*http.Server\n\twaiterMux sync.Mutex\n\twaiter    *errgroup.Group\n\tlistenErr error\n}\n\n//NewHTTPServer creates a new HTTPServer\nfunc NewHTTPServer() *HTTPServer {\n\treturn &HTTPServer{\n\t\tServer: &http.Server{},\n\t}\n\n}\n\nfunc (h *HTTPServer) GoListenAndServe(addr string, handler http.Handler) error {\n\treturn h.GoListenAndServeContext(context.Background(), addr, handler)\n}\n\nfunc (h *HTTPServer) GoListenAndServeContext(ctx context.Context, addr string, handler http.Handler) error {\n\tif ctx == nil {\n\t\treturn errors.New(\"ctx must be set\")\n\t}\n\tl, err := net.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn h.GoServe(ctx, l, handler)\n}\n\nfunc (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.Handler) error {\n\tif ctx == nil {\n\t\treturn errors.New(\"ctx must be set\")\n\t}\n\th.waiterMux.Lock()\n\tdefer h.waiterMux.Unlock()\n\th.Handler = handler\n\th.waiter, ctx = errgroup.WithContext(ctx)\n\th.waiter.Go(func() error {\n\t\treturn h.Serve(l)\n\t})\n\tgo func() {\n\t\t<-ctx.Done()\n\t\th.Close()\n\t}()\n\treturn nil\n}\n\nfunc (h *HTTPServer) Close() error {\n\th.waiterMux.Lock()\n\tdefer h.waiterMux.Unlock()\n\tif h.waiter == nil {\n\t\treturn errors.New(\"not started yet\")\n\t}\n\treturn h.Server.Close()\n}\n\nfunc (h *HTTPServer) Wait() error {\n\th.waiterMux.Lock()\n\tunset := h.waiter == nil\n\th.waiterMux.Unlock()\n\tif unset {\n\t\treturn errors.New(\"not started yet\")\n\t}\n\th.waiterMux.Lock()\n\twait := h.waiter.Wait\n\th.waiterMux.Unlock()\n\terr := wait()\n\tif err == http.ErrServerClosed {\n\t\terr = nil //success\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "share/cnet/meter.go",
    "content": "package cnet\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/sizestr\"\n)\n\n//NewMeter to measure readers/writers\nfunc NewMeter(l *cio.Logger) *Meter {\n\treturn &Meter{l: l}\n}\n\n//Meter can be inserted in the path or\n//of a reader or writer to measure the\n//throughput\ntype Meter struct {\n\t//meter state\n\tsent, recv int64\n\t//print state\n\tl            *cio.Logger\n\tprinting     uint32\n\tlast         int64\n\tlsent, lrecv int64\n}\n\nfunc (m *Meter) print() {\n\t//move out of the read/write path asap\n\tif atomic.CompareAndSwapUint32(&m.printing, 0, 1) {\n\t\tgo m.goprint()\n\t}\n}\n\nfunc (m *Meter) goprint() {\n\ttime.Sleep(time.Second)\n\t//snapshot\n\ts := atomic.LoadInt64(&m.sent)\n\tr := atomic.LoadInt64(&m.recv)\n\t//compute speed\n\tcurr := time.Now().UnixNano()\n\tlast := atomic.LoadInt64(&m.last)\n\tdt := time.Duration(curr-last) * time.Nanosecond\n\tls := atomic.LoadInt64(&m.lsent)\n\tlr := atomic.LoadInt64(&m.lrecv)\n\t//DEBUG\n\t// m.l.Infof(\"%s = %d(%d-%d), %d(%d-%d)\", dt, s-ls, s, ls, r-lr, r, lr)\n\t//scale to per second V=D/T\n\tsps := int64(float64(s-ls) / float64(dt) * float64(time.Second))\n\trps := int64(float64(r-lr) / float64(dt) * float64(time.Second))\n\tif last > 0 && (sps != 0 || rps != 0) {\n\t\tm.l.Debugf(\"write %s/s read %s/s\", sizestr.ToString(sps), sizestr.ToString(rps))\n\t}\n\t//record last printed\n\tatomic.StoreInt64(&m.lsent, s)\n\tatomic.StoreInt64(&m.lrecv, r)\n\t//done\n\tatomic.StoreInt64(&m.last, curr)\n\tatomic.StoreUint32(&m.printing, 0)\n}\n\n//TeeReader inserts Meter into the read path\n//if the linked logger is in debug mode,\n//otherwise this is a no-op\nfunc (m *Meter) TeeReader(r io.Reader) io.Reader {\n\tif m.l.IsDebug() {\n\t\treturn &meterReader{m, r}\n\t}\n\treturn r\n}\n\ntype meterReader struct {\n\t*Meter\n\tinner io.Reader\n}\n\nfunc (m *meterReader) Read(p []byte) (n int, err error) {\n\tn, err = m.inner.Read(p)\n\tatomic.AddInt64(&m.recv, int64(n))\n\tm.Meter.print()\n\treturn\n}\n\n//TeeWriter inserts Meter into the write path\n//if the linked logger is in debug mode,\n//otherwise this is a no-op\nfunc (m *Meter) TeeWriter(w io.Writer) io.Writer {\n\tif m.l.IsDebug() {\n\t\treturn &meterWriter{m, w}\n\t}\n\treturn w\n}\n\ntype meterWriter struct {\n\t*Meter\n\tinner io.Writer\n}\n\nfunc (m *meterWriter) Write(p []byte) (n int, err error) {\n\tn, err = m.inner.Write(p)\n\tatomic.AddInt64(&m.sent, int64(n))\n\tm.Meter.print()\n\treturn\n}\n\n//MeterConn inserts Meter into the connection path\n//if the linked logger is in debug mode,\n//otherwise this is a no-op\nfunc MeterConn(l *cio.Logger, conn net.Conn) net.Conn {\n\tm := NewMeter(l)\n\treturn &meterConn{\n\t\tmread:  m.TeeReader(conn),\n\t\tmwrite: m.TeeWriter(conn),\n\t\tConn:   conn,\n\t}\n}\n\ntype meterConn struct {\n\tmread  io.Reader\n\tmwrite io.Writer\n\tnet.Conn\n}\n\nfunc (m *meterConn) Read(p []byte) (n int, err error) {\n\treturn m.mread.Read(p)\n}\n\nfunc (m *meterConn) Write(p []byte) (n int, err error) {\n\treturn m.mwrite.Write(p)\n}\n\n//MeterRWC inserts Meter into the RWC path\n//if the linked logger is in debug mode,\n//otherwise this is a no-op\nfunc MeterRWC(l *cio.Logger, rwc io.ReadWriteCloser) io.ReadWriteCloser {\n\tm := NewMeter(l)\n\treturn &struct {\n\t\tio.Reader\n\t\tio.Writer\n\t\tio.Closer\n\t}{\n\t\tReader: m.TeeReader(rwc),\n\t\tWriter: m.TeeWriter(rwc),\n\t\tCloser: rwc,\n\t}\n}\n"
  },
  {
    "path": "share/compat.go",
    "content": "package chshare\n\n//this file exists to maintain backwards compatibility\n\nimport (\n\t\"github.com/jpillora/chisel/share/ccrypto\"\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/cos\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/chisel/share/tunnel\"\n)\n\nconst (\n\tDetermRandIter = ccrypto.DetermRandIter\n)\n\ntype (\n\tConfig     = settings.Config\n\tRemote     = settings.Remote\n\tRemotes    = settings.Remotes\n\tUser       = settings.User\n\tUsers      = settings.Users\n\tUserIndex  = settings.UserIndex\n\tHTTPServer = cnet.HTTPServer\n\tConnStats  = cnet.ConnCount\n\tLogger     = cio.Logger\n\tTCPProxy   = tunnel.Proxy\n)\n\nvar (\n\tNewDetermRand    = ccrypto.NewDetermRand\n\tGenerateKey      = ccrypto.GenerateKey\n\tFingerprintKey   = ccrypto.FingerprintKey\n\tPipe             = cio.Pipe\n\tNewLoggerFlag    = cio.NewLoggerFlag\n\tNewLogger        = cio.NewLogger\n\tStdio            = cio.Stdio\n\tDecodeConfig     = settings.DecodeConfig\n\tDecodeRemote     = settings.DecodeRemote\n\tNewUsers         = settings.NewUsers\n\tNewUserIndex     = settings.NewUserIndex\n\tUserAllowAll     = settings.UserAllowAll\n\tParseAuth        = settings.ParseAuth\n\tNewRWCConn       = cnet.NewRWCConn\n\tNewWebSocketConn = cnet.NewWebSocketConn\n\tNewHTTPServer    = cnet.NewHTTPServer\n\tGoStats          = cos.GoStats\n\tSleepSignal      = cos.SleepSignal\n\tNewTCPProxy      = tunnel.NewProxy\n)\n\n//EncodeConfig old version\nfunc EncodeConfig(c *settings.Config) ([]byte, error) {\n\treturn settings.EncodeConfig(*c), nil\n}\n"
  },
  {
    "path": "share/cos/common.go",
    "content": "package cos\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n)\n\n//InterruptContext returns a context which is\n//cancelled on OS Interrupt\nfunc InterruptContext() context.Context {\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\tsig := make(chan os.Signal, 1)\n\t\tsignal.Notify(sig, os.Interrupt) //windows compatible?\n\t\t<-sig\n\t\tsignal.Stop(sig)\n\t\tcancel()\n\t}()\n\treturn ctx\n}\n\n//SleepSignal sleeps for the given duration,\n//or until a SIGHUP is received\nfunc SleepSignal(d time.Duration) {\n\t<-AfterSignal(d)\n}\n"
  },
  {
    "path": "share/cos/pprof.go",
    "content": "// +build pprof\n\npackage cos\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t_ \"net/http/pprof\" //import http profiler api\n)\n\nfunc init() {\n\tgo func() {\n\t\tlog.Fatal(http.ListenAndServe(\"localhost:6060\", nil))\n\t}()\n\tlog.Printf(\"[pprof] listening on 6060\")\n}\n"
  },
  {
    "path": "share/cos/signal.go",
    "content": "//+build !windows\n\npackage cos\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/jpillora/sizestr\"\n)\n\n//GoStats prints statistics to\n//stdout on SIGUSR2 (posix-only)\nfunc GoStats() {\n\t//silence complaints from windows\n\tconst SIGUSR2 = syscall.Signal(0x1f)\n\ttime.Sleep(time.Second)\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, SIGUSR2)\n\tfor range c {\n\t\tmemStats := runtime.MemStats{}\n\t\truntime.ReadMemStats(&memStats)\n\t\tlog.Printf(\"recieved SIGUSR2, go-routines: %d, go-memory-usage: %s\",\n\t\t\truntime.NumGoroutine(),\n\t\t\tsizestr.ToString(int64(memStats.Alloc)))\n\t}\n}\n\n//AfterSignal returns a channel which will be closed\n//after the given duration or until a SIGHUP is received\nfunc AfterSignal(d time.Duration) <-chan struct{} {\n\tch := make(chan struct{})\n\tgo func() {\n\t\tsig := make(chan os.Signal, 1)\n\t\tsignal.Notify(sig, syscall.SIGHUP)\n\t\tselect {\n\t\tcase <-time.After(d):\n\t\tcase <-sig:\n\t\t}\n\t\tsignal.Stop(sig)\n\t\tclose(ch)\n\t}()\n\treturn ch\n}\n"
  },
  {
    "path": "share/cos/signal_windows.go",
    "content": "//+build windows\n\npackage cos\n\nimport (\n\t\"time\"\n)\n\nfunc GoStats() {\n\t//noop\n}\n\nfunc AfterSignal(d time.Duration) <-chan struct{} {\n\tch := make(chan struct{})\n\tgo func() {\n\t\t<-time.After(d)\n\t\tclose(ch)\n\t}()\n\treturn ch\n}\n"
  },
  {
    "path": "share/settings/config.go",
    "content": "package settings\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype Config struct {\n\tVersion string\n\tRemotes\n}\n\nfunc DecodeConfig(b []byte) (*Config, error) {\n\tc := &Config{}\n\terr := json.Unmarshal(b, c)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Invalid JSON config\")\n\t}\n\treturn c, nil\n}\n\nfunc EncodeConfig(c Config) []byte {\n\t//Config doesn't have types that can fail to marshal\n\tb, _ := json.Marshal(c)\n\treturn b\n}\n"
  },
  {
    "path": "share/settings/env.go",
    "content": "package settings\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Env returns a chisel environment variable\nfunc Env(name string) string {\n\treturn os.Getenv(\"CHISEL_\" + name)\n}\n\n// EnvInt returns an integer using an environment variable, with a default fallback\nfunc EnvInt(name string, def int) int {\n\tif n, err := strconv.Atoi(Env(name)); err == nil {\n\t\treturn n\n\t}\n\treturn def\n}\n\n// EnvDuration returns a duration using an environment variable, with a default fallback\nfunc EnvDuration(name string, def time.Duration) time.Duration {\n\tif n, err := time.ParseDuration(Env(name)); err == nil {\n\t\treturn n\n\t}\n\treturn def\n}\n\n// EnvBool returns a boolean using an environment variable\nfunc EnvBool(name string) bool {\n\tv := Env(name)\n\treturn v == \"1\" || strings.ToLower(v) == \"true\"\n}\n"
  },
  {
    "path": "share/settings/remote.go",
    "content": "package settings\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// short-hand conversions (see remote_test)\n//   3000 ->\n//     local  127.0.0.1:3000\n//     remote 127.0.0.1:3000\n//   foobar.com:3000 ->\n//     local  127.0.0.1:3000\n//     remote foobar.com:3000\n//   3000:google.com:80 ->\n//     local  127.0.0.1:3000\n//     remote google.com:80\n//   192.168.0.1:3000:google.com:80 ->\n//     local  192.168.0.1:3000\n//     remote google.com:80\n//   127.0.0.1:1080:socks\n//     local  127.0.0.1:1080\n//     remote socks\n//   stdio:example.com:22\n//     local  stdio\n//     remote example.com:22\n//   1.1.1.1:53/udp\n//     local  127.0.0.1:53/udp\n//     remote 1.1.1.1:53/udp\n\ntype Remote struct {\n\tLocalHost, LocalPort, LocalProto    string\n\tRemoteHost, RemotePort, RemoteProto string\n\tSocks, Reverse, Stdio               bool\n}\n\nconst revPrefix = \"R:\"\n\nfunc DecodeRemote(s string) (*Remote, error) {\n\treverse := false\n\tif strings.HasPrefix(s, revPrefix) {\n\t\ts = strings.TrimPrefix(s, revPrefix)\n\t\treverse = true\n\t}\n\tparts := regexp.MustCompile(`(\\[[^\\[\\]]+\\]|[^\\[\\]:]+):?`).FindAllStringSubmatch(s, -1)\n\tif len(parts) <= 0 || len(parts) >= 5 {\n\t\treturn nil, errors.New(\"Invalid remote\")\n\t}\n\tr := &Remote{Reverse: reverse}\n\t//parse from back to front, to set 'remote' fields first,\n\t//then to set 'local' fields second (allows the 'remote' side\n\t//to provide the defaults)\n\tfor i := len(parts) - 1; i >= 0; i-- {\n\t\tp := parts[i][1]\n\t\t//remote portion is socks?\n\t\tif i == len(parts)-1 && p == \"socks\" {\n\t\t\tr.Socks = true\n\t\t\tcontinue\n\t\t}\n\t\t//local portion is stdio?\n\t\tif i == 0 && p == \"stdio\" {\n\t\t\tr.Stdio = true\n\t\t\tcontinue\n\t\t}\n\t\tp, proto := L4Proto(p)\n\t\tif proto != \"\" {\n\t\t\tif r.RemotePort == \"\" {\n\t\t\t\tr.RemoteProto = proto\n\t\t\t} else if r.LocalProto == \"\" {\n\t\t\t\tr.LocalProto = proto\n\t\t\t}\n\t\t}\n\t\tif isPort(p) {\n\t\t\tif !r.Socks && r.RemotePort == \"\" {\n\t\t\t\tr.RemotePort = p\n\t\t\t}\n\t\t\tr.LocalPort = p\n\t\t\tcontinue\n\t\t}\n\t\tif !r.Socks && (r.RemotePort == \"\" && r.LocalPort == \"\") {\n\t\t\treturn nil, errors.New(\"Missing ports\")\n\t\t}\n\t\tif !isHost(p) {\n\t\t\treturn nil, errors.New(\"Invalid host\")\n\t\t}\n\t\tif !r.Socks && r.RemoteHost == \"\" {\n\t\t\tr.RemoteHost = p\n\t\t} else {\n\t\t\tr.LocalHost = p\n\t\t}\n\t}\n\t//remote string parsed, apply defaults...\n\tif r.Socks {\n\t\t//socks defaults\n\t\tif r.LocalHost == \"\" {\n\t\t\tr.LocalHost = \"127.0.0.1\"\n\t\t}\n\t\tif r.LocalPort == \"\" {\n\t\t\tr.LocalPort = \"1080\"\n\t\t}\n\t} else {\n\t\t//non-socks defaults\n\t\tif r.LocalHost == \"\" {\n\t\t\tr.LocalHost = \"0.0.0.0\"\n\t\t}\n\t\tif r.RemoteHost == \"\" {\n\t\t\tr.RemoteHost = \"127.0.0.1\"\n\t\t}\n\t}\n\tif r.RemoteProto == \"\" {\n\t\tr.RemoteProto = \"tcp\"\n\t}\n\tif r.LocalProto == \"\" {\n\t\tr.LocalProto = r.RemoteProto\n\t}\n\tif r.LocalProto != r.RemoteProto {\n\t\t//TODO support cross protocol\n\t\t//tcp <-> udp, is faily straight forward\n\t\t//udp <-> tcp, is trickier since udp is stateless and tcp is not\n\t\treturn nil, errors.New(\"cross-protocol remotes are not supported yet\")\n\t}\n\tif r.Socks && r.RemoteProto != \"tcp\" {\n\t\treturn nil, errors.New(\"only TCP SOCKS is supported\")\n\t}\n\tif r.Stdio && r.Reverse {\n\t\treturn nil, errors.New(\"stdio cannot be reversed\")\n\t}\n\treturn r, nil\n}\n\nfunc isPort(s string) bool {\n\tn, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif n <= 0 || n > 65535 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc isHost(s string) bool {\n\t_, err := url.Parse(\"//\" + s)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nvar l4Proto = regexp.MustCompile(`(?i)\\/(tcp|udp)$`)\n\n//L4Proto extacts the layer-4 protocol from the given string\nfunc L4Proto(s string) (head, proto string) {\n\tif l4Proto.MatchString(s) {\n\t\tl := len(s)\n\t\treturn strings.ToLower(s[:l-4]), s[l-3:]\n\t}\n\treturn s, \"\"\n}\n\n//implement Stringer\nfunc (r Remote) String() string {\n\tsb := strings.Builder{}\n\tif r.Reverse {\n\t\tsb.WriteString(revPrefix)\n\t}\n\tsb.WriteString(strings.TrimPrefix(r.Local(), \"0.0.0.0:\"))\n\tsb.WriteString(\"=>\")\n\tsb.WriteString(strings.TrimPrefix(r.Remote(), \"127.0.0.1:\"))\n\tif r.RemoteProto == \"udp\" {\n\t\tsb.WriteString(\"/udp\")\n\t}\n\treturn sb.String()\n}\n\n//Encode remote to a string\nfunc (r Remote) Encode() string {\n\tif r.LocalPort == \"\" {\n\t\tr.LocalPort = r.RemotePort\n\t}\n\tlocal := r.Local()\n\tremote := r.Remote()\n\tif r.RemoteProto == \"udp\" {\n\t\tremote += \"/udp\"\n\t}\n\tif r.Reverse {\n\t\treturn \"R:\" + local + \":\" + remote\n\t}\n\treturn local + \":\" + remote\n}\n\n//Local is the decodable local portion\nfunc (r Remote) Local() string {\n\tif r.Stdio {\n\t\treturn \"stdio\"\n\t}\n\tif r.LocalHost == \"\" {\n\t\tr.LocalHost = \"0.0.0.0\"\n\t}\n\treturn r.LocalHost + \":\" + r.LocalPort\n}\n\n//Remote is the decodable remote portion\nfunc (r Remote) Remote() string {\n\tif r.Socks {\n\t\treturn \"socks\"\n\t}\n\tif r.RemoteHost == \"\" {\n\t\tr.RemoteHost = \"127.0.0.1\"\n\t}\n\treturn r.RemoteHost + \":\" + r.RemotePort\n}\n\n//UserAddr is checked when checking if a\n//user has access to a given remote\nfunc (r Remote) UserAddr() string {\n\tif r.Reverse {\n\t\treturn \"R:\" + r.LocalHost + \":\" + r.LocalPort\n\t}\n\treturn r.RemoteHost + \":\" + r.RemotePort\n}\n\n//CanListen checks if the port can be listened on\nfunc (r Remote) CanListen() bool {\n\t//valid protocols\n\tswitch r.LocalProto {\n\tcase \"tcp\":\n\t\tconn, err := net.Listen(\"tcp\", r.Local())\n\t\tif err == nil {\n\t\t\tconn.Close()\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\tcase \"udp\":\n\t\taddr, err := net.ResolveUDPAddr(\"udp\", r.Local())\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tconn, err := net.ListenUDP(r.LocalProto, addr)\n\t\tif err == nil {\n\t\t\tconn.Close()\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\t//invalid\n\treturn false\n}\n\ntype Remotes []*Remote\n\n//Filter out forward reversed/non-reversed remotes\nfunc (rs Remotes) Reversed(reverse bool) Remotes {\n\tsubset := Remotes{}\n\tfor _, r := range rs {\n\t\tmatch := r.Reverse == reverse\n\t\tif match {\n\t\t\tsubset = append(subset, r)\n\t\t}\n\t}\n\treturn subset\n}\n\n//Encode back into strings\nfunc (rs Remotes) Encode() []string {\n\ts := make([]string, len(rs))\n\tfor i, r := range rs {\n\t\ts[i] = r.Encode()\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "share/settings/remote_test.go",
    "content": "package settings\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestRemoteDecode(t *testing.T) {\n\t//test table\n\tfor i, test := range []struct {\n\t\tInput   string\n\t\tOutput  Remote\n\t\tEncoded string\n\t}{\n\t\t{\n\t\t\t\"3000\",\n\t\t\tRemote{\n\t\t\t\tLocalPort:  \"3000\",\n\t\t\t\tRemoteHost: \"127.0.0.1\",\n\t\t\t\tRemotePort: \"3000\",\n\t\t\t},\n\t\t\t\"0.0.0.0:3000:127.0.0.1:3000\",\n\t\t},\n\t\t{\n\t\t\t\"google.com:80\",\n\t\t\tRemote{\n\t\t\t\tLocalPort:  \"80\",\n\t\t\t\tRemoteHost: \"google.com\",\n\t\t\t\tRemotePort: \"80\",\n\t\t\t},\n\t\t\t\"0.0.0.0:80:google.com:80\",\n\t\t},\n\t\t{\n\t\t\t\"R:google.com:80\",\n\t\t\tRemote{\n\t\t\t\tLocalPort:  \"80\",\n\t\t\t\tRemoteHost: \"google.com\",\n\t\t\t\tRemotePort: \"80\",\n\t\t\t\tReverse:    true,\n\t\t\t},\n\t\t\t\"R:0.0.0.0:80:google.com:80\",\n\t\t},\n\t\t{\n\t\t\t\"示例網站.com:80\",\n\t\t\tRemote{\n\t\t\t\tLocalPort:  \"80\",\n\t\t\t\tRemoteHost: \"示例網站.com\",\n\t\t\t\tRemotePort: \"80\",\n\t\t\t},\n\t\t\t\"0.0.0.0:80:示例網站.com:80\",\n\t\t},\n\t\t{\n\t\t\t\"socks\",\n\t\t\tRemote{\n\t\t\t\tLocalHost: \"127.0.0.1\",\n\t\t\t\tLocalPort: \"1080\",\n\t\t\t\tSocks:     true,\n\t\t\t},\n\t\t\t\"127.0.0.1:1080:socks\",\n\t\t},\n\t\t{\n\t\t\t\"127.0.0.1:1081:socks\",\n\t\t\tRemote{\n\t\t\t\tLocalHost: \"127.0.0.1\",\n\t\t\t\tLocalPort: \"1081\",\n\t\t\t\tSocks:     true,\n\t\t\t},\n\t\t\t\"127.0.0.1:1081:socks\",\n\t\t},\n\t\t{\n\t\t\t\"1.1.1.1:53/udp\",\n\t\t\tRemote{\n\t\t\t\tLocalPort:   \"53\",\n\t\t\t\tLocalProto:  \"udp\",\n\t\t\t\tRemoteHost:  \"1.1.1.1\",\n\t\t\t\tRemotePort:  \"53\",\n\t\t\t\tRemoteProto: \"udp\",\n\t\t\t},\n\t\t\t\"0.0.0.0:53:1.1.1.1:53/udp\",\n\t\t},\n\t\t{\n\t\t\t\"localhost:5353:1.1.1.1:53/udp\",\n\t\t\tRemote{\n\t\t\t\tLocalHost:   \"localhost\",\n\t\t\t\tLocalPort:   \"5353\",\n\t\t\t\tLocalProto:  \"udp\",\n\t\t\t\tRemoteHost:  \"1.1.1.1\",\n\t\t\t\tRemotePort:  \"53\",\n\t\t\t\tRemoteProto: \"udp\",\n\t\t\t},\n\t\t\t\"localhost:5353:1.1.1.1:53/udp\",\n\t\t},\n\t\t{\n\t\t\t\"[::1]:8080:google.com:80\",\n\t\t\tRemote{\n\t\t\t\tLocalHost:  \"[::1]\",\n\t\t\t\tLocalPort:  \"8080\",\n\t\t\t\tRemoteHost: \"google.com\",\n\t\t\t\tRemotePort: \"80\",\n\t\t\t},\n\t\t\t\"[::1]:8080:google.com:80\",\n\t\t},\n\t\t{\n\t\t\t\"R:[::]:3000:[::1]:3000\",\n\t\t\tRemote{\n\t\t\t\tLocalHost:  \"[::]\",\n\t\t\t\tLocalPort:  \"3000\",\n\t\t\t\tRemoteHost: \"[::1]\",\n\t\t\t\tRemotePort: \"3000\",\n\t\t\t\tReverse:    true,\n\t\t\t},\n\t\t\t\"R:[::]:3000:[::1]:3000\",\n\t\t},\n\t} {\n\t\t//expected defaults\n\t\texpected := test.Output\n\t\tif expected.LocalHost == \"\" {\n\t\t\texpected.LocalHost = \"0.0.0.0\"\n\t\t}\n\t\tif expected.RemoteProto == \"\" {\n\t\t\texpected.RemoteProto = \"tcp\"\n\t\t}\n\t\tif expected.LocalProto == \"\" {\n\t\t\texpected.LocalProto = \"tcp\"\n\t\t}\n\t\t//compare\n\t\tgot, err := DecodeRemote(test.Input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"decode #%d '%s' failed: %s\", i+1, test.Input, err)\n\t\t}\n\t\tif !reflect.DeepEqual(got, &expected) {\n\t\t\tt.Fatalf(\"decode #%d '%s' expected\\n  %#v\\ngot\\n  %#v\", i+1, test.Input, expected, got)\n\t\t}\n\t\tif e := got.Encode(); test.Encoded != e {\n\t\t\tt.Fatalf(\"encode #%d '%s' expected\\n  %#v\\ngot\\n  %#v\", i+1, test.Input, test.Encoded, e)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "share/settings/user.go",
    "content": "package settings\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar UserAllowAll = regexp.MustCompile(\"\")\n\nfunc ParseAuth(auth string) (string, string) {\n\tif strings.Contains(auth, \":\") {\n\t\tpair := strings.SplitN(auth, \":\", 2)\n\t\treturn pair[0], pair[1]\n\t}\n\treturn \"\", \"\"\n}\n\ntype User struct {\n\tName  string\n\tPass  string\n\tAddrs []*regexp.Regexp\n}\n\nfunc (u *User) HasAccess(addr string) bool {\n\tm := false\n\tfor _, r := range u.Addrs {\n\t\tif r.MatchString(addr) {\n\t\t\tm = true\n\t\t\tbreak\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "share/settings/users.go",
    "content": "package settings\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"sync\"\n\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/jpillora/chisel/share/cio\"\n)\n\ntype Users struct {\n\tsync.RWMutex\n\tinner map[string]*User\n}\n\nfunc NewUsers() *Users {\n\treturn &Users{inner: map[string]*User{}}\n}\n\n// Len returns the numbers of users\nfunc (u *Users) Len() int {\n\tu.RLock()\n\tl := len(u.inner)\n\tu.RUnlock()\n\treturn l\n}\n\n// Get user from the index by key\nfunc (u *Users) Get(key string) (*User, bool) {\n\tu.RLock()\n\tuser, found := u.inner[key]\n\tu.RUnlock()\n\treturn user, found\n}\n\n// Set a users into the list by specific key\nfunc (u *Users) Set(key string, user *User) {\n\tu.Lock()\n\tu.inner[key] = user\n\tu.Unlock()\n}\n\n// Del ete a users from the list\nfunc (u *Users) Del(key string) {\n\tu.Lock()\n\tdelete(u.inner, key)\n\tu.Unlock()\n}\n\n// AddUser adds a users to the set\nfunc (u *Users) AddUser(user *User) {\n\tu.Set(user.Name, user)\n}\n\n// Reset all users to the given set,\n// Use nil to remove all.\nfunc (u *Users) Reset(users []*User) {\n\tm := map[string]*User{}\n\tfor _, u := range users {\n\t\tm[u.Name] = u\n\t}\n\tu.Lock()\n\tu.inner = m\n\tu.Unlock()\n}\n\n// UserIndex is a reloadable user source\ntype UserIndex struct {\n\t*cio.Logger\n\t*Users\n\tconfigFile string\n}\n\n// NewUserIndex creates a source for users\nfunc NewUserIndex(logger *cio.Logger) *UserIndex {\n\treturn &UserIndex{\n\t\tLogger: logger.Fork(\"users\"),\n\t\tUsers:  NewUsers(),\n\t}\n}\n\n// LoadUsers is responsible for loading users from a file\nfunc (u *UserIndex) LoadUsers(configFile string) error {\n\tu.configFile = configFile\n\tu.Infof(\"Loading configuration file %s\", configFile)\n\tif err := u.loadUserIndex(); err != nil {\n\t\treturn err\n\t}\n\tif err := u.addWatchEvents(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// watchEvents is responsible for watching for updates to the file and reloading\nfunc (u *UserIndex) addWatchEvents() error {\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := watcher.Add(u.configFile); err != nil {\n\t\treturn err\n\t}\n\tgo func() {\n\t\tfor e := range watcher.Events {\n\t\t\tif e.Op&fsnotify.Write != fsnotify.Write {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := u.loadUserIndex(); err != nil {\n\t\t\t\tu.Infof(\"Failed to reload the users configuration: %s\", err)\n\t\t\t} else {\n\t\t\t\tu.Debugf(\"Users configuration successfully reloaded from: %s\", u.configFile)\n\t\t\t}\n\t\t}\n\t}()\n\treturn nil\n}\n\n// loadUserIndex is responsible for loading the users configuration\nfunc (u *UserIndex) loadUserIndex() error {\n\tif u.configFile == \"\" {\n\t\treturn errors.New(\"configuration file not set\")\n\t}\n\tb, err := os.ReadFile(u.configFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to read auth file: %s, error: %s\", u.configFile, err)\n\t}\n\tvar raw map[string][]string\n\tif err := json.Unmarshal(b, &raw); err != nil {\n\t\treturn errors.New(\"Invalid JSON: \" + err.Error())\n\t}\n\tusers := []*User{}\n\tfor auth, remotes := range raw {\n\t\tuser := &User{}\n\t\tuser.Name, user.Pass = ParseAuth(auth)\n\t\tif user.Name == \"\" {\n\t\t\treturn errors.New(\"Invalid user:pass string\")\n\t\t}\n\t\tfor _, r := range remotes {\n\t\t\tif r == \"\" || r == \"*\" {\n\t\t\t\tuser.Addrs = append(user.Addrs, UserAllowAll)\n\t\t\t} else {\n\t\t\t\tre, err := regexp.Compile(r)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.New(\"Invalid address regex\")\n\t\t\t\t}\n\t\t\t\tuser.Addrs = append(user.Addrs, re)\n\t\t\t}\n\t\t}\n\t\tusers = append(users, user)\n\t}\n\t//swap\n\tu.Reset(users)\n\treturn nil\n}\n"
  },
  {
    "path": "share/tunnel/tunnel.go",
    "content": "package tunnel\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/armon/go-socks5\"\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n//Config a Tunnel\ntype Config struct {\n\t*cio.Logger\n\tInbound   bool\n\tOutbound  bool\n\tSocks     bool\n\tKeepAlive time.Duration\n\t//ACL optionally checks if a given address (host:port) is allowed.\n\t//When set, outbound connections are denied if this returns false.\n\tACL func(addr string) bool\n}\n\n//Tunnel represents an SSH tunnel with proxy capabilities.\n//Both chisel client and server are Tunnels.\n//chisel client has a single set of remotes, whereas\n//chisel server has multiple sets of remotes (one set per client).\n//Each remote has a 1:1 mapping to a proxy.\n//Proxies listen, send data over ssh, and the other end of the ssh connection\n//communicates with the endpoint and returns the response.\ntype Tunnel struct {\n\tConfig\n\t//ssh connection\n\tactiveConnMut  sync.RWMutex\n\tactivatingConn waitGroup\n\tactiveConn     ssh.Conn\n\t//proxies\n\tproxyCount int\n\t//internals\n\tconnStats   cnet.ConnCount\n\tsocksServer *socks5.Server\n}\n\n//New Tunnel from the given Config\nfunc New(c Config) *Tunnel {\n\tc.Logger = c.Logger.Fork(\"tun\")\n\tt := &Tunnel{\n\t\tConfig: c,\n\t}\n\tt.activatingConn.Add(1)\n\t//setup socks server (not listening on any port!)\n\textra := \"\"\n\tif c.Socks {\n\t\tsl := log.New(io.Discard, \"\", 0)\n\t\tif t.Logger.Debug {\n\t\t\tsl = log.New(os.Stdout, \"[socks]\", log.Ldate|log.Ltime)\n\t\t}\n\t\tt.socksServer, _ = socks5.New(&socks5.Config{Logger: sl})\n\t\textra += \" (SOCKS enabled)\"\n\t}\n\tt.Debugf(\"Created%s\", extra)\n\treturn t\n}\n\n//BindSSH provides an active SSH for use for tunnelling\nfunc (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error {\n\t//link ctx to ssh-conn\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tif c.Close() == nil {\n\t\t\tt.Debugf(\"SSH cancelled\")\n\t\t}\n\t\tt.activatingConn.DoneAll()\n\t}()\n\t//mark active and unblock\n\tt.activeConnMut.Lock()\n\tif t.activeConn != nil {\n\t\tpanic(\"double bind ssh\")\n\t}\n\tt.activeConn = c\n\tt.activeConnMut.Unlock()\n\tt.activatingConn.Done()\n\t//optional keepalive loop against this connection\n\tif t.Config.KeepAlive > 0 {\n\t\tgo t.keepAliveLoop(c)\n\t}\n\t//block until closed\n\tgo t.handleSSHRequests(reqs)\n\tgo t.handleSSHChannels(chans)\n\tt.Debugf(\"SSH connected\")\n\terr := c.Wait()\n\tt.Debugf(\"SSH disconnected\")\n\t//mark inactive and block\n\tt.activatingConn.Add(1)\n\tt.activeConnMut.Lock()\n\tt.activeConn = nil\n\tt.activeConnMut.Unlock()\n\treturn err\n}\n\n//getSSH blocks while connecting\nfunc (t *Tunnel) getSSH(ctx context.Context) ssh.Conn {\n\t//cancelled already?\n\tif isDone(ctx) {\n\t\treturn nil\n\t}\n\tt.activeConnMut.RLock()\n\tc := t.activeConn\n\tt.activeConnMut.RUnlock()\n\t//connected already?\n\tif c != nil {\n\t\treturn c\n\t}\n\t//connecting...\n\tselect {\n\tcase <-ctx.Done(): //cancelled\n\t\treturn nil\n\tcase <-time.After(settings.EnvDuration(\"SSH_WAIT\", 35*time.Second)):\n\t\treturn nil //a bit longer than ssh timeout\n\tcase <-t.activatingConnWait():\n\t\tt.activeConnMut.RLock()\n\t\tc := t.activeConn\n\t\tt.activeConnMut.RUnlock()\n\t\treturn c\n\t}\n}\n\nfunc (t *Tunnel) activatingConnWait() <-chan struct{} {\n\tch := make(chan struct{})\n\tgo func() {\n\t\tt.activatingConn.Wait()\n\t\tclose(ch)\n\t}()\n\treturn ch\n}\n\n//BindRemotes converts the given remotes into proxies, and blocks\n//until the caller cancels the context or there is a proxy error.\nfunc (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error {\n\tif len(remotes) == 0 {\n\t\treturn errors.New(\"no remotes\")\n\t}\n\tif !t.Inbound {\n\t\treturn errors.New(\"inbound connections blocked\")\n\t}\n\tproxies := make([]*Proxy, len(remotes))\n\tfor i, remote := range remotes {\n\t\tp, err := NewProxy(t.Logger, t, t.proxyCount, remote)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproxies[i] = p\n\t\tt.proxyCount++\n\t}\n\t//TODO: handle tunnel close\n\teg, ctx := errgroup.WithContext(ctx)\n\tfor _, proxy := range proxies {\n\t\tp := proxy\n\t\teg.Go(func() error {\n\t\t\treturn p.Run(ctx)\n\t\t})\n\t}\n\tt.Debugf(\"Bound proxies\")\n\terr := eg.Wait()\n\tt.Debugf(\"Unbound proxies\")\n\treturn err\n}\n\nfunc (t *Tunnel) keepAliveLoop(sshConn ssh.Conn) {\n\t//ping forever\n\tfor {\n\t\ttime.Sleep(t.Config.KeepAlive)\n\t\t_, b, err := sshConn.SendRequest(\"ping\", true, nil)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif len(b) > 0 && !bytes.Equal(b, []byte(\"pong\")) {\n\t\t\tt.Debugf(\"strange ping response\")\n\t\t\tbreak\n\t\t}\n\t}\n\t//close ssh connection on abnormal ping\n\tsshConn.Close()\n}\n"
  },
  {
    "path": "share/tunnel/tunnel_in_proxy.go",
    "content": "package tunnel\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/sizestr\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n//sshTunnel exposes a subset of Tunnel to subtypes\ntype sshTunnel interface {\n\tgetSSH(ctx context.Context) ssh.Conn\n}\n\n//Proxy is the inbound portion of a Tunnel\ntype Proxy struct {\n\t*cio.Logger\n\tsshTun sshTunnel\n\tid     int\n\tcount  int\n\tremote *settings.Remote\n\tdialer net.Dialer\n\ttcp    *net.TCPListener\n\tudp    *udpListener\n\tmu     sync.Mutex\n}\n\n//NewProxy creates a Proxy\nfunc NewProxy(logger *cio.Logger, sshTun sshTunnel, index int, remote *settings.Remote) (*Proxy, error) {\n\tid := index + 1\n\tp := &Proxy{\n\t\tLogger: logger.Fork(\"proxy#%s\", remote.String()),\n\t\tsshTun: sshTun,\n\t\tid:     id,\n\t\tremote: remote,\n\t}\n\treturn p, p.listen()\n}\n\nfunc (p *Proxy) listen() error {\n\tif p.remote.Stdio {\n\t\t//TODO check if pipes active?\n\t} else if p.remote.LocalProto == \"tcp\" {\n\t\taddr, err := net.ResolveTCPAddr(\"tcp\", p.remote.LocalHost+\":\"+p.remote.LocalPort)\n\t\tif err != nil {\n\t\t\treturn p.Errorf(\"resolve: %s\", err)\n\t\t}\n\t\tl, err := net.ListenTCP(\"tcp\", addr)\n\t\tif err != nil {\n\t\t\treturn p.Errorf(\"tcp: %s\", err)\n\t\t}\n\t\tp.Infof(\"Listening\")\n\t\tp.tcp = l\n\t} else if p.remote.LocalProto == \"udp\" {\n\t\tl, err := listenUDP(p.Logger, p.sshTun, p.remote)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.Infof(\"Listening\")\n\t\tp.udp = l\n\t} else {\n\t\treturn p.Errorf(\"unknown local proto\")\n\t}\n\treturn nil\n}\n\n//Run enables the proxy and blocks while its active,\n//close the proxy by cancelling the context.\nfunc (p *Proxy) Run(ctx context.Context) error {\n\tif p.remote.Stdio {\n\t\treturn p.runStdio(ctx)\n\t} else if p.remote.LocalProto == \"tcp\" {\n\t\treturn p.runTCP(ctx)\n\t} else if p.remote.LocalProto == \"udp\" {\n\t\treturn p.udp.run(ctx)\n\t}\n\tpanic(\"should not get here\")\n}\n\nfunc (p *Proxy) runStdio(ctx context.Context) error {\n\tdefer p.Infof(\"Closed\")\n\tfor {\n\t\tp.pipeRemote(ctx, cio.Stdio)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tdefault:\n\t\t\t// the connection is not ready yet, keep waiting\n\t\t}\n\t}\n}\n\nfunc (p *Proxy) runTCP(ctx context.Context) error {\n\tdone := make(chan struct{})\n\t//implements missing net.ListenContext\n\tgo func() {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tp.tcp.Close()\n\t\tcase <-done:\n\t\t}\n\t}()\n\tfor {\n\t\tsrc, err := p.tcp.Accept()\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\t//listener closed\n\t\t\t\terr = nil\n\t\t\tdefault:\n\t\t\t\tp.Infof(\"Accept error: %s\", err)\n\t\t\t}\n\t\t\tclose(done)\n\t\t\treturn err\n\t\t}\n\t\tgo p.pipeRemote(ctx, src)\n\t}\n}\n\nfunc (p *Proxy) pipeRemote(ctx context.Context, src io.ReadWriteCloser) {\n\tdefer src.Close()\n\n\tp.mu.Lock()\n\tp.count++\n\tcid := p.count\n\tp.mu.Unlock()\n\n\tl := p.Fork(\"conn#%d\", cid)\n\tl.Debugf(\"Open\")\n\tsshConn := p.sshTun.getSSH(ctx)\n\tif sshConn == nil {\n\t\tl.Debugf(\"No remote connection\")\n\t\treturn\n\t}\n\t//ssh request for tcp connection for this proxy's remote\n\tdst, reqs, err := sshConn.OpenChannel(\"chisel\", []byte(p.remote.Remote()))\n\tif err != nil {\n\t\tl.Infof(\"Stream error: %s\", err)\n\t\treturn\n\t}\n\tgo ssh.DiscardRequests(reqs)\n\t//then pipe\n\ts, r := cio.Pipe(src, dst)\n\tl.Debugf(\"Close (sent %s received %s)\", sizestr.ToString(s), sizestr.ToString(r))\n}\n"
  },
  {
    "path": "share/tunnel/tunnel_in_proxy_udp.go",
    "content": "package tunnel\n\nimport (\n\t\"context\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/sizestr\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n//listenUDP is a special listener which forwards packets via\n//the bound ssh connection. tricky part is multiplexing lots of\n//udp clients through the entry node. each will listen on its\n//own source-port for a response:\n//                                                (random)\n//    src-1 1111->...                         dst-1 6345->7777\n//    src-2 2222->... <---> udp <---> udp <-> dst-1 7543->7777\n//    src-3 3333->...    listener    handler  dst-1 1444->7777\n//\n//we must store these mappings (1111-6345, etc) in memory for a length\n//of time, so that when the exit node receives a response on 6345, it\n//knows to return it to 1111.\nfunc listenUDP(l *cio.Logger, sshTun sshTunnel, remote *settings.Remote) (*udpListener, error) {\n\ta, err := net.ResolveUDPAddr(\"udp\", remote.Local())\n\tif err != nil {\n\t\treturn nil, l.Errorf(\"resolve: %s\", err)\n\t}\n\tconn, err := net.ListenUDP(\"udp\", a)\n\tif err != nil {\n\t\treturn nil, l.Errorf(\"listen: %s\", err)\n\t}\n\t//ready\n\tu := &udpListener{\n\t\tLogger:  l,\n\t\tsshTun:  sshTun,\n\t\tremote:  remote,\n\t\tinbound: conn,\n\t\tmaxMTU:  settings.EnvInt(\"UDP_MAX_SIZE\", 9012),\n\t}\n\tu.Debugf(\"UDP max size: %d bytes\", u.maxMTU)\n\treturn u, nil\n}\n\ntype udpListener struct {\n\t*cio.Logger\n\tsshTun      sshTunnel\n\tremote      *settings.Remote\n\tinbound     *net.UDPConn\n\toutboundMut sync.Mutex\n\toutbound    *udpChannel\n\tsent, recv  int64\n\tmaxMTU      int\n}\n\nfunc (u *udpListener) run(ctx context.Context) error {\n\tdefer u.inbound.Close()\n\t//udp doesnt accept connections,\n\t//udp simply forwards packets\n\t//and therefore only needs to listen\n\teg, ctx := errgroup.WithContext(ctx)\n\teg.Go(func() error {\n\t\treturn u.runInbound(ctx)\n\t})\n\teg.Go(func() error {\n\t\treturn u.runOutbound(ctx)\n\t})\n\tif err := eg.Wait(); err != nil {\n\t\tu.Debugf(\"listen: %s\", err)\n\t\treturn err\n\t}\n\tu.Debugf(\"Close (sent %s received %s)\", sizestr.ToString(u.sent), sizestr.ToString(u.recv))\n\treturn nil\n}\n\nfunc (u *udpListener) runInbound(ctx context.Context) error {\n\tbuff := make([]byte, u.maxMTU)\n\tfor !isDone(ctx) {\n\t\t//read from inbound udp\n\t\tu.inbound.SetReadDeadline(time.Now().Add(time.Second))\n\t\tn, addr, err := u.inbound.ReadFromUDP(buff)\n\t\tif e, ok := err.(net.Error); ok && (e.Timeout() || e.Temporary()) {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn u.Errorf(\"read error: %w\", err)\n\t\t}\n\t\t//upsert ssh channel\n\t\tuc, err := u.getUDPChan(ctx)\n\t\tif err != nil {\n\t\t\tif strings.HasSuffix(err.Error(), \"EOF\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn u.Errorf(\"inbound-udpchan: %w\", err)\n\t\t}\n\t\t//send over channel, including source address\n\t\tb := buff[:n]\n\t\tif err := uc.encode(addr.String(), b); err != nil {\n\t\t\tif strings.HasSuffix(err.Error(), \"EOF\") {\n\t\t\t\tcontinue //dropped packet...\n\t\t\t}\n\t\t\treturn u.Errorf(\"encode error: %w\", err)\n\t\t}\n\t\t//stats\n\t\tatomic.AddInt64(&u.sent, int64(n))\n\t}\n\treturn nil\n}\n\nfunc (u *udpListener) runOutbound(ctx context.Context) error {\n\tfor !isDone(ctx) {\n\t\t//upsert ssh channel\n\t\tuc, err := u.getUDPChan(ctx)\n\t\tif err != nil {\n\t\t\tif strings.HasSuffix(err.Error(), \"EOF\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn u.Errorf(\"outbound-udpchan: %w\", err)\n\t\t}\n\t\t//receive from channel, including source address\n\t\tp := udpPacket{}\n\t\tif err := uc.decode(&p); err == io.EOF {\n\t\t\t//outbound ssh disconnected, get new connection...\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\treturn u.Errorf(\"decode error: %w\", err)\n\t\t}\n\t\t//write back to inbound udp\n\t\taddr, err := net.ResolveUDPAddr(\"udp\", p.Src)\n\t\tif err != nil {\n\t\t\treturn u.Errorf(\"resolve error: %w\", err)\n\t\t}\n\t\tn, err := u.inbound.WriteToUDP(p.Payload, addr)\n\t\tif err != nil {\n\t\t\treturn u.Errorf(\"write error: %w\", err)\n\t\t}\n\t\t//stats\n\t\tatomic.AddInt64(&u.recv, int64(n))\n\t}\n\treturn nil\n}\n\nfunc (u *udpListener) getUDPChan(ctx context.Context) (*udpChannel, error) {\n\tu.outboundMut.Lock()\n\tdefer u.outboundMut.Unlock()\n\t//cached\n\tif u.outbound != nil {\n\t\treturn u.outbound, nil\n\t}\n\t//not cached, bind\n\tsshConn := u.sshTun.getSSH(ctx)\n\tif sshConn == nil {\n\t\treturn nil, fmt.Errorf(\"ssh-conn nil\")\n\t}\n\t//ssh request for udp packets for this proxy's remote,\n\t//just \"udp\" since the remote address is sent with each packet\n\tdstAddr := u.remote.Remote() + \"/udp\"\n\trwc, reqs, err := sshConn.OpenChannel(\"chisel\", []byte(dstAddr))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ssh-chan error: %s\", err)\n\t}\n\tgo ssh.DiscardRequests(reqs)\n\t//remove on disconnect\n\tgo u.unsetUDPChan(sshConn)\n\t//ready\n\to := &udpChannel{\n\t\tr: gob.NewDecoder(rwc),\n\t\tw: gob.NewEncoder(rwc),\n\t\tc: rwc,\n\t}\n\tu.outbound = o\n\tu.Debugf(\"aquired channel\")\n\treturn o, nil\n}\n\nfunc (u *udpListener) unsetUDPChan(sshConn ssh.Conn) {\n\tsshConn.Wait()\n\tu.Debugf(\"lost channel\")\n\tu.outboundMut.Lock()\n\tu.outbound = nil\n\tu.outboundMut.Unlock()\n}\n"
  },
  {
    "path": "share/tunnel/tunnel_out_ssh.go",
    "content": "package tunnel\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\t\"github.com/jpillora/sizestr\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc (t *Tunnel) handleSSHRequests(reqs <-chan *ssh.Request) {\n\tfor r := range reqs {\n\t\tswitch r.Type {\n\t\tcase \"ping\":\n\t\t\tr.Reply(true, []byte(\"pong\"))\n\t\tdefault:\n\t\t\tt.Debugf(\"Unknown request: %s\", r.Type)\n\t\t}\n\t}\n}\n\nfunc (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) {\n\tfor ch := range chans {\n\t\tgo t.handleSSHChannel(ch)\n\t}\n}\n\nfunc (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) {\n\tif !t.Config.Outbound {\n\t\tt.Debugf(\"Denied outbound connection\")\n\t\tch.Reject(ssh.Prohibited, \"Denied outbound connection\")\n\t\treturn\n\t}\n\tremote := string(ch.ExtraData())\n\t//extract protocol\n\thostPort, proto := settings.L4Proto(remote)\n\tudp := proto == \"udp\"\n\tsocks := hostPort == \"socks\"\n\tif socks && t.socksServer == nil {\n\t\tt.Debugf(\"Denied socks request, please enable socks\")\n\t\tch.Reject(ssh.Prohibited, \"SOCKS5 is not enabled\")\n\t\treturn\n\t}\n\t//check ACL against the actual requested destination\n\tif t.Config.ACL != nil && !socks && !t.Config.ACL(hostPort) {\n\t\tt.Debugf(\"Denied connection to %s (ACL)\", hostPort)\n\t\tch.Reject(ssh.Prohibited, \"access denied\")\n\t\treturn\n\t}\n\tsshChan, reqs, err := ch.Accept()\n\tif err != nil {\n\t\tt.Debugf(\"Failed to accept stream: %s\", err)\n\t\treturn\n\t}\n\tstream := io.ReadWriteCloser(sshChan)\n\t//cnet.MeterRWC(t.Logger.Fork(\"sshchan\"), sshChan)\n\tdefer stream.Close()\n\tgo ssh.DiscardRequests(reqs)\n\tl := t.Logger.Fork(\"conn#%d\", t.connStats.New())\n\t//ready to handle\n\tt.connStats.Open()\n\tl.Debugf(\"Open %s\", t.connStats.String())\n\tif socks {\n\t\terr = t.handleSocks(stream)\n\t} else if udp {\n\t\terr = t.handleUDP(l, stream, hostPort)\n\t} else {\n\t\terr = t.handleTCP(l, stream, hostPort)\n\t}\n\tt.connStats.Close()\n\terrmsg := \"\"\n\tif err != nil && !strings.HasSuffix(err.Error(), \"EOF\") {\n\t\terrmsg = fmt.Sprintf(\" (error %s)\", err)\n\t}\n\tl.Debugf(\"Close %s%s\", t.connStats.String(), errmsg)\n}\n\nfunc (t *Tunnel) handleSocks(src io.ReadWriteCloser) error {\n\treturn t.socksServer.ServeConn(cnet.NewRWCConn(src))\n}\n\nfunc (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error {\n\tdst, err := net.Dial(\"tcp\", hostPort)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts, r := cio.Pipe(src, dst)\n\tl.Debugf(\"sent %s received %s\", sizestr.ToString(s), sizestr.ToString(r))\n\treturn nil\n}\n"
  },
  {
    "path": "share/tunnel/tunnel_out_ssh_udp.go",
    "content": "package tunnel\n\nimport (\n\t\"encoding/gob\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jpillora/chisel/share/cio\"\n\t\"github.com/jpillora/chisel/share/settings\"\n)\n\nfunc (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error {\n\tconns := &udpConns{\n\t\tLogger: l,\n\t\tm:      map[string]*udpConn{},\n\t}\n\tdefer conns.closeAll()\n\th := &udpHandler{\n\t\tLogger:   l,\n\t\thostPort: hostPort,\n\t\tudpChannel: &udpChannel{\n\t\t\tr: gob.NewDecoder(rwc),\n\t\t\tw: gob.NewEncoder(rwc),\n\t\t\tc: rwc,\n\t\t},\n\t\tudpConns: conns,\n\t\tmaxMTU:   settings.EnvInt(\"UDP_MAX_SIZE\", 9012),\n\t}\n\th.Debugf(\"UDP max size: %d bytes\", h.maxMTU)\n\tfor {\n\t\tp := udpPacket{}\n\t\tif err := h.handleWrite(&p); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\ntype udpHandler struct {\n\t*cio.Logger\n\thostPort string\n\t*udpChannel\n\t*udpConns\n\tmaxMTU int\n}\n\nfunc (h *udpHandler) handleWrite(p *udpPacket) error {\n\tif err := h.r.Decode(&p); err != nil {\n\t\treturn err\n\t}\n\t//dial now, we know we must write\n\tconn, exists, err := h.udpConns.dial(p.Src, h.hostPort)\n\tif err != nil {\n\t\treturn err\n\t}\n\t//however, we dont know if we must read...\n\t//spawn up to <max-conns> go-routines to wait\n\t//for a reply.\n\t//TODO configurable\n\t//TODO++ dont use go-routines, switch to pollable\n\t//  array of listeners where all listeners are\n\t//  sweeped periodically, removing the idle ones\n\tconst maxConns = 100\n\tif !exists {\n\t\tif h.udpConns.len() <= maxConns {\n\t\t\tgo h.handleRead(p, conn)\n\t\t} else {\n\t\t\th.Debugf(\"exceeded max udp connections (%d)\", maxConns)\n\t\t}\n\t}\n\t_, err = conn.Write(p.Payload)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (h *udpHandler) handleRead(p *udpPacket, conn *udpConn) {\n\t//ensure connection is cleaned up\n\tdefer h.udpConns.remove(conn.id)\n\tbuff := make([]byte, h.maxMTU)\n\tfor {\n\t\t//response must arrive within 15 seconds\n\t\tdeadline := settings.EnvDuration(\"UDP_DEADLINE\", 15*time.Second)\n\t\tconn.SetReadDeadline(time.Now().Add(deadline))\n\t\t//read response\n\t\tn, err := conn.Read(buff)\n\t\tif err != nil {\n\t\t\tif !os.IsTimeout(err) && err != io.EOF {\n\t\t\t\th.Debugf(\"read error: %s\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tb := buff[:n]\n\t\t//encode back over ssh connection\n\t\terr = h.udpChannel.encode(p.Src, b)\n\t\tif err != nil {\n\t\t\th.Debugf(\"encode error: %s\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\ntype udpConns struct {\n\t*cio.Logger\n\tsync.Mutex\n\tm map[string]*udpConn\n}\n\nfunc (cs *udpConns) dial(id, addr string) (*udpConn, bool, error) {\n\tcs.Lock()\n\tdefer cs.Unlock()\n\tconn, ok := cs.m[id]\n\tif !ok {\n\t\tc, err := net.Dial(\"udp\", addr)\n\t\tif err != nil {\n\t\t\treturn nil, false, err\n\t\t}\n\t\tconn = &udpConn{\n\t\t\tid:   id,\n\t\t\tConn: c, // cnet.MeterConn(cs.Logger.Fork(addr), c),\n\t\t}\n\t\tcs.m[id] = conn\n\t}\n\treturn conn, ok, nil\n}\n\nfunc (cs *udpConns) len() int {\n\tcs.Lock()\n\tl := len(cs.m)\n\tcs.Unlock()\n\treturn l\n}\n\nfunc (cs *udpConns) remove(id string) {\n\tcs.Lock()\n\tdelete(cs.m, id)\n\tcs.Unlock()\n}\n\nfunc (cs *udpConns) closeAll() {\n\tcs.Lock()\n\tfor id, conn := range cs.m {\n\t\tconn.Close()\n\t\tdelete(cs.m, id)\n\t}\n\tcs.Unlock()\n}\n\ntype udpConn struct {\n\tid string\n\tnet.Conn\n}\n"
  },
  {
    "path": "share/tunnel/udp.go",
    "content": "package tunnel\n\nimport (\n\t\"context\"\n\t\"encoding/gob\"\n\t\"io\"\n)\n\ntype udpPacket struct {\n\tSrc     string\n\tPayload []byte\n}\n\nfunc init() {\n\tgob.Register(&udpPacket{})\n}\n\n//udpChannel encodes/decodes udp payloads over a stream\ntype udpChannel struct {\n\tr *gob.Decoder\n\tw *gob.Encoder\n\tc io.Closer\n}\n\nfunc (o *udpChannel) encode(src string, b []byte) error {\n\treturn o.w.Encode(udpPacket{\n\t\tSrc:     src,\n\t\tPayload: b,\n\t})\n}\n\nfunc (o *udpChannel) decode(p *udpPacket) error {\n\treturn o.r.Decode(p)\n}\n\nfunc isDone(ctx context.Context) bool {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "share/tunnel/wg.go",
    "content": "package tunnel\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\ntype waitGroup struct {\n\tinner sync.WaitGroup\n\tn     int32\n}\n\nfunc (w *waitGroup) Add(n int) {\n\tatomic.AddInt32(&w.n, int32(n))\n\tw.inner.Add(n)\n}\n\nfunc (w *waitGroup) Done() {\n\tif n := atomic.LoadInt32(&w.n); n > 0 && atomic.CompareAndSwapInt32(&w.n, n, n-1) {\n\t\tw.inner.Done()\n\t}\n}\n\nfunc (w *waitGroup) DoneAll() {\n\tfor atomic.LoadInt32(&w.n) > 0 {\n\t\tw.Done()\n\t}\n}\n\nfunc (w *waitGroup) Wait() {\n\tw.inner.Wait()\n}\n"
  },
  {
    "path": "share/version.go",
    "content": "package chshare\n\n//ProtocolVersion of chisel. When backwards\n//incompatible changes are made, this will\n//be incremented to signify a protocol\n//mismatch.\nvar ProtocolVersion = \"chisel-v3\"\n\nvar BuildVersion = \"0.0.0-src\"\n"
  },
  {
    "path": "test/bench/main.go",
    "content": "//chisel end-to-end test\n//======================\n//\n//                    (direct)\n//         .--------------->----------------.\n//        /    chisel         chisel         \\\n// request--->client:2001--->server:2002---->fileserver:3000\n//        \\                                  /\n//         '--> crowbar:4001--->crowbar:4002'\n//              client           server\n//\n// crowbar and chisel binaries should be in your PATH\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"strconv\"\n\n\t\"github.com/jpillora/chisel/share/cnet\"\n\n\t\"time\"\n)\n\nconst ENABLE_CROWBAR = false\n\nconst (\n\tB  = 1\n\tKB = 1000 * B\n\tMB = 1000 * KB\n\tGB = 1000 * MB\n)\n\nfunc run() {\n\tflag.Parse()\n\targs := flag.Args()\n\tif len(args) == 0 {\n\t\tfatal(\"go run main.go [test] or [bench]\")\n\t}\n\tfor _, a := range args {\n\t\tswitch a {\n\t\tcase \"test\":\n\t\t\ttest()\n\t\tcase \"bench\":\n\t\t\tbench()\n\t\t}\n\t}\n}\n\n//test\nfunc test() {\n\ttestTunnel(\"2001\", 500)\n\ttestTunnel(\"2001\", 50000)\n}\n\n//benchmark\nfunc bench() {\n\tbenchSizes(\"3000\")\n\tbenchSizes(\"2001\")\n\tif ENABLE_CROWBAR {\n\t\tbenchSizes(\"4001\")\n\t}\n}\n\nfunc benchSizes(port string) {\n\tfor size := 1; size <= 100*MB; size *= 10 {\n\t\ttestTunnel(port, size)\n\t}\n}\n\nfunc testTunnel(port string, size int) {\n\tt0 := time.Now()\n\tresp, err := requestFile(port, size)\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\tif resp.StatusCode != 200 {\n\t\tfatal(err)\n\t}\n\n\tn, err := io.Copy(io.Discard, resp.Body)\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\tt1 := time.Now()\n\tfmt.Printf(\":%s => %d bytes in %s\\n\", port, size, t1.Sub(t0))\n\tif int(n) != size {\n\t\tfatalf(\"%d bytes expected, got %d\", size, n)\n\t}\n}\n\n//============================\n\nfunc requestFile(port string, size int) (*http.Response, error) {\n\turl := \"http://127.0.0.1:\" + port + \"/\" + strconv.Itoa(size)\n\t// fmt.Println(url)\n\treturn http.Get(url)\n}\n\nfunc makeFileServer() *cnet.HTTPServer {\n\tbsize := 3 * MB\n\tbytes := make([]byte, bsize)\n\t//filling huge buffer\n\tfor i := 0; i < len(bytes); i++ {\n\t\tbytes[i] = byte(i)\n\t}\n\n\ts := cnet.NewHTTPServer()\n\ts.Server.SetKeepAlivesEnabled(false)\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\trsize, _ := strconv.Atoi(r.URL.Path[1:])\n\t\tfor rsize >= bsize {\n\t\t\tw.Write(bytes)\n\t\t\trsize -= bsize\n\t\t}\n\t\tw.Write(bytes[:rsize])\n\t})\n\ts.GoListenAndServe(\"0.0.0.0:3000\", handler)\n\treturn s\n}\n\n//============================\n\nfunc fatal(args ...interface{}) {\n\tpanic(fmt.Sprint(args...))\n}\nfunc fatalf(f string, args ...interface{}) {\n\tpanic(fmt.Sprintf(f, args...))\n}\n\n//global setup\nfunc main() {\n\n\tfs := makeFileServer()\n\tgo func() {\n\t\terr := fs.Wait()\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"fs server closed (%s)\\n\", err)\n\t\t}\n\t}()\n\n\tif ENABLE_CROWBAR {\n\t\tdir, _ := os.Getwd()\n\t\tcd := exec.Command(\"crowbard\",\n\t\t\t`-listen`, \"0.0.0.0:4002\",\n\t\t\t`-userfile`, path.Join(dir, \"userfile\"))\n\t\tif err := cd.Start(); err != nil {\n\t\t\tfatal(err)\n\t\t}\n\t\tgo func() {\n\t\t\tfatalf(\"crowbard: %v\", cd.Wait())\n\t\t}()\n\t\tdefer cd.Process.Kill()\n\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\tcf := exec.Command(\"crowbar-forward\",\n\t\t\t\"-local=0.0.0.0:4001\",\n\t\t\t\"-server=http://127.0.0.1:4002\",\n\t\t\t\"-remote=127.0.0.1:3000\",\n\t\t\t\"-username\", \"foo\",\n\t\t\t\"-password\", \"bar\")\n\t\tif err := cf.Start(); err != nil {\n\t\t\tfatal(err)\n\t\t}\n\t\tdefer cf.Process.Kill()\n\t}\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\thd := exec.Command(\"chisel\", \"server\",\n\t\t// \"-v\",\n\t\t\"--key\", \"foobar\",\n\t\t\"--port\", \"2002\")\n\thd.Stdout = os.Stdout\n\tif err := hd.Start(); err != nil {\n\t\tfatal(err)\n\t}\n\tdefer hd.Process.Kill()\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\thf := exec.Command(\"chisel\", \"client\",\n\t\t// \"-v\",\n\t\t\"--fingerprint\", \"mOz4rg9zlQ409XAhhj6+fDDVwQMY42CL3Zg2W2oTYxA=\",\n\t\t\"127.0.0.1:2002\",\n\t\t\"2001:3000\")\n\thf.Stdout = os.Stdout\n\tif err := hf.Start(); err != nil {\n\t\tfatal(err)\n\t}\n\tdefer hf.Process.Kill()\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Print(r)\n\t\t}\n\t}()\n\trun()\n\n\tfs.Close()\n}\n"
  },
  {
    "path": "test/bench/perf.md",
    "content": "\n### Performance\n\nWith [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.\n\nIn this simple benchmark, we have:\n\n```\n\t\t\t\t\t(direct)\n        .--------------->----------------.\n       /    chisel         chisel         \\\nrequest--->client:2001--->server:2002---->fileserver:3000\n       \\                                  /\n        '--> crowbar:4001--->crowbar:4002'\n             client           server\n```\n\nNote, we're using an in-memory \"file\" server on localhost for these tests\n\n_direct_\n\n```\n:3000 => 1 bytes in 1.291417ms\n:3000 => 10 bytes in 713.525µs\n:3000 => 100 bytes in 562.48µs\n:3000 => 1000 bytes in 595.445µs\n:3000 => 10000 bytes in 1.053298ms\n:3000 => 100000 bytes in 741.351µs\n:3000 => 1000000 bytes in 1.367143ms\n:3000 => 10000000 bytes in 8.601549ms\n:3000 => 100000000 bytes in 76.3939ms\n```\n\n`chisel`\n\n```\n:2001 => 1 bytes in 1.351976ms\n:2001 => 10 bytes in 1.106086ms\n:2001 => 100 bytes in 1.005729ms\n:2001 => 1000 bytes in 1.254396ms\n:2001 => 10000 bytes in 1.139777ms\n:2001 => 100000 bytes in 2.35437ms\n:2001 => 1000000 bytes in 11.502673ms\n:2001 => 10000000 bytes in 123.130246ms\n:2001 => 100000000 bytes in 966.48636ms\n```\n\n~100MB in **~1 second**\n\n`crowbar`\n\n```\n:4001 => 1 bytes in 3.335797ms\n:4001 => 10 bytes in 1.453007ms\n:4001 => 100 bytes in 1.811727ms\n:4001 => 1000 bytes in 1.621525ms\n:4001 => 10000 bytes in 5.20729ms\n:4001 => 100000 bytes in 38.461926ms\n:4001 => 1000000 bytes in 358.784864ms\n:4001 => 10000000 bytes in 3.603206487s\n:4001 => 100000000 bytes in 36.332395213s\n```\n\n~100MB in **36 seconds**\n\nSee `test/bench/main.go`"
  },
  {
    "path": "test/bench/userfile",
    "content": "foo:bar"
  },
  {
    "path": "test/e2e/acl_channel_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\tchserver \"github.com/jpillora/chisel/server\"\n\t\"github.com/jpillora/chisel/share/cnet\"\n\t\"github.com/jpillora/chisel/share/settings\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// dialChiselSSH connects to the chisel server via websocket and\n// performs an SSH handshake as the given user.\nfunc dialChiselSSH(t *testing.T, serverAddr, user, pass string) (ssh.Conn, <-chan ssh.NewChannel, <-chan *ssh.Request) {\n\tt.Helper()\n\tws, _, err := (&websocket.Dialer{\n\t\tHandshakeTimeout: 5 * time.Second,\n\t\tSubprotocols:     []string{\"chisel-v3\"},\n\t}).Dial(\"ws://\"+serverAddr, http.Header{})\n\tif err != nil {\n\t\tt.Fatalf(\"websocket dial: %v\", err)\n\t}\n\tconn := cnet.NewWebSocketConn(ws)\n\tsc, chans, reqs, err := ssh.NewClientConn(conn, \"\", &ssh.ClientConfig{\n\t\tUser:            user,\n\t\tAuth:            []ssh.AuthMethod{ssh.Password(pass)},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"ssh handshake: %v\", err)\n\t}\n\tgo ssh.DiscardRequests(reqs)\n\tgo func() { for c := range chans { c.Reject(ssh.Prohibited, \"\") } }()\n\treturn sc, chans, reqs\n}\n\n// sendConfig sends the chisel config request with the given remotes.\nfunc sendConfig(t *testing.T, sc ssh.Conn, remotes []*settings.Remote) {\n\tt.Helper()\n\tcfg, err := json.Marshal(settings.Config{Version: \"0\", Remotes: remotes})\n\tif err != nil {\n\t\tt.Fatalf(\"marshal config: %v\", err)\n\t}\n\tok, reply, err := sc.SendRequest(\"config\", true, cfg)\n\tif err != nil {\n\t\tt.Fatalf(\"config request: %v\", err)\n\t}\n\tif !ok {\n\t\tt.Fatalf(\"config rejected: %s\", reply)\n\t}\n}\n\n// TestAuthChannelDenied verifies that a channel to an unauthorized\n// destination is rejected.\nfunc TestAuthChannelDenied(t *testing.T) {\n\tallowedPort := availablePort()\n\tblockedPort := availablePort()\n\n\tblockedListener, err := net.Listen(\"tcp\", \"127.0.0.1:\"+blockedPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer blockedListener.Close()\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := blockedListener.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconn.Write([]byte(\"FORBIDDEN\"))\n\t\t\tconn.Close()\n\t\t}\n\t}()\n\n\t// Start chisel server with ACL: user can only reach allowedPort\n\ts, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: \"acl-test\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts.Debug = debug\n\tif err := s.AddUser(\"user\", \"pass\", fmt.Sprintf(`^127\\.0\\.0\\.1:%s$`, allowedPort)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tserverPort := availablePort()\n\tif err := s.Start(\"127.0.0.1\", serverPort); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer s.Close()\n\n\tserverAddr := \"127.0.0.1:\" + serverPort\n\n\t// Connect and send config with only the allowed remote\n\tsc, _, _ := dialChiselSSH(t, serverAddr, \"user\", \"pass\")\n\tdefer sc.Close()\n\n\tr, err := settings.DecodeRemote(fmt.Sprintf(\"0.0.0.0:%s:127.0.0.1:%s\", allowedPort, allowedPort))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsendConfig(t, sc, []*settings.Remote{r})\n\n\t// Try to open a channel to the BLOCKED port — must be rejected\n\ttarget := net.JoinHostPort(\"127.0.0.1\", blockedPort)\n\tch, _, err := sc.OpenChannel(\"chisel\", []byte(target))\n\tif err == nil {\n\t\tch.Close()\n\t\tt.Fatalf(\"channel to blocked port %s was accepted\", blockedPort)\n\t}\n\tt.Logf(\"channel to blocked port correctly rejected: %v\", err)\n}\n\n// TestAuthChannelAllowed verifies that a channel to an authorized\n// destination is accepted.\nfunc TestAuthChannelAllowed(t *testing.T) {\n\tallowedPort := availablePort()\n\n\t// Start a TCP listener on the allowed port\n\tallowedListener, err := net.Listen(\"tcp\", \"127.0.0.1:\"+allowedPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer allowedListener.Close()\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := allowedListener.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconn.Write([]byte(\"ALLOWED\"))\n\t\t\tconn.Close()\n\t\t}\n\t}()\n\n\t// Start chisel server with ACL: user can only reach allowedPort\n\ts, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: \"acl-test-allowed\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts.Debug = debug\n\tif err := s.AddUser(\"user\", \"pass\", fmt.Sprintf(`^127\\.0\\.0\\.1:%s$`, allowedPort)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tserverPort := availablePort()\n\tif err := s.Start(\"127.0.0.1\", serverPort); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer s.Close()\n\n\tserverAddr := \"127.0.0.1:\" + serverPort\n\n\t// Connect and send config with the allowed remote\n\tsc, _, _ := dialChiselSSH(t, serverAddr, \"user\", \"pass\")\n\tdefer sc.Close()\n\n\tr, err := settings.DecodeRemote(fmt.Sprintf(\"0.0.0.0:%s:127.0.0.1:%s\", allowedPort, allowedPort))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsendConfig(t, sc, []*settings.Remote{r})\n\n\t// Open channel to the allowed port — must succeed\n\ttarget := net.JoinHostPort(\"127.0.0.1\", allowedPort)\n\tch, reqs, err := sc.OpenChannel(\"chisel\", []byte(target))\n\tif err != nil {\n\t\tt.Fatalf(\"channel to allowed port %s was rejected: %v\", allowedPort, err)\n\t}\n\tgo ssh.DiscardRequests(reqs)\n\tdefer ch.Close()\n\n\t// Read data from the allowed target\n\tbuf := make([]byte, 64)\n\tn, err := ch.Read(buf)\n\tif err != nil && err != io.EOF {\n\t\tt.Fatalf(\"read from allowed channel: %v\", err)\n\t}\n\tif string(buf[:n]) != \"ALLOWED\" {\n\t\tt.Fatalf(\"expected 'ALLOWED', got %q\", buf[:n])\n\t}\n\tt.Logf(\"channel to allowed port works correctly, received: %s\", buf[:n])\n}\n\n// TestNoAuthChannel verifies that when no auth is configured,\n// all destinations are reachable.\nfunc TestNoAuthChannel(t *testing.T) {\n\ttargetPort := availablePort()\n\n\t// Start a TCP listener\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:\"+targetPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer listener.Close()\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconn.Write([]byte(\"OPEN\"))\n\t\t\tconn.Close()\n\t\t}\n\t}()\n\n\t// Start chisel server with NO auth\n\ts, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: \"no-acl-test\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts.Debug = debug\n\tserverPort := availablePort()\n\tif err := s.Start(\"127.0.0.1\", serverPort); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer s.Close()\n\n\tserverAddr := \"127.0.0.1:\" + serverPort\n\n\t// Connect with any credentials (server accepts all when no auth configured)\n\tsc, _, _ := dialChiselSSH(t, serverAddr, \"anyone\", \"anything\")\n\tdefer sc.Close()\n\n\tr, err := settings.DecodeRemote(fmt.Sprintf(\"0.0.0.0:%s:127.0.0.1:%s\", targetPort, targetPort))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsendConfig(t, sc, []*settings.Remote{r})\n\n\t// Open channel — should be accepted since no ACL\n\ttarget := net.JoinHostPort(\"127.0.0.1\", targetPort)\n\tch, creqs, err := sc.OpenChannel(\"chisel\", []byte(target))\n\tif err != nil {\n\t\tt.Fatalf(\"channel rejected when no ACL is configured: %v\", err)\n\t}\n\tgo ssh.DiscardRequests(creqs)\n\tdefer ch.Close()\n\n\tbuf := make([]byte, 64)\n\tn, err := ch.Read(buf)\n\tif err != nil && err != io.EOF {\n\t\tt.Fatalf(\"read: %v\", err)\n\t}\n\tif string(buf[:n]) != \"OPEN\" {\n\t\tt.Fatalf(\"expected 'OPEN', got %q\", buf[:n])\n\t}\n\tt.Logf(\"no-ACL mode works correctly\")\n}\n\n// TestAuthWildcardChannel verifies that a user with wildcard access\n// can reach any destination.\nfunc TestAuthWildcardChannel(t *testing.T) {\n\ttargetPort := availablePort()\n\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:\"+targetPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer listener.Close()\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconn.Write([]byte(\"WILDCARD\"))\n\t\t\tconn.Close()\n\t\t}\n\t}()\n\n\ts, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: \"acl-wildcard-test\",\n\t\tAuth:    \"admin:secret\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts.Debug = debug\n\tserverPort := availablePort()\n\tif err := s.Start(\"127.0.0.1\", serverPort); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer s.Close()\n\n\tsc, _, _ := dialChiselSSH(t, \"127.0.0.1:\"+serverPort, \"admin\", \"secret\")\n\tdefer sc.Close()\n\n\tr, err := settings.DecodeRemote(fmt.Sprintf(\"0.0.0.0:%s:127.0.0.1:%s\", targetPort, targetPort))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsendConfig(t, sc, []*settings.Remote{r})\n\n\ttarget := net.JoinHostPort(\"127.0.0.1\", targetPort)\n\tch, reqs, err := sc.OpenChannel(\"chisel\", []byte(target))\n\tif err != nil {\n\t\tt.Fatalf(\"wildcard user channel rejected: %v\", err)\n\t}\n\tgo ssh.DiscardRequests(reqs)\n\tdefer ch.Close()\n\n\tbuf := make([]byte, 64)\n\tn, err := ch.Read(buf)\n\tif err != nil && err != io.EOF {\n\t\tt.Fatalf(\"read: %v\", err)\n\t}\n\tif string(buf[:n]) != \"WILDCARD\" {\n\t\tt.Fatalf(\"expected 'WILDCARD', got %q\", buf[:n])\n\t}\n\tt.Logf(\"wildcard user correctly allowed\")\n}\n"
  },
  {
    "path": "test/e2e/auth_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"testing\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\n//TODO tests for:\n// - failed auth\n// - dynamic auth (server add/remove user)\n// - watch auth file\n\nfunc TestAuth(t *testing.T) {\n\ttmpPort1 := availablePort()\n\ttmpPort2 := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tKeySeed: \"foobar\",\n\t\t\tAuth:    \"../bench/userfile\",\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{\n\t\t\t\t\"0.0.0.0:\" + tmpPort1 + \":127.0.0.1:$FILEPORT\",\n\t\t\t\t\"0.0.0.0:\" + tmpPort2 + \":localhost:$FILEPORT\",\n\t\t\t},\n\t\t\tAuth: \"foo:bar\",\n\t\t})\n\tdefer teardown()\n\t//test first remote\n\tresult, err := post(\"http://localhost:\"+tmpPort1, \"foo\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"foo!\" {\n\t\tt.Fatalf(\"expected exclamation mark added\")\n\t}\n\t//test second remote\n\tresult, err = post(\"http://localhost:\"+tmpPort2, \"bar\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"bar!\" {\n\t\tt.Fatalf(\"expected exclamation mark added again\")\n\t}\n}\n"
  },
  {
    "path": "test/e2e/base_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"testing\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\nfunc TestBase(t *testing.T) {\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t})\n\tdefer teardown()\n\t//test remote\n\tresult, err := post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"foo!\" {\n\t\tt.Fatalf(\"expected exclamation mark added\")\n\t}\n}\n\nfunc TestReverse(t *testing.T) {\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tReverse: true,\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{\"R:\" + tmpPort + \":$FILEPORT\"},\n\t\t})\n\tdefer teardown()\n\t//test remote (this goes through the server and out the client)\n\tresult, err := post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"foo!\" {\n\t\tt.Fatalf(\"expected exclamation mark added\")\n\t}\n}\n"
  },
  {
    "path": "test/e2e/cert_utils_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\ntype tlsConfig struct {\n\tserverTLS *chserver.TLSConfig\n\tclientTLS *chclient.TLSConfig\n\ttmpDir    string\n}\n\nfunc (t *tlsConfig) Close() {\n\tif t.tmpDir != \"\" {\n\t\tos.RemoveAll(t.tmpDir)\n\t}\n}\n\nfunc newTestTLSConfig() (*tlsConfig, error) {\n\ttlsConfig := &tlsConfig{}\n\t_, serverCertPEM, serverKeyPEM, err := certGetCertificate(&certConfig{\n\t\thosts: []string{\n\t\t\t\"0.0.0.0\",\n\t\t\t\"localhost\",\n\t\t},\n\t\textKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, clientCertPEM, clientKeyPEM, err := certGetCertificate(&certConfig{\n\t\textKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttlsConfig.tmpDir, err = os.MkdirTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdirServerCA := path.Join(tlsConfig.tmpDir, \"server-ca\")\n\tif err := os.Mkdir(dirServerCA, 0777); err != nil {\n\t\treturn nil, err\n\t}\n\tpathServerCACrt := path.Join(dirServerCA, \"client.crt\")\n\tif err := os.WriteFile(pathServerCACrt, clientCertPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdirClientCA := path.Join(tlsConfig.tmpDir, \"client-ca\")\n\tif err := os.Mkdir(dirClientCA, 0777); err != nil {\n\t\treturn nil, err\n\t}\n\tpathClientCACrt := path.Join(dirClientCA, \"server.crt\")\n\tif err := os.WriteFile(pathClientCACrt, serverCertPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdirServerCrt := path.Join(tlsConfig.tmpDir, \"server-crt\")\n\tif err := os.Mkdir(dirServerCrt, 0777); err != nil {\n\t\treturn nil, err\n\t}\n\tpathServerCrtCrt := path.Join(dirServerCrt, \"server.crt\")\n\tif err := os.WriteFile(pathServerCrtCrt, serverCertPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\tpathServerCrtKey := path.Join(dirServerCrt, \"server.key\")\n\tif err := os.WriteFile(pathServerCrtKey, serverKeyPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdirClientCrt := path.Join(tlsConfig.tmpDir, \"client-crt\")\n\tif err := os.Mkdir(dirClientCrt, 0777); err != nil {\n\t\treturn nil, err\n\t}\n\tpathClientCrtCrt := path.Join(dirClientCrt, \"client.crt\")\n\tif err := os.WriteFile(pathClientCrtCrt, clientCertPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\tpathClientCrtKey := path.Join(dirClientCrt, \"client.key\")\n\tif err := os.WriteFile(pathClientCrtKey, clientKeyPEM, 0666); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// for self signed cert, it needs the server cert, for real cert, this need to be the trusted CA cert\n\ttlsConfig.serverTLS = &chserver.TLSConfig{\n\t\tCA:   pathServerCACrt,\n\t\tCert: pathServerCrtCrt,\n\t\tKey:  pathServerCrtKey,\n\t}\n\ttlsConfig.clientTLS = &chclient.TLSConfig{\n\t\tCA:   pathClientCACrt,\n\t\tCert: pathClientCrtCrt,\n\t\tKey:  pathClientCrtKey,\n\t}\n\treturn tlsConfig, nil\n}\n\ntype certConfig struct {\n\tsignCA      *x509.Certificate\n\tisCA        bool\n\thosts       []string\n\tvalidFrom   *time.Time\n\tvalidFor    *time.Time\n\textKeyUsage []x509.ExtKeyUsage\n\trsaBits     int\n\tecdsaCurve  string\n\ted25519Key  bool\n}\n\nfunc certGetCertificate(c *certConfig) (*x509.Certificate, []byte, []byte, error) {\n\tvar err error\n\tvar priv interface{}\n\tswitch c.ecdsaCurve {\n\tcase \"\":\n\t\tif c.ed25519Key {\n\t\t\t_, priv, err = ed25519.GenerateKey(rand.Reader)\n\t\t} else {\n\t\t\trsaBits := c.rsaBits\n\t\t\tif rsaBits == 0 {\n\t\t\t\trsaBits = 2048\n\t\t\t}\n\t\t\tpriv, err = rsa.GenerateKey(rand.Reader, rsaBits)\n\t\t}\n\tcase \"P224\":\n\t\tpriv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)\n\tcase \"P256\":\n\t\tpriv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tcase \"P384\":\n\t\tpriv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)\n\tcase \"P521\":\n\t\tpriv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)\n\tdefault:\n\t\treturn nil, nil, nil, fmt.Errorf(\"Unrecognized elliptic curve: %q\", c.ecdsaCurve)\n\t}\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"Failed to generate private key: %v\", err)\n\t}\n\n\t// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature\n\t// KeyUsage bits set in the x509.Certificate template\n\tkeyUsage := x509.KeyUsageDigitalSignature\n\t// Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In\n\t// the context of TLS this KeyUsage is particular to RSA key exchange and\n\t// authentication.\n\tif _, isRSA := priv.(*rsa.PrivateKey); isRSA {\n\t\tkeyUsage |= x509.KeyUsageKeyEncipherment\n\t}\n\n\tnotBefore := time.Now()\n\tif c.validFrom != nil {\n\t\tnotBefore = *c.validFrom\n\t}\n\n\tnotAfter := time.Now().Add(24 * time.Hour)\n\tif c.validFor != nil {\n\t\tnotAfter = *c.validFor\n\t}\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"Failed to generate serial number: %v\", err)\n\t}\n\n\tcert := &x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganizationalUnit: []string{\"test\"},\n\t\t\tOrganization:       []string{\"Chisel\"},\n\t\t\tCountry:            []string{\"us\"},\n\t\t\tProvince:           []string{\"ma\"},\n\t\t\tLocality:           []string{\"Boston\"},\n\t\t\tCommonName:         \"localhost\",\n\t\t},\n\t\tNotBefore: notBefore,\n\t\tNotAfter:  notAfter,\n\n\t\tKeyUsage:              keyUsage,\n\t\tExtKeyUsage:           c.extKeyUsage,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\tfor _, h := range c.hosts {\n\t\tif ip := net.ParseIP(h); ip != nil {\n\t\t\tcert.IPAddresses = append(cert.IPAddresses, ip)\n\t\t} else {\n\t\t\tcert.DNSNames = append(cert.DNSNames, h)\n\t\t}\n\t}\n\n\tif c.isCA {\n\t\tcert.IsCA = true\n\t\tcert.KeyUsage |= x509.KeyUsageCertSign\n\t}\n\n\tca := cert\n\tif c.signCA != nil {\n\t\tca = c.signCA\n\t}\n\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, certGetPublicKey(priv), priv)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"Failed to create certificate: %v\", err)\n\t}\n\n\tcertPEM := new(bytes.Buffer)\n\tpem.Encode(certPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certBytes,\n\t})\n\n\tprivBytes, err := x509.MarshalPKCS8PrivateKey(priv)\n\tif err != nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"Unable to marshal private key: %v\", err)\n\t}\n\tcertPrivKeyPEM := new(bytes.Buffer)\n\tpem.Encode(certPrivKeyPEM, &pem.Block{\n\t\tType:  \"PRIVATE KEY\",\n\t\tBytes: privBytes,\n\t})\n\n\treturn cert, certPEM.Bytes(), certPrivKeyPEM.Bytes(), nil\n}\n\nfunc certGetPublicKey(priv interface{}) interface{} {\n\tswitch k := priv.(type) {\n\tcase *rsa.PrivateKey:\n\t\treturn &k.PublicKey\n\tcase *ecdsa.PrivateKey:\n\t\treturn &k.PublicKey\n\tcase ed25519.PrivateKey:\n\t\treturn k.Public().(ed25519.PublicKey)\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "test/e2e/env_key_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\nfunc TestChiselKeyEnvironmentVariable(t *testing.T) {\n\t// Set the CHISEL_KEY environment variable\n\tos.Setenv(\"CHISEL_KEY\", \"test-key-value\")\n\tdefer os.Unsetenv(\"CHISEL_KEY\")\n\n\ttmpPort := availablePort()\n\t\n\t// Create server with empty config - should pick up CHISEL_KEY env var\n\tserverConfig := &chserver.Config{}\n\t\n\t// Setup server and client\n\tteardown := simpleSetup(t,\n\t\tserverConfig,\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t})\n\tdefer teardown()\n\n\t// Test that the connection works - if the key is properly set,\n\t// the server should start successfully and connections should work\n\tresult, err := post(\"http://localhost:\"+tmpPort, \"env-key-test\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"env-key-test!\" {\n\t\tt.Fatalf(\"expected exclamation mark added, got: %s\", result)\n\t}\n}\n\nfunc TestChiselKeyEnvironmentVariableConsistency(t *testing.T) {\n\t// This test verifies that the same CHISEL_KEY value produces\n\t// consistent behavior (same fingerprint) by manually setting KeySeed\n\tkeyValue := \"consistency-test-key\"\n\n\t// Create two server instances with the same KeySeed (simulating what main.go does)\n\tserver1, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: keyValue,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create first server: %v\", err)\n\t}\n\n\tserver2, err := chserver.NewServer(&chserver.Config{\n\t\tKeySeed: keyValue,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create second server: %v\", err)\n\t}\n\n\t// Both servers should have the same fingerprint since they use the same key\n\tif server1.GetFingerprint() != server2.GetFingerprint() {\n\t\tt.Fatalf(\"Expected same fingerprint for same key, got %s and %s\",\n\t\t\tserver1.GetFingerprint(), server2.GetFingerprint())\n\t}\n}"
  },
  {
    "path": "test/e2e/proxy_test.go",
    "content": "package e2e_test\n\n//TODO tests for:\n// client -> CONNECT proxy -> server -> endpoint\n// client -> SOCKS proxy -> server -> endpoint\n"
  },
  {
    "path": "test/e2e/setup_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\nconst debug = true\n\n// test layout configuration\ntype testLayout struct {\n\tserver     *chserver.Config\n\tclient     *chclient.Config\n\tfileServer bool\n\tudpEcho    bool\n\tudpServer  bool\n}\n\nfunc (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chclient.Client, teardown func()) {\n\t//start of the world\n\t// goroutines := runtime.NumGoroutine()\n\t//root cancel\n\tctx, cancel := context.WithCancel(context.Background())\n\t//fileserver (fake endpoint)\n\tfilePort := availablePort()\n\tif tl.fileServer {\n\t\tfileAddr := \"127.0.0.1:\" + filePort\n\t\tf := http.Server{\n\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tb, _ := io.ReadAll(r.Body)\n\t\t\t\tw.Write(append(b, '!'))\n\t\t\t}),\n\t\t}\n\t\tfl, err := net.Listen(\"tcp\", fileAddr)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tlog.Printf(\"fileserver: listening on %s\", fileAddr)\n\t\tgo func() {\n\t\t\tf.Serve(fl)\n\t\t\tcancel()\n\t\t}()\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tf.Close()\n\t\t}()\n\t}\n\t//server\n\tserver, err := chserver.NewServer(tl.server)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tserver.Debug = debug\n\tport := availablePort()\n\tif err := server.StartContext(ctx, \"127.0.0.1\", port); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgo func() {\n\t\tserver.Wait()\n\t\tserver.Infof(\"Closed\")\n\t\tcancel()\n\t}()\n\t//client (with defaults)\n\ttl.client.Fingerprint = server.GetFingerprint()\n\tif tl.server.TLS.Key != \"\" {\n\t\t//the domain name has to be localhost to match the ssl cert\n\t\ttl.client.Server = \"https://localhost:\" + port\n\t} else {\n\t\ttl.client.Server = \"http://127.0.0.1:\" + port\n\t}\n\tfor i, r := range tl.client.Remotes {\n\t\t//convert $FILEPORT into the allocated port for this test case\n\t\tif tl.fileServer {\n\t\t\ttl.client.Remotes[i] = strings.Replace(r, \"$FILEPORT\", filePort, 1)\n\t\t}\n\t}\n\tclient, err = chclient.NewClient(tl.client)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tclient.Debug = debug\n\tif err := client.Start(ctx); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgo func() {\n\t\tclient.Wait()\n\t\tclient.Infof(\"Closed\")\n\t\tcancel()\n\t}()\n\t//cancel context tree, and wait for both client and server to stop\n\tteardown = func() {\n\t\tcancel()\n\t\tserver.Wait()\n\t\tclient.Wait()\n\t\t//confirm goroutines have been cleaned up\n\t\t// time.Sleep(500 * time.Millisecond)\n\t\t// TODO remove sleep\n\t\t// d := runtime.NumGoroutine() - goroutines\n\t\t// if d != 0 {\n\t\t// \tpprof.Lookup(\"goroutine\").WriteTo(os.Stdout, 1)\n\t\t// \tt.Fatalf(\"goroutines left %d\", d)\n\t\t// }\n\t}\n\t//wait a bit...\n\t//TODO: client signal API, similar to os.Notify(signal)\n\t//      wait for client setup\n\ttime.Sleep(50 * time.Millisecond)\n\t//ready\n\treturn server, client, teardown\n}\n\nfunc simpleSetup(t *testing.T, s *chserver.Config, c *chclient.Config) context.CancelFunc {\n\tconf := testLayout{\n\t\tserver:     s,\n\t\tclient:     c,\n\t\tfileServer: true,\n\t}\n\t_, _, teardown := conf.setup(t)\n\treturn teardown\n}\n\nfunc post(url, body string) (string, error) {\n\tresp, err := http.Post(url, \"text/plain\", strings.NewReader(body))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(b), nil\n}\n\nfunc availablePort() string {\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tlog.Panic(err)\n\t}\n\tl.Close()\n\t_, port, err := net.SplitHostPort(l.Addr().String())\n\tif err != nil {\n\t\tlog.Panic(err)\n\t}\n\treturn port\n}\n"
  },
  {
    "path": "test/e2e/socks_test.go",
    "content": "package e2e_test\n\n//TODO tests for:\n// - SOCKS-client -> [client -> server SOCKS] -> endpoint\n// - SOCKS-client -> [server -> client SOCKS] -> endpoint\n"
  },
  {
    "path": "test/e2e/tls_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"path\"\n\t\"testing\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n)\n\nfunc TestTLS(t *testing.T) {\n\ttlsConfig, err := newTestTLSConfig()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer tlsConfig.Close()\n\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tTLS: *tlsConfig.serverTLS,\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t\tTLS:     *tlsConfig.clientTLS,\n\t\t\tServer:  \"https://localhost:\" + tmpPort,\n\t\t})\n\tdefer teardown()\n\t//test remote\n\tresult, err := post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"foo!\" {\n\t\tt.Fatalf(\"expected exclamation mark added\")\n\t}\n}\n\nfunc TestMTLS(t *testing.T) {\n\ttlsConfig, err := newTestTLSConfig()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer tlsConfig.Close()\n\t//provide no client cert, server should reject the client request\n\ttlsConfig.serverTLS.CA = path.Dir(tlsConfig.serverTLS.CA)\n\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tTLS: *tlsConfig.serverTLS,\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t\tTLS:     *tlsConfig.clientTLS,\n\t\t\tServer:  \"https://localhost:\" + tmpPort,\n\t\t})\n\tdefer teardown()\n\t//test remote\n\tresult, err := post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif result != \"foo!\" {\n\t\tt.Fatalf(\"expected exclamation mark added\")\n\t}\n}\n\nfunc TestTLSMissingClientCert(t *testing.T) {\n\ttlsConfig, err := newTestTLSConfig()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer tlsConfig.Close()\n\t//provide no client cert, server should reject the client request\n\ttlsConfig.clientTLS.Cert = \"\"\n\ttlsConfig.clientTLS.Key = \"\"\n\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tTLS: *tlsConfig.serverTLS,\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t\tTLS:     *tlsConfig.clientTLS,\n\t\t\tServer:  \"https://localhost:\" + tmpPort,\n\t\t})\n\tdefer teardown()\n\t//test remote\n\t_, err = post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestTLSMissingClientCA(t *testing.T) {\n\ttlsConfig, err := newTestTLSConfig()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer tlsConfig.Close()\n\t//specify a CA which does not match the client cert\n\t//server should reject the client request\n\t//provide no client cert, server should reject the client request\n\ttlsConfig.serverTLS.CA = tlsConfig.clientTLS.CA\n\n\ttmpPort := availablePort()\n\t//setup server, client, fileserver\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{\n\t\t\tTLS: *tlsConfig.serverTLS,\n\t\t},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{tmpPort + \":$FILEPORT\"},\n\t\t\tTLS:     *tlsConfig.clientTLS,\n\t\t\tServer:  \"https://localhost:\" + tmpPort,\n\t\t})\n\tdefer teardown()\n\t//test remote\n\t_, err = post(\"http://localhost:\"+tmpPort, \"foo\")\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "test/e2e/udp_test.go",
    "content": "package e2e_test\n\nimport (\n\t\"log\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\tchclient \"github.com/jpillora/chisel/client\"\n\tchserver \"github.com/jpillora/chisel/server\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc TestUDP(t *testing.T) {\n\t//listen on random udp port\n\techoPort := availableUDPPort()\n\ta, _ := net.ResolveUDPAddr(\"udp\", \":\"+echoPort)\n\tl, err := net.ListenUDP(\"udp\", a)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t//chisel client+server\n\tinboundPort := availableUDPPort()\n\tteardown := simpleSetup(t,\n\t\t&chserver.Config{},\n\t\t&chclient.Config{\n\t\t\tRemotes: []string{\n\t\t\t\tinboundPort + \":\" + echoPort + \"/udp\",\n\t\t\t},\n\t\t},\n\t)\n\tdefer teardown()\n\t//fake udp server, read and echo back duplicated, close\n\teg := errgroup.Group{}\n\teg.Go(func() error {\n\t\tdefer l.Close()\n\t\tb := make([]byte, 128)\n\t\tn, a, err := l.ReadFrom(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err = l.WriteTo(append(b[:n], b[:n]...), a); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\t//fake udp client\n\tconn, err := net.Dial(\"udp4\", \"localhost:\"+inboundPort)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t//write bazz through the tunnel\n\tif _, err := conn.Write([]byte(\"bazz\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\t//receive bazzbazz back\n\tb := make([]byte, 128)\n\tconn.SetReadDeadline(time.Now().Add(2 * time.Second))\n\tn, err := conn.Read(b)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t\treturn\n\t}\n\t//udp server should close correctly\n\tif err := eg.Wait(); err != nil {\n\t\tt.Fatal(err)\n\t\treturn\n\t}\n\t//ensure expected\n\ts := string(b[:n])\n\tif s != \"bazzbazz\" {\n\t\tt.Fatalf(\"expected double bazz\")\n\t}\n}\n\nfunc availableUDPPort() string {\n\ta, _ := net.ResolveUDPAddr(\"udp\", \":0\")\n\tl, err := net.ListenUDP(\"udp\", a)\n\tif err != nil {\n\t\tlog.Panicf(\"availability listen: %s\", err)\n\t}\n\tl.Close()\n\t_, port, err := net.SplitHostPort(l.LocalAddr().String())\n\tif err != nil {\n\t\tlog.Panic(err)\n\t}\n\treturn port\n}\n"
  }
]