[
  {
    "path": ".dockerignore",
    "content": "/target\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [ekzhang]\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  rust:\n    name: Build and Test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n\n      - run: cargo build --all-features\n\n      - run: cargo test\n\n  rustfmt:\n    name: Rustfmt\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          components: rustfmt\n\n      - run: cargo fmt -- --check\n\n  clippy:\n    name: Clippy\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          components: clippy\n\n      - run: cargo clippy -- -D warnings\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\njobs:\n  build_deploy:\n    name: Build and Deploy\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: ekzhang/bore\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n        with:\n          platforms: arm64\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v3\n        with:\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n      - name: Image digest\n        run: echo ${{ steps.docker_build.outputs.digest }}\n"
  },
  {
    "path": ".github/workflows/mean_bean_ci.yml",
    "content": "name: Mean Bean CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  install-cross:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 50\n\n      - uses: XAMPPRocky/get-github-release@v1\n        id: cross\n        with:\n          owner: rust-embedded\n          repo: cross\n          matches: ${{ matrix.platform }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: cross-${{ matrix.platform }}\n          path: ${{ steps.cross.outputs.install_path }}\n    strategy:\n      matrix:\n        platform: [linux-musl]\n\n  macos:\n    runs-on: macos-latest\n    strategy:\n      fail-fast: true\n      matrix:\n        channel: [stable]\n        target:\n          - aarch64-apple-darwin\n\n    steps:\n      - name: Setup | Checkout\n        uses: actions/checkout@v4\n      - name: Setup | Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          override: true\n          profile: minimal\n          target: ${{ matrix.target }}\n      - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}\n      - name: Test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --target ${{ matrix.target }}\n          use-cross: false\n\n  linux:\n    runs-on: ubuntu-latest\n    needs: install-cross\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 50\n\n      - name: Download Cross\n        uses: actions/download-artifact@v4\n        with:\n          name: cross-linux-musl\n          path: /tmp/\n      - run: chmod +x /tmp/cross\n      - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}\n      - run: ci/build.bash /tmp/cross ${{ matrix.target }}\n        # These targets have issues with being tested so they are disabled\n        # by default. You can try disabling to see if they work for\n        # your project.\n      - run: ci/test.bash /tmp/cross ${{ matrix.target }}\n        if: |\n          !contains(matrix.target, 'android') &&\n          !contains(matrix.target, 'bsd') &&\n          !contains(matrix.target, 'solaris') &&\n          matrix.target != 'armv5te-unknown-linux-musleabi' &&\n          matrix.target != 'sparc64-unknown-linux-gnu'\n\n    strategy:\n      fail-fast: true\n      matrix:\n        channel: [stable]\n        target:\n          - aarch64-unknown-linux-musl\n          - arm-unknown-linux-musleabi\n          - arm-unknown-linux-gnueabi\n          - armv7-unknown-linux-gnueabihf\n          - armv7-unknown-linux-musleabihf\n          - i686-unknown-linux-musl\n          - x86_64-unknown-linux-musl\n\n  windows:\n    runs-on: windows-latest\n    # Windows technically doesn't need this, but if we don't block windows on it\n    # some of the windows jobs could fill up the concurrent job queue before\n    # one of the install-cross jobs has started, so this makes sure all\n    # artifacts are downloaded first.\n    needs: install-cross\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 50\n      - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}\n        shell: bash\n      - run: ci/build.bash cargo ${{ matrix.target }}\n        shell: bash\n      - run: ci/test.bash cargo ${{ matrix.target }}\n        shell: bash\n\n    strategy:\n      fail-fast: true\n      matrix:\n        channel: [stable]\n        target:\n          # MSVC\n          - i686-pc-windows-msvc\n          - x86_64-pc-windows-msvc\n          # GNU: You typically only need to test Windows GNU if you're\n          # specifically targetting it, and it can cause issues with some\n          # dependencies if you're not so it's disabled by self.\n          # - i686-pc-windows-gnu\n          # - x86_64-pc-windows-gnu\n"
  },
  {
    "path": ".github/workflows/mean_bean_deploy.yml",
    "content": "on:\n  push:\n    # # Sequence of patterns matched against refs/tags\n    tags:\n      - \"v*\" # Push events to matching v*, i.e. v1.0, v20.15.10\n\nname: Mean Bean Deploy\nenv:\n  BIN: bore\n\njobs:\n  # This job downloads and stores `cross` as an artifact, so that it can be\n  # redownloaded across all of the jobs. Currently this copied pasted between\n  # `mean_bean_ci.yml` and `mean_bean_deploy.yml`. Make sure to update both places when making\n  # changes.\n  install-cross:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 50\n\n      - uses: XAMPPRocky/get-github-release@v1\n        id: cross\n        with:\n          owner: rust-embedded\n          repo: cross\n          matches: ${{ matrix.platform }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: cross-${{ matrix.platform }}\n          path: ${{ steps.cross.outputs.install_path }}\n    strategy:\n      matrix:\n        platform: [linux-musl]\n\n  macos:\n    runs-on: macos-latest\n    strategy:\n      matrix:\n        target:\n          # macOS\n          - x86_64-apple-darwin\n          - aarch64-apple-darwin\n    steps:\n      - name: Get tag\n        id: tag\n        uses: dawidd6/action-get-tag@v1\n\n      - name: Setup | Checkout\n        uses: actions/checkout@v4\n\n      # Cache files between builds\n      - name: Setup | Cache Cargo\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.target }}\n\n      - name: Setup | Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          override: true\n          profile: minimal\n          target: ${{ matrix.target }}\n\n      - name: Build | Build\n        uses: actions-rs/cargo@v1\n        with:\n          command: build\n          args: --release --target ${{ matrix.target }}\n\n      - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}\n      - uses: XAMPPRocky/create-release@v1.0.2\n        id: create_release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          draft: false\n          prerelease: false\n\n      - uses: actions/upload-release-asset@v1\n        id: upload-release-asset\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ${{ env.BIN }}.tar.gz\n          asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz\n          asset_content_type: application/gzip\n\n  linux:\n    runs-on: ubuntu-latest\n    needs: install-cross\n    strategy:\n      matrix:\n        target:\n          - aarch64-unknown-linux-musl\n          - arm-unknown-linux-musleabi\n          - arm-unknown-linux-gnueabi\n          - armv7-unknown-linux-gnueabihf\n          - armv7-unknown-linux-musleabihf\n          - i686-unknown-linux-musl\n          - x86_64-unknown-linux-musl\n    steps:\n      - name: Get tag\n        id: tag\n        uses: dawidd6/action-get-tag@v1\n\n      - uses: actions/checkout@v4\n      - uses: actions/download-artifact@v4\n        with:\n          name: cross-linux-musl\n          path: /tmp/\n      - run: chmod +x /tmp/cross\n\n      - run: ci/set_rust_version.bash stable ${{ matrix.target }}\n      - run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE\n      - run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}\n\n      - uses: XAMPPRocky/create-release@v1.0.2\n        id: create_release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          draft: false\n          prerelease: false\n\n      - name: Upload Release Asset\n        id: upload-release-asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ${{ env.BIN }}.tar.gz\n          asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz\n          asset_content_type: application/gzip\n\n  windows:\n    runs-on: windows-latest\n    needs: install-cross\n    strategy:\n      matrix:\n        target:\n          # MSVC\n          - i686-pc-windows-msvc\n          - x86_64-pc-windows-msvc\n          # GNU\n          # - i686-pc-windows-gnu\n          # - x86_64-pc-windows-gnu\n    steps:\n      - name: Get tag\n        id: tag\n        uses: dawidd6/action-get-tag@v1\n\n      - uses: actions/checkout@v4\n      - run: bash ci/set_rust_version.bash stable ${{ matrix.target }}\n      - run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE\n      - run: |\n          cd ./target/${{ matrix.target }}/release/\n          7z a \"${{ env.BIN }}.zip\" \"${{ env.BIN }}.exe\"\n          mv \"${{ env.BIN }}.zip\" $GITHUB_WORKSPACE\n        shell: bash\n        # We're using using a fork of `actions/create-release` that detects\n        # whether a release is already available or not first.\n      - uses: XAMPPRocky/create-release@v1.0.2\n        id: create_release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: ${{ github.ref }}\n          # Draft should **always** be false. GitHub doesn't provide a way to\n          # get draft releases from its API, so there's no point using it.\n          draft: false\n          prerelease: false\n      - uses: actions/upload-release-asset@v1\n        id: upload-release-asset\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: ${{ env.BIN }}.zip\n          asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.zip\n          asset_content_type: application/zip\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"bore-cli\"\nversion = \"0.6.0\"\nauthors = [\"Eric Zhang <ekzhang1@gmail.com>\"]\nlicense = \"MIT\"\ndescription = \"A modern, simple TCP tunnel in Rust that exposes local ports to a remote server, bypassing standard NAT connection firewalls.\"\nrepository = \"https://github.com/ekzhang/bore\"\ndocumentation = \"https://docs.rs/bore-cli\"\nkeywords = [\"network\", \"cli\", \"tunnel\", \"tcp\"]\ncategories = [\"network-programming\", \"web-programming\", \"command-line-utilities\"]\nreadme = \"README.md\"\nedition = \"2021\"\n\n[[bin]]\nname = \"bore\"\npath = \"src/main.rs\"\n\n[dependencies]\nanyhow = { version = \"1.0.56\", features = [\"backtrace\"] }\nclap = { version = \"4.0.22\", features = [\"derive\", \"env\"] }\ndashmap = \"5.2.0\"\nfastrand = \"1.9.0\"\nfutures-util = { version = \"0.3.21\", features = [\"sink\"] }\nhex = \"0.4.3\"\nhmac = \"0.12.1\"\nserde = { version = \"1.0.136\", features = [\"derive\"] }\nserde_json = \"1.0.79\"\nsha2 = \"0.10.2\"\ntokio = { version = \"1.17.0\", features = [\"rt-multi-thread\", \"io-util\", \"macros\", \"net\", \"time\"] }\ntokio-util = { version = \"0.7.1\", features = [\"codec\"] }\ntracing = \"0.1.32\"\ntracing-subscriber = \"0.3.18\"\nuuid = { version = \"1.2.1\", features = [\"serde\", \"v4\"] }\n\n[dev-dependencies]\nlazy_static = \"1.4.0\"\nrstest = \"0.15.0\"\ntokio = { version = \"1.17.0\", features = [\"sync\"] }\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM rust:alpine AS builder\nWORKDIR /home/rust/src\nRUN apk --no-cache add musl-dev\nCOPY . .\nRUN cargo install --path .\n\nFROM scratch\nCOPY --from=builder /usr/local/cargo/bin/bore .\nUSER 1000:1000\nENTRYPOINT [\"./bore\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Eric Zhang\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": "README.md",
    "content": "# bore\n\n[![Build status](https://img.shields.io/github/actions/workflow/status/ekzhang/bore/ci.yml)](https://github.com/ekzhang/bore/actions)\n[![Crates.io](https://img.shields.io/crates/v/bore-cli.svg)](https://crates.io/crates/bore-cli)\n\nA modern, simple TCP tunnel in Rust that exposes local ports to a remote server, bypassing standard NAT connection firewalls. **That's all it does: no more, and no less.**\n\n![Video demo](https://i.imgur.com/vDeGsmx.gif)\n\n```shell\n# Installation (requires Rust, see alternatives below)\ncargo install bore-cli\n\n# On your local machine\nbore local 8000 --to bore.pub\n```\n\nThis will expose your local port at `localhost:8000` to the public internet at `bore.pub:<PORT>`, where the port number is assigned randomly.\n\nSimilar to [localtunnel](https://github.com/localtunnel/localtunnel) and [ngrok](https://ngrok.io/), except `bore` is intended to be a highly efficient, unopinionated tool for forwarding TCP traffic that is simple to install and easy to self-host, with no frills attached.\n\n(`bore` totals about 400 lines of safe, async Rust code and is trivial to set up — just run a single binary for the client and server.)\n\n## Installation\n\n### macOS\n\n`bore` is packaged as a Homebrew core formula.\n\n```shell\nbrew install bore-cli\n```\n\n### Linux\n\n#### Arch Linux\n\n`bore` is available in the AUR as `bore`.\n\n```shell\nyay -S bore # or your favorite AUR helper\n```\n\n#### Gentoo Linux\n\n`bore` is available in the [gentoo-zh](https://github.com/microcai/gentoo-zh) overlay.\n\n```shell\nsudo eselect repository enable gentoo-zh\nsudo emerge --sync gentoo-zh\nsudo emerge net-proxy/bore\n```\n\n### Binary Distribution\n\nOtherwise, the easiest way to install bore is from prebuilt binaries. These are available on the [releases page](https://github.com/ekzhang/bore/releases) for macOS, Windows, and Linux. Just unzip the appropriate file for your platform and move the `bore` executable into a folder on your PATH.\n\n### Cargo\n\nYou also can build `bore` from source using [Cargo](https://doc.rust-lang.org/cargo/), the Rust package manager. This command installs the `bore` binary at a user-accessible path.\n\n```shell\ncargo install bore-cli\n```\n\n### Docker\n\nWe also publish versioned Docker images for each release. The image is built for an AMD 64-bit architecture. They're tagged with the specific version and allow you to run the statically-linked `bore` binary from a minimal \"scratch\" container.\n\n```shell\ndocker run -it --init --rm --network host ekzhang/bore <ARGS>\n```\n\n## Detailed Usage\n\nThis section describes detailed usage for the `bore` CLI command.\n\n### Local Forwarding\n\nYou can forward a port on your local machine by using the `bore local` command. This takes a positional argument, the local port to forward, as well as a mandatory `--to` option, which specifies the address of the remote server.\n\n```shell\nbore local 5000 --to bore.pub\n```\n\nYou can optionally pass in a `--port` option to pick a specific port on the remote to expose, although the command will fail if this port is not available. Also, passing `--local-host` allows you to expose a different host on your local area network besides the loopback address `localhost`.\n\nThe full options are shown below.\n\n```shell\nStarts a local proxy to the remote server\n\nUsage: bore local [OPTIONS] --to <TO> <LOCAL_PORT>\n\nArguments:\n  <LOCAL_PORT>  The local port to expose [env: BORE_LOCAL_PORT=]\n\nOptions:\n  -l, --local-host <HOST>  The local host to expose [default: localhost]\n  -t, --to <TO>            Address of the remote server to expose local ports to [env: BORE_SERVER=]\n  -p, --port <PORT>        Optional port on the remote server to select [default: 0]\n  -s, --secret <SECRET>    Optional secret for authentication [env: BORE_SECRET]\n  -h, --help               Print help\n```\n\n### Self-Hosting\n\nAs mentioned in the startup instructions, there is a public instance of the `bore` server running at `bore.pub`. However, if you want to self-host `bore` on your own network, you can do so with the following command:\n\n```shell\nbore server\n```\n\nThat's all it takes! After the server starts running at a given address, you can then update the `bore local` command with option `--to <ADDRESS>` to forward a local port to this remote server.\n\nIt's possible to specify different IP addresses for the control server and for the tunnels. This setup is useful for cases where you might want the control server to be on a private network while allowing tunnel connections over a public interface, or vice versa.\n\nThe full options for the `bore server` command are shown below.\n\n```shell\nRuns the remote proxy server\n\nUsage: bore server [OPTIONS]\n\nOptions:\n      --min-port <MIN_PORT>          Minimum accepted TCP port number [env: BORE_MIN_PORT=] [default: 1024]\n      --max-port <MAX_PORT>          Maximum accepted TCP port number [env: BORE_MAX_PORT=] [default: 65535]\n  -s, --secret <SECRET>              Optional secret for authentication [env: BORE_SECRET]\n      --bind-addr <BIND_ADDR>        IP address to bind to, clients must reach this [default: 0.0.0.0]\n      --bind-tunnels <BIND_TUNNELS>  IP address where tunnels will listen on, defaults to --bind-addr\n  -h, --help                         Print help\n```\n\n## Protocol\n\nThere is an implicit _control port_ at `7835`, used for creating new connections on demand. At initialization, the client sends a \"Hello\" message to the server on the TCP control port, asking to proxy a selected remote port. The server then responds with an acknowledgement and begins listening for external TCP connections.\n\nWhenever the server obtains a connection on the remote port, it generates a secure [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) for that connection and sends it back to the client. The client then opens a separate TCP stream to the server and sends an \"Accept\" message containing the UUID on that stream. The server then proxies the two connections between each other.\n\nFor correctness reasons and to avoid memory leaks, incoming connections are only stored by the server for up to 10 seconds before being discarded if the client does not accept them.\n\n## Authentication\n\nOn a custom deployment of `bore server`, you can optionally require a _secret_ to prevent the server from being used by others. The protocol requires clients to verify possession of the secret on each TCP connection by answering random challenges in the form of HMAC codes. (This secret is only used for the initial handshake, and no further traffic is encrypted by default.)\n\n```shell\n# on the server\nbore server --secret my_secret_string\n\n# on the client\nbore local <LOCAL_PORT> --to <TO> --secret my_secret_string\n```\n\nIf a secret is not present in the arguments, `bore` will also attempt to read from the `BORE_SECRET` environment variable.\n\n## Acknowledgements\n\nCreated by Eric Zhang ([@ekzhang1](https://twitter.com/ekzhang1)). Licensed under the [MIT license](LICENSE).\n\nThe author would like to thank the contributors and maintainers of the [Tokio](https://tokio.rs/) project for making it possible to write ergonomic and efficient network services in Rust.\n"
  },
  {
    "path": "ci/build.bash",
    "content": "#!/usr/bin/env bash\n# Script for building your rust projects.\nset -e\n\nsource ci/common.bash\n\n# $1 {path} = Path to cross/cargo executable\nCROSS=$1\n# $1 {string} = <Target Triple> e.g. x86_64-pc-windows-msvc\nTARGET_TRIPLE=$2\n# $3 {boolean} = Are we building for deployment? \nRELEASE_BUILD=$3\n\nrequired_arg $CROSS 'CROSS'\nrequired_arg $TARGET_TRIPLE '<Target Triple>'\n\nif [ -z \"$RELEASE_BUILD\" ]; then\n    $CROSS build --target $TARGET_TRIPLE\n    $CROSS build --target $TARGET_TRIPLE --all-features\nelse\n    $CROSS build --target $TARGET_TRIPLE --all-features --release\nfi\n"
  },
  {
    "path": "ci/common.bash",
    "content": "required_arg() {\n    if [ -z \"$1\" ]; then\n        echo \"Required argument $2 missing\"\n        exit 1\n    fi\n}\n"
  },
  {
    "path": "ci/set_rust_version.bash",
    "content": "#!/usr/bin/env bash\nset -e\nrustup default $1\nrustup target add $2\n"
  },
  {
    "path": "ci/test.bash",
    "content": "#!/usr/bin/env bash\n# Script for building your rust projects.\nset -e\n\nsource ci/common.bash\n\n# $1 {path} = Path to cross/cargo executable\nCROSS=$1\n# $1 {string} = <Target Triple>\nTARGET_TRIPLE=$2\n\nrequired_arg $CROSS 'CROSS'\nrequired_arg $TARGET_TRIPLE '<Target Triple>'\n\nmax_attempts=3\ncount=0\n\nwhile [ $count -lt $max_attempts ]; do\n    $CROSS test --target $TARGET_TRIPLE\n    status=$?\n    if [ $status -eq 0 ]; then\n        echo \"Test passed\"\n        break\n    else\n        echo \"Test failed, attempt $(($count + 1))\"\n    fi\n    count=$(($count + 1))\ndone\n\nif [ $status -ne 0 ]; then\n    echo \"Test failed after $max_attempts attempts\"\nfi\n"
  },
  {
    "path": "src/auth.rs",
    "content": "//! Auth implementation for bore client and server.\n\nuse anyhow::{bail, ensure, Result};\nuse hmac::{Hmac, Mac};\nuse sha2::{Digest, Sha256};\nuse tokio::io::{AsyncRead, AsyncWrite};\nuse uuid::Uuid;\n\nuse crate::shared::{ClientMessage, Delimited, ServerMessage};\n\n/// Wrapper around a MAC used for authenticating clients that have a secret.\npub struct Authenticator(Hmac<Sha256>);\n\nimpl Authenticator {\n    /// Generate an authenticator from a secret.\n    pub fn new(secret: &str) -> Self {\n        let hashed_secret = Sha256::new().chain_update(secret).finalize();\n        Self(Hmac::new_from_slice(&hashed_secret).expect(\"HMAC can take key of any size\"))\n    }\n\n    /// Generate a reply message for a challenge.\n    pub fn answer(&self, challenge: &Uuid) -> String {\n        let mut hmac = self.0.clone();\n        hmac.update(challenge.as_bytes());\n        hex::encode(hmac.finalize().into_bytes())\n    }\n\n    /// Validate a reply to a challenge.\n    ///\n    /// ```\n    /// use bore_cli::auth::Authenticator;\n    /// use uuid::Uuid;\n    ///\n    /// let auth = Authenticator::new(\"secret\");\n    /// let challenge = Uuid::new_v4();\n    ///\n    /// assert!(auth.validate(&challenge, &auth.answer(&challenge)));\n    /// assert!(!auth.validate(&challenge, \"wrong answer\"));\n    /// ```\n    pub fn validate(&self, challenge: &Uuid, tag: &str) -> bool {\n        if let Ok(tag) = hex::decode(tag) {\n            let mut hmac = self.0.clone();\n            hmac.update(challenge.as_bytes());\n            hmac.verify_slice(&tag).is_ok()\n        } else {\n            false\n        }\n    }\n\n    /// As the server, send a challenge to the client and validate their response.\n    pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(\n        &self,\n        stream: &mut Delimited<T>,\n    ) -> Result<()> {\n        let challenge = Uuid::new_v4();\n        stream.send(ServerMessage::Challenge(challenge)).await?;\n        match stream.recv_timeout().await? {\n            Some(ClientMessage::Authenticate(tag)) => {\n                ensure!(self.validate(&challenge, &tag), \"invalid secret\");\n                Ok(())\n            }\n            _ => bail!(\"server requires secret, but no secret was provided\"),\n        }\n    }\n\n    /// As the client, answer a challenge to attempt to authenticate with the server.\n    pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(\n        &self,\n        stream: &mut Delimited<T>,\n    ) -> Result<()> {\n        let challenge = match stream.recv_timeout().await? {\n            Some(ServerMessage::Challenge(challenge)) => challenge,\n            _ => bail!(\"expected authentication challenge, but no secret was required\"),\n        };\n        let tag = self.answer(&challenge);\n        stream.send(ClientMessage::Authenticate(tag)).await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "src/client.rs",
    "content": "//! Client implementation for the `bore` service.\n\nuse std::sync::Arc;\n\nuse anyhow::{bail, Context, Result};\nuse tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};\nuse tracing::{error, info, info_span, warn, Instrument};\nuse uuid::Uuid;\n\nuse crate::auth::Authenticator;\nuse crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT};\n\n/// State structure for the client.\npub struct Client {\n    /// Control connection to the server.\n    conn: Option<Delimited<TcpStream>>,\n\n    /// Destination address of the server.\n    to: String,\n\n    // Local host that is forwarded.\n    local_host: String,\n\n    /// Local port that is forwarded.\n    local_port: u16,\n\n    /// Port that is publicly available on the remote.\n    remote_port: u16,\n\n    /// Optional secret used to authenticate clients.\n    auth: Option<Authenticator>,\n}\n\nimpl Client {\n    /// Create a new client.\n    pub async fn new(\n        local_host: &str,\n        local_port: u16,\n        to: &str,\n        port: u16,\n        secret: Option<&str>,\n    ) -> Result<Self> {\n        let mut stream = Delimited::new(connect_with_timeout(to, CONTROL_PORT).await?);\n        let auth = secret.map(Authenticator::new);\n        if let Some(auth) = &auth {\n            auth.client_handshake(&mut stream).await?;\n        }\n\n        stream.send(ClientMessage::Hello(port)).await?;\n        let remote_port = match stream.recv_timeout().await? {\n            Some(ServerMessage::Hello(remote_port)) => remote_port,\n            Some(ServerMessage::Error(message)) => bail!(\"server error: {message}\"),\n            Some(ServerMessage::Challenge(_)) => {\n                bail!(\"server requires authentication, but no client secret was provided\");\n            }\n            Some(_) => bail!(\"unexpected initial non-hello message\"),\n            None => bail!(\"unexpected EOF\"),\n        };\n        info!(remote_port, \"connected to server\");\n        info!(\"listening at {to}:{remote_port}\");\n\n        Ok(Client {\n            conn: Some(stream),\n            to: to.to_string(),\n            local_host: local_host.to_string(),\n            local_port,\n            remote_port,\n            auth,\n        })\n    }\n\n    /// Returns the port publicly available on the remote.\n    pub fn remote_port(&self) -> u16 {\n        self.remote_port\n    }\n\n    /// Start the client, listening for new connections.\n    pub async fn listen(mut self) -> Result<()> {\n        let mut conn = self.conn.take().unwrap();\n        let this = Arc::new(self);\n        loop {\n            match conn.recv().await? {\n                Some(ServerMessage::Hello(_)) => warn!(\"unexpected hello\"),\n                Some(ServerMessage::Challenge(_)) => warn!(\"unexpected challenge\"),\n                Some(ServerMessage::Heartbeat) => (),\n                Some(ServerMessage::Connection(id)) => {\n                    let this = Arc::clone(&this);\n                    tokio::spawn(\n                        async move {\n                            info!(\"new connection\");\n                            match this.handle_connection(id).await {\n                                Ok(_) => info!(\"connection exited\"),\n                                Err(err) => warn!(%err, \"connection exited with error\"),\n                            }\n                        }\n                        .instrument(info_span!(\"proxy\", %id)),\n                    );\n                }\n                Some(ServerMessage::Error(err)) => error!(%err, \"server error\"),\n                None => return Ok(()),\n            }\n        }\n    }\n\n    async fn handle_connection(&self, id: Uuid) -> Result<()> {\n        let mut remote_conn =\n            Delimited::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?);\n        if let Some(auth) = &self.auth {\n            auth.client_handshake(&mut remote_conn).await?;\n        }\n        remote_conn.send(ClientMessage::Accept(id)).await?;\n        let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;\n        let mut parts = remote_conn.into_parts();\n        debug_assert!(parts.write_buf.is_empty(), \"framed write buffer not empty\");\n        local_conn.write_all(&parts.read_buf).await?; // mostly of the cases, this will be empty\n        tokio::io::copy_bidirectional(&mut local_conn, &mut parts.io).await?;\n        Ok(())\n    }\n}\n\nasync fn connect_with_timeout(to: &str, port: u16) -> Result<TcpStream> {\n    match timeout(NETWORK_TIMEOUT, TcpStream::connect((to, port))).await {\n        Ok(res) => res,\n        Err(err) => Err(err.into()),\n    }\n    .with_context(|| format!(\"could not connect to {to}:{port}\"))\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! A modern, simple TCP tunnel in Rust that exposes local ports to a remote\n//! server, bypassing standard NAT connection firewalls.\n//!\n//! This is the library crate documentation. If you're looking for usage\n//! information about the binary, see the command below.\n//!\n//! ```shell\n//! $ bore help\n//! ```\n//!\n//! There are two components to the crate, offering implementations of the\n//! server network daemon and client local forwarding proxy. Both are public\n//! members and can be run programmatically with a Tokio 1.0 runtime.\n\n#![forbid(unsafe_code)]\n#![warn(missing_docs)]\n\npub mod auth;\npub mod client;\npub mod server;\npub mod shared;\n"
  },
  {
    "path": "src/main.rs",
    "content": "use std::net::IpAddr;\n\nuse anyhow::Result;\nuse bore_cli::{client::Client, server::Server};\nuse clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};\n\n#[derive(Parser, Debug)]\n#[clap(author, version, about)]\nstruct Args {\n    #[clap(subcommand)]\n    command: Command,\n}\n\n#[derive(Subcommand, Debug)]\nenum Command {\n    /// Starts a local proxy to the remote server.\n    Local {\n        /// The local port to expose.\n        #[clap(env = \"BORE_LOCAL_PORT\")]\n        local_port: u16,\n\n        /// The local host to expose.\n        #[clap(short, long, value_name = \"HOST\", default_value = \"localhost\")]\n        local_host: String,\n\n        /// Address of the remote server to expose local ports to.\n        #[clap(short, long, env = \"BORE_SERVER\")]\n        to: String,\n\n        /// Optional port on the remote server to select.\n        #[clap(short, long, default_value_t = 0)]\n        port: u16,\n\n        /// Optional secret for authentication.\n        #[clap(short, long, env = \"BORE_SECRET\", hide_env_values = true)]\n        secret: Option<String>,\n    },\n\n    /// Runs the remote proxy server.\n    Server {\n        /// Minimum accepted TCP port number.\n        #[clap(long, default_value_t = 1024, env = \"BORE_MIN_PORT\")]\n        min_port: u16,\n\n        /// Maximum accepted TCP port number.\n        #[clap(long, default_value_t = 65535, env = \"BORE_MAX_PORT\")]\n        max_port: u16,\n\n        /// Optional secret for authentication.\n        #[clap(short, long, env = \"BORE_SECRET\", hide_env_values = true)]\n        secret: Option<String>,\n\n        /// IP address to bind to, clients must reach this.\n        #[clap(long, default_value = \"0.0.0.0\")]\n        bind_addr: IpAddr,\n\n        /// IP address where tunnels will listen on, defaults to --bind-addr.\n        #[clap(long)]\n        bind_tunnels: Option<IpAddr>,\n    },\n}\n\n#[tokio::main]\nasync fn run(command: Command) -> Result<()> {\n    match command {\n        Command::Local {\n            local_host,\n            local_port,\n            to,\n            port,\n            secret,\n        } => {\n            let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?;\n            client.listen().await?;\n        }\n        Command::Server {\n            min_port,\n            max_port,\n            secret,\n            bind_addr,\n            bind_tunnels,\n        } => {\n            let port_range = min_port..=max_port;\n            if port_range.is_empty() {\n                Args::command()\n                    .error(ErrorKind::InvalidValue, \"port range is empty\")\n                    .exit();\n            }\n            let mut server = Server::new(port_range, secret.as_deref());\n            server.set_bind_addr(bind_addr);\n            server.set_bind_tunnels(bind_tunnels.unwrap_or(bind_addr));\n            server.listen().await?;\n        }\n    }\n\n    Ok(())\n}\n\nfn main() -> Result<()> {\n    tracing_subscriber::fmt::init();\n    run(Args::parse().command)\n}\n"
  },
  {
    "path": "src/server.rs",
    "content": "//! Server implementation for the `bore` service.\n\nuse std::net::{IpAddr, Ipv4Addr};\nuse std::{io, ops::RangeInclusive, sync::Arc, time::Duration};\n\nuse anyhow::Result;\nuse dashmap::DashMap;\nuse tokio::io::AsyncWriteExt;\nuse tokio::net::{TcpListener, TcpStream};\nuse tokio::time::{sleep, timeout};\nuse tracing::{info, info_span, warn, Instrument};\nuse uuid::Uuid;\n\nuse crate::auth::Authenticator;\nuse crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT};\n\n/// State structure for the server.\npub struct Server {\n    /// Range of TCP ports that can be forwarded.\n    port_range: RangeInclusive<u16>,\n\n    /// Optional secret used to authenticate clients.\n    auth: Option<Authenticator>,\n\n    /// Concurrent map of IDs to incoming connections.\n    conns: Arc<DashMap<Uuid, TcpStream>>,\n\n    /// IP address where the control server will bind to.\n    bind_addr: IpAddr,\n\n    /// IP address where tunnels will listen on.\n    bind_tunnels: IpAddr,\n}\n\nimpl Server {\n    /// Create a new server with a specified minimum port number.\n    pub fn new(port_range: RangeInclusive<u16>, secret: Option<&str>) -> Self {\n        assert!(!port_range.is_empty(), \"must provide at least one port\");\n        Server {\n            port_range,\n            conns: Arc::new(DashMap::new()),\n            auth: secret.map(Authenticator::new),\n            bind_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n            bind_tunnels: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n        }\n    }\n\n    /// Set the IP address where tunnels will listen on.\n    pub fn set_bind_addr(&mut self, bind_addr: IpAddr) {\n        self.bind_addr = bind_addr;\n    }\n\n    /// Set the IP address where the control server will bind to.\n    pub fn set_bind_tunnels(&mut self, bind_tunnels: IpAddr) {\n        self.bind_tunnels = bind_tunnels;\n    }\n\n    /// Start the server, listening for new connections.\n    pub async fn listen(self) -> Result<()> {\n        let this = Arc::new(self);\n        let listener = TcpListener::bind((this.bind_addr, CONTROL_PORT)).await?;\n        info!(addr = ?this.bind_addr, \"server listening\");\n\n        loop {\n            let (stream, addr) = listener.accept().await?;\n            let this = Arc::clone(&this);\n            tokio::spawn(\n                async move {\n                    info!(\"incoming connection\");\n                    if let Err(err) = this.handle_connection(stream).await {\n                        warn!(%err, \"connection exited with error\");\n                    } else {\n                        info!(\"connection exited\");\n                    }\n                }\n                .instrument(info_span!(\"control\", ?addr)),\n            );\n        }\n    }\n\n    async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> {\n        let try_bind = |port: u16| async move {\n            TcpListener::bind((self.bind_tunnels, port))\n                .await\n                .map_err(|err| match err.kind() {\n                    io::ErrorKind::AddrInUse => \"port already in use\",\n                    io::ErrorKind::PermissionDenied => \"permission denied\",\n                    _ => \"failed to bind to port\",\n                })\n        };\n        if port > 0 {\n            // Client requests a specific port number.\n            if !self.port_range.contains(&port) {\n                return Err(\"client port number not in allowed range\");\n            }\n            try_bind(port).await\n        } else {\n            // Client requests any available port in range.\n            //\n            // In this case, we bind to 150 random port numbers. We choose this value because in\n            // order to find a free port with probability at least 1-δ, when ε proportion of the\n            // ports are currently available, it suffices to check approximately -2 ln(δ) / ε\n            // independently and uniformly chosen ports (up to a second-order term in ε).\n            //\n            // Checking 150 times gives us 99.999% success at utilizing 85% of ports under these\n            // conditions, when ε=0.15 and δ=0.00001.\n            for _ in 0..150 {\n                let port = fastrand::u16(self.port_range.clone());\n                match try_bind(port).await {\n                    Ok(listener) => return Ok(listener),\n                    Err(_) => continue,\n                }\n            }\n            Err(\"failed to find an available port\")\n        }\n    }\n\n    async fn handle_connection(&self, stream: TcpStream) -> Result<()> {\n        let mut stream = Delimited::new(stream);\n        if let Some(auth) = &self.auth {\n            if let Err(err) = auth.server_handshake(&mut stream).await {\n                warn!(%err, \"server handshake failed\");\n                stream.send(ServerMessage::Error(err.to_string())).await?;\n                return Ok(());\n            }\n        }\n\n        match stream.recv_timeout().await? {\n            Some(ClientMessage::Authenticate(_)) => {\n                warn!(\"unexpected authenticate\");\n                Ok(())\n            }\n            Some(ClientMessage::Hello(port)) => {\n                let listener = match self.create_listener(port).await {\n                    Ok(listener) => listener,\n                    Err(err) => {\n                        stream.send(ServerMessage::Error(err.into())).await?;\n                        return Ok(());\n                    }\n                };\n                let host = listener.local_addr()?.ip();\n                let port = listener.local_addr()?.port();\n                info!(?host, ?port, \"new client\");\n                stream.send(ServerMessage::Hello(port)).await?;\n\n                loop {\n                    if stream.send(ServerMessage::Heartbeat).await.is_err() {\n                        // Assume that the TCP connection has been dropped.\n                        return Ok(());\n                    }\n                    const TIMEOUT: Duration = Duration::from_millis(500);\n                    if let Ok(result) = timeout(TIMEOUT, listener.accept()).await {\n                        let (stream2, addr) = result?;\n                        info!(?addr, ?port, \"new connection\");\n\n                        let id = Uuid::new_v4();\n                        let conns = Arc::clone(&self.conns);\n\n                        conns.insert(id, stream2);\n                        tokio::spawn(async move {\n                            // Remove stale entries to avoid memory leaks.\n                            sleep(Duration::from_secs(10)).await;\n                            if conns.remove(&id).is_some() {\n                                warn!(%id, \"removed stale connection\");\n                            }\n                        });\n                        stream.send(ServerMessage::Connection(id)).await?;\n                    }\n                }\n            }\n            Some(ClientMessage::Accept(id)) => {\n                info!(%id, \"forwarding connection\");\n                match self.conns.remove(&id) {\n                    Some((_, mut stream2)) => {\n                        let mut parts = stream.into_parts();\n                        debug_assert!(parts.write_buf.is_empty(), \"framed write buffer not empty\");\n                        stream2.write_all(&parts.read_buf).await?;\n                        tokio::io::copy_bidirectional(&mut parts.io, &mut stream2).await?;\n                    }\n                    None => warn!(%id, \"missing connection\"),\n                }\n                Ok(())\n            }\n            None => Ok(()),\n        }\n    }\n}\n"
  },
  {
    "path": "src/shared.rs",
    "content": "//! Shared data structures, utilities, and protocol definitions.\n\nuse std::time::Duration;\n\nuse anyhow::{Context, Result};\nuse futures_util::{SinkExt, StreamExt};\nuse serde::{de::DeserializeOwned, Deserialize, Serialize};\nuse tokio::io::{AsyncRead, AsyncWrite};\nuse tokio::time::timeout;\nuse tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};\nuse tracing::trace;\nuse uuid::Uuid;\n\n/// TCP port used for control connections with the server.\npub const CONTROL_PORT: u16 = 7835;\n\n/// Maximum byte length for a JSON frame in the stream.\npub const MAX_FRAME_LENGTH: usize = 256;\n\n/// Timeout for network connections and initial protocol messages.\npub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3);\n\n/// A message from the client on the control connection.\n#[derive(Debug, Serialize, Deserialize)]\npub enum ClientMessage {\n    /// Response to an authentication challenge from the server.\n    Authenticate(String),\n\n    /// Initial client message specifying a port to forward.\n    Hello(u16),\n\n    /// Accepts an incoming TCP connection, using this stream as a proxy.\n    Accept(Uuid),\n}\n\n/// A message from the server on the control connection.\n#[derive(Debug, Serialize, Deserialize)]\npub enum ServerMessage {\n    /// Authentication challenge, sent as the first message, if enabled.\n    Challenge(Uuid),\n\n    /// Response to a client's initial message, with actual public port.\n    Hello(u16),\n\n    /// No-op used to test if the client is still reachable.\n    Heartbeat,\n\n    /// Asks the client to accept a forwarded TCP connection.\n    Connection(Uuid),\n\n    /// Indicates a server error that terminates the connection.\n    Error(String),\n}\n\n/// Transport stream with JSON frames delimited by null characters.\npub struct Delimited<U>(Framed<U, AnyDelimiterCodec>);\n\nimpl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {\n    /// Construct a new delimited stream.\n    pub fn new(stream: U) -> Self {\n        let codec = AnyDelimiterCodec::new_with_max_length(vec![0], vec![0], MAX_FRAME_LENGTH);\n        Self(Framed::new(stream, codec))\n    }\n\n    /// Read the next null-delimited JSON instruction from a stream.\n    pub async fn recv<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {\n        trace!(\"waiting to receive json message\");\n        if let Some(next_message) = self.0.next().await {\n            let byte_message = next_message.context(\"frame error, invalid byte length\")?;\n            let serialized_obj =\n                serde_json::from_slice(&byte_message).context(\"unable to parse message\")?;\n            Ok(serialized_obj)\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Read the next null-delimited JSON instruction, with a default timeout.\n    ///\n    /// This is useful for parsing the initial message of a stream for handshake or\n    /// other protocol purposes, where we do not want to wait indefinitely.\n    pub async fn recv_timeout<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {\n        timeout(NETWORK_TIMEOUT, self.recv())\n            .await\n            .context(\"timed out waiting for initial message\")?\n    }\n\n    /// Send a null-terminated JSON instruction on a stream.\n    pub async fn send<T: Serialize>(&mut self, msg: T) -> Result<()> {\n        trace!(\"sending json message\");\n        self.0.send(serde_json::to_string(&msg)?).await?;\n        Ok(())\n    }\n\n    /// Consume this object, returning current buffers and the inner transport.\n    pub fn into_parts(self) -> FramedParts<U, AnyDelimiterCodec> {\n        self.0.into_parts()\n    }\n}\n"
  },
  {
    "path": "tests/auth_test.rs",
    "content": "use anyhow::Result;\nuse bore_cli::{auth::Authenticator, shared::Delimited};\nuse tokio::io::{self};\n\n#[tokio::test]\nasync fn auth_handshake() -> Result<()> {\n    let auth = Authenticator::new(\"some secret string\");\n\n    let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.\n    let mut client = Delimited::new(client);\n    let mut server = Delimited::new(server);\n\n    tokio::try_join!(\n        auth.client_handshake(&mut client),\n        auth.server_handshake(&mut server),\n    )?;\n\n    Ok(())\n}\n\n#[tokio::test]\nasync fn auth_handshake_fail() {\n    let auth = Authenticator::new(\"client secret\");\n    let auth2 = Authenticator::new(\"different server secret\");\n\n    let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.\n    let mut client = Delimited::new(client);\n    let mut server = Delimited::new(server);\n\n    let result = tokio::try_join!(\n        auth.client_handshake(&mut client),\n        auth2.server_handshake(&mut server),\n    );\n    assert!(result.is_err());\n}\n"
  },
  {
    "path": "tests/e2e_test.rs",
    "content": "use std::net::SocketAddr;\nuse std::time::Duration;\n\nuse anyhow::{anyhow, Result};\nuse bore_cli::{client::Client, server::Server, shared::CONTROL_PORT};\nuse lazy_static::lazy_static;\nuse rstest::*;\nuse tokio::io::{AsyncReadExt, AsyncWriteExt};\nuse tokio::net::{TcpListener, TcpStream};\nuse tokio::sync::Mutex;\nuse tokio::time;\n\nlazy_static! {\n    /// Guard to make sure that tests are run serially, not concurrently.\n    static ref SERIAL_GUARD: Mutex<()> = Mutex::new(());\n}\n\n/// Spawn the server, giving some time for the control port TcpListener to start.\nasync fn spawn_server(secret: Option<&str>) {\n    tokio::spawn(Server::new(1024..=65535, secret).listen());\n    time::sleep(Duration::from_millis(50)).await;\n}\n\n/// Spawns a client with randomly assigned ports, returning the listener and remote address.\nasync fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, SocketAddr)> {\n    let listener = TcpListener::bind(\"localhost:0\").await?;\n    let local_port = listener.local_addr()?.port();\n    let client = Client::new(\"localhost\", local_port, \"localhost\", 0, secret).await?;\n    let remote_addr = ([127, 0, 0, 1], client.remote_port()).into();\n    tokio::spawn(client.listen());\n    Ok((listener, remote_addr))\n}\n\n#[rstest]\n#[tokio::test]\nasync fn basic_proxy(#[values(None, Some(\"\"), Some(\"abc\"))] secret: Option<&str>) -> Result<()> {\n    let _guard = SERIAL_GUARD.lock().await;\n\n    spawn_server(secret).await;\n    let (listener, addr) = spawn_client(secret).await?;\n\n    tokio::spawn(async move {\n        let (mut stream, _) = listener.accept().await?;\n        let mut buf = [0u8; 11];\n        stream.read_exact(&mut buf).await?;\n        assert_eq!(&buf, b\"hello world\");\n\n        stream.write_all(b\"I can send a message too!\").await?;\n        anyhow::Ok(())\n    });\n\n    let mut stream = TcpStream::connect(addr).await?;\n    stream.write_all(b\"hello world\").await?;\n\n    let mut buf = [0u8; 25];\n    stream.read_exact(&mut buf).await?;\n    assert_eq!(&buf, b\"I can send a message too!\");\n\n    // Ensure that the client end of the stream is closed now.\n    assert_eq!(stream.read(&mut buf).await?, 0);\n\n    // Also ensure that additional connections do not produce any data.\n    let mut stream = TcpStream::connect(addr).await?;\n    assert_eq!(stream.read(&mut buf).await?, 0);\n\n    Ok(())\n}\n\n#[rstest]\n#[case(None, Some(\"my secret\"))]\n#[case(Some(\"my secret\"), None)]\n#[tokio::test]\nasync fn mismatched_secret(\n    #[case] server_secret: Option<&str>,\n    #[case] client_secret: Option<&str>,\n) {\n    let _guard = SERIAL_GUARD.lock().await;\n\n    spawn_server(server_secret).await;\n    assert!(spawn_client(client_secret).await.is_err());\n}\n\n#[tokio::test]\nasync fn invalid_address() -> Result<()> {\n    // We don't need the serial guard for this test because it doesn't create a server.\n    async fn check_address(to: &str, use_secret: bool) -> Result<()> {\n        match Client::new(\"localhost\", 5000, to, 0, use_secret.then_some(\"a secret\")).await {\n            Ok(_) => Err(anyhow!(\"expected error for {to}, use_secret={use_secret}\")),\n            Err(_) => Ok(()),\n        }\n    }\n    tokio::try_join!(\n        check_address(\"google.com\", false),\n        check_address(\"google.com\", true),\n        check_address(\"nonexistent.domain.for.demonstration\", false),\n        check_address(\"nonexistent.domain.for.demonstration\", true),\n        check_address(\"malformed !$uri$%\", false),\n        check_address(\"malformed !$uri$%\", true),\n    )?;\n    Ok(())\n}\n\n#[tokio::test]\nasync fn very_long_frame() -> Result<()> {\n    let _guard = SERIAL_GUARD.lock().await;\n\n    spawn_server(None).await;\n    let mut attacker = TcpStream::connect((\"localhost\", CONTROL_PORT)).await?;\n\n    // Slowly send a very long frame.\n    for _ in 0..10 {\n        let result = attacker.write_all(&[42u8; 100000]).await;\n        if result.is_err() {\n            return Ok(());\n        }\n        time::sleep(Duration::from_millis(10)).await;\n    }\n    panic!(\"did not exit after a 1 MB frame\");\n}\n\n#[test]\n#[should_panic]\nfn empty_port_range() {\n    let min_port = 5000;\n    let max_port = 3000;\n    let _ = Server::new(min_port..=max_port, None);\n}\n\n#[tokio::test]\nasync fn half_closed_tcp_stream() -> Result<()> {\n    // Check that \"half-closed\" TCP streams will not result in spontaneous hangups.\n    let _guard = SERIAL_GUARD.lock().await;\n\n    spawn_server(None).await;\n    let (listener, addr) = spawn_client(None).await?;\n\n    let (mut cli, (mut srv, _)) = tokio::try_join!(TcpStream::connect(addr), listener.accept())?;\n\n    // Send data before half-closing one of the streams.\n    let mut buf = b\"message before shutdown\".to_vec();\n    cli.write_all(&buf).await?;\n\n    // Only close the write half of the stream. This is a half-closed stream. In the\n    // TCP protocol, it is represented as a FIN packet on one end. The entire stream\n    // is only closed after two FINs are exchanged and ACKed by the other end.\n    cli.shutdown().await?;\n\n    srv.read_exact(&mut buf).await?;\n    assert_eq!(buf, b\"message before shutdown\");\n    assert_eq!(srv.read(&mut buf).await?, 0); // EOF\n\n    // Now make sure that the other stream can still send data, despite\n    // half-shutdown on client->server side.\n    let mut buf = b\"hello from the other side!\".to_vec();\n    srv.write_all(&buf).await?;\n    cli.read_exact(&mut buf).await?;\n    assert_eq!(buf, b\"hello from the other side!\");\n\n    // We don't have to think about CLOSE_RD handling because that's not really\n    // part of the TCP protocol, just the POSIX streams API. It is implemented by\n    // the OS ignoring future packets received on that stream.\n\n    Ok(())\n}\n"
  }
]