Repository: ekzhang/bore
Branch: main
Commit: 00a735a89917
Files: 23
Total size: 50.5 KB
Directory structure:
gitextract_o1_hc2gb/
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ ├── docker.yml
│ ├── mean_bean_ci.yml
│ └── mean_bean_deploy.yml
├── .gitignore
├── Cargo.toml
├── Dockerfile
├── LICENSE
├── README.md
├── ci/
│ ├── build.bash
│ ├── common.bash
│ ├── set_rust_version.bash
│ └── test.bash
├── src/
│ ├── auth.rs
│ ├── client.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── server.rs
│ └── shared.rs
└── tests/
├── auth_test.rs
└── e2e_test.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
/target
================================================
FILE: .github/FUNDING.yml
================================================
github: [ekzhang]
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
rust:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- run: cargo build --all-features
- run: cargo test
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt
- run: cargo fmt -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
- run: cargo clippy -- -D warnings
================================================
FILE: .github/workflows/docker.yml
================================================
name: Docker
on:
push:
tags:
- "v*.*.*"
jobs:
build_deploy:
name: Build and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ekzhang/bore
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
================================================
FILE: .github/workflows/mean_bean_ci.yml
================================================
name: Mean Bean CI
on:
push:
branches:
- main
pull_request:
jobs:
install-cross:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: XAMPPRocky/get-github-release@v1
id: cross
with:
owner: rust-embedded
repo: cross
matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }}
strategy:
matrix:
platform: [linux-musl]
macos:
runs-on: macos-latest
strategy:
fail-fast: true
matrix:
channel: [stable]
target:
- aarch64-apple-darwin
steps:
- name: Setup | Checkout
uses: actions/checkout@v4
- name: Setup | Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
target: ${{ matrix.target }}
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --target ${{ matrix.target }}
use-cross: false
linux:
runs-on: ubuntu-latest
needs: install-cross
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Download Cross
uses: actions/download-artifact@v4
with:
name: cross-linux-musl
path: /tmp/
- run: chmod +x /tmp/cross
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
- run: ci/build.bash /tmp/cross ${{ matrix.target }}
# These targets have issues with being tested so they are disabled
# by default. You can try disabling to see if they work for
# your project.
- run: ci/test.bash /tmp/cross ${{ matrix.target }}
if: |
!contains(matrix.target, 'android') &&
!contains(matrix.target, 'bsd') &&
!contains(matrix.target, 'solaris') &&
matrix.target != 'armv5te-unknown-linux-musleabi' &&
matrix.target != 'sparc64-unknown-linux-gnu'
strategy:
fail-fast: true
matrix:
channel: [stable]
target:
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabi
- arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- i686-unknown-linux-musl
- x86_64-unknown-linux-musl
windows:
runs-on: windows-latest
# Windows technically doesn't need this, but if we don't block windows on it
# some of the windows jobs could fill up the concurrent job queue before
# one of the install-cross jobs has started, so this makes sure all
# artifacts are downloaded first.
needs: install-cross
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
shell: bash
- run: ci/build.bash cargo ${{ matrix.target }}
shell: bash
- run: ci/test.bash cargo ${{ matrix.target }}
shell: bash
strategy:
fail-fast: true
matrix:
channel: [stable]
target:
# MSVC
- i686-pc-windows-msvc
- x86_64-pc-windows-msvc
# GNU: You typically only need to test Windows GNU if you're
# specifically targetting it, and it can cause issues with some
# dependencies if you're not so it's disabled by self.
# - i686-pc-windows-gnu
# - x86_64-pc-windows-gnu
================================================
FILE: .github/workflows/mean_bean_deploy.yml
================================================
on:
push:
# # Sequence of patterns matched against refs/tags
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
name: Mean Bean Deploy
env:
BIN: bore
jobs:
# This job downloads and stores `cross` as an artifact, so that it can be
# redownloaded across all of the jobs. Currently this copied pasted between
# `mean_bean_ci.yml` and `mean_bean_deploy.yml`. Make sure to update both places when making
# changes.
install-cross:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: XAMPPRocky/get-github-release@v1
id: cross
with:
owner: rust-embedded
repo: cross
matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
with:
name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }}
strategy:
matrix:
platform: [linux-musl]
macos:
runs-on: macos-latest
strategy:
matrix:
target:
# macOS
- x86_64-apple-darwin
- aarch64-apple-darwin
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Setup | Checkout
uses: actions/checkout@v4
# Cache files between builds
- name: Setup | Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.target }}
- name: Setup | Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
target: ${{ matrix.target }}
- name: Build | Build
uses: actions-rs/cargo@v1
with:
command: build
args: --release --target ${{ matrix.target }}
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- uses: actions/upload-release-asset@v1
id: upload-release-asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.tar.gz
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
asset_content_type: application/gzip
linux:
runs-on: ubuntu-latest
needs: install-cross
strategy:
matrix:
target:
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabi
- arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- i686-unknown-linux-musl
- x86_64-unknown-linux-musl
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: cross-linux-musl
path: /tmp/
- run: chmod +x /tmp/cross
- run: ci/set_rust_version.bash stable ${{ matrix.target }}
- run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.tar.gz
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
asset_content_type: application/gzip
windows:
runs-on: windows-latest
needs: install-cross
strategy:
matrix:
target:
# MSVC
- i686-pc-windows-msvc
- x86_64-pc-windows-msvc
# GNU
# - i686-pc-windows-gnu
# - x86_64-pc-windows-gnu
steps:
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v4
- run: bash ci/set_rust_version.bash stable ${{ matrix.target }}
- run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE
- run: |
cd ./target/${{ matrix.target }}/release/
7z a "${{ env.BIN }}.zip" "${{ env.BIN }}.exe"
mv "${{ env.BIN }}.zip" $GITHUB_WORKSPACE
shell: bash
# We're using using a fork of `actions/create-release` that detects
# whether a release is already available or not first.
- uses: XAMPPRocky/create-release@v1.0.2
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
# Draft should **always** be false. GitHub doesn't provide a way to
# get draft releases from its API, so there's no point using it.
draft: false
prerelease: false
- uses: actions/upload-release-asset@v1
id: upload-release-asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.BIN }}.zip
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.zip
asset_content_type: application/zip
================================================
FILE: .gitignore
================================================
/target
================================================
FILE: Cargo.toml
================================================
[package]
name = "bore-cli"
version = "0.6.0"
authors = ["Eric Zhang <ekzhang1@gmail.com>"]
license = "MIT"
description = "A modern, simple TCP tunnel in Rust that exposes local ports to a remote server, bypassing standard NAT connection firewalls."
repository = "https://github.com/ekzhang/bore"
documentation = "https://docs.rs/bore-cli"
keywords = ["network", "cli", "tunnel", "tcp"]
categories = ["network-programming", "web-programming", "command-line-utilities"]
readme = "README.md"
edition = "2021"
[[bin]]
name = "bore"
path = "src/main.rs"
[dependencies]
anyhow = { version = "1.0.56", features = ["backtrace"] }
clap = { version = "4.0.22", features = ["derive", "env"] }
dashmap = "5.2.0"
fastrand = "1.9.0"
futures-util = { version = "0.3.21", features = ["sink"] }
hex = "0.4.3"
hmac = "0.12.1"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
sha2 = "0.10.2"
tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros", "net", "time"] }
tokio-util = { version = "0.7.1", features = ["codec"] }
tracing = "0.1.32"
tracing-subscriber = "0.3.18"
uuid = { version = "1.2.1", features = ["serde", "v4"] }
[dev-dependencies]
lazy_static = "1.4.0"
rstest = "0.15.0"
tokio = { version = "1.17.0", features = ["sync"] }
================================================
FILE: Dockerfile
================================================
FROM rust:alpine AS builder
WORKDIR /home/rust/src
RUN apk --no-cache add musl-dev
COPY . .
RUN cargo install --path .
FROM scratch
COPY --from=builder /usr/local/cargo/bin/bore .
USER 1000:1000
ENTRYPOINT ["./bore"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Eric Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# bore
[](https://github.com/ekzhang/bore/actions)
[](https://crates.io/crates/bore-cli)
A 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.**

```shell
# Installation (requires Rust, see alternatives below)
cargo install bore-cli
# On your local machine
bore local 8000 --to bore.pub
```
This will expose your local port at `localhost:8000` to the public internet at `bore.pub:<PORT>`, where the port number is assigned randomly.
Similar 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.
(`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.)
## Installation
### macOS
`bore` is packaged as a Homebrew core formula.
```shell
brew install bore-cli
```
### Linux
#### Arch Linux
`bore` is available in the AUR as `bore`.
```shell
yay -S bore # or your favorite AUR helper
```
#### Gentoo Linux
`bore` is available in the [gentoo-zh](https://github.com/microcai/gentoo-zh) overlay.
```shell
sudo eselect repository enable gentoo-zh
sudo emerge --sync gentoo-zh
sudo emerge net-proxy/bore
```
### Binary Distribution
Otherwise, 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.
### Cargo
You 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.
```shell
cargo install bore-cli
```
### Docker
We 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.
```shell
docker run -it --init --rm --network host ekzhang/bore <ARGS>
```
## Detailed Usage
This section describes detailed usage for the `bore` CLI command.
### Local Forwarding
You 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.
```shell
bore local 5000 --to bore.pub
```
You 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`.
The full options are shown below.
```shell
Starts a local proxy to the remote server
Usage: bore local [OPTIONS] --to <TO> <LOCAL_PORT>
Arguments:
<LOCAL_PORT> The local port to expose [env: BORE_LOCAL_PORT=]
Options:
-l, --local-host <HOST> The local host to expose [default: localhost]
-t, --to <TO> Address of the remote server to expose local ports to [env: BORE_SERVER=]
-p, --port <PORT> Optional port on the remote server to select [default: 0]
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
-h, --help Print help
```
### Self-Hosting
As 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:
```shell
bore server
```
That'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.
It'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.
The full options for the `bore server` command are shown below.
```shell
Runs the remote proxy server
Usage: bore server [OPTIONS]
Options:
--min-port <MIN_PORT> Minimum accepted TCP port number [env: BORE_MIN_PORT=] [default: 1024]
--max-port <MAX_PORT> Maximum accepted TCP port number [env: BORE_MAX_PORT=] [default: 65535]
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
--bind-addr <BIND_ADDR> IP address to bind to, clients must reach this [default: 0.0.0.0]
--bind-tunnels <BIND_TUNNELS> IP address where tunnels will listen on, defaults to --bind-addr
-h, --help Print help
```
## Protocol
There 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.
Whenever 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.
For 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.
## Authentication
On 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.)
```shell
# on the server
bore server --secret my_secret_string
# on the client
bore local <LOCAL_PORT> --to <TO> --secret my_secret_string
```
If a secret is not present in the arguments, `bore` will also attempt to read from the `BORE_SECRET` environment variable.
## Acknowledgements
Created by Eric Zhang ([@ekzhang1](https://twitter.com/ekzhang1)). Licensed under the [MIT license](LICENSE).
The 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.
================================================
FILE: ci/build.bash
================================================
#!/usr/bin/env bash
# Script for building your rust projects.
set -e
source ci/common.bash
# $1 {path} = Path to cross/cargo executable
CROSS=$1
# $1 {string} = <Target Triple> e.g. x86_64-pc-windows-msvc
TARGET_TRIPLE=$2
# $3 {boolean} = Are we building for deployment?
RELEASE_BUILD=$3
required_arg $CROSS 'CROSS'
required_arg $TARGET_TRIPLE '<Target Triple>'
if [ -z "$RELEASE_BUILD" ]; then
$CROSS build --target $TARGET_TRIPLE
$CROSS build --target $TARGET_TRIPLE --all-features
else
$CROSS build --target $TARGET_TRIPLE --all-features --release
fi
================================================
FILE: ci/common.bash
================================================
required_arg() {
if [ -z "$1" ]; then
echo "Required argument $2 missing"
exit 1
fi
}
================================================
FILE: ci/set_rust_version.bash
================================================
#!/usr/bin/env bash
set -e
rustup default $1
rustup target add $2
================================================
FILE: ci/test.bash
================================================
#!/usr/bin/env bash
# Script for building your rust projects.
set -e
source ci/common.bash
# $1 {path} = Path to cross/cargo executable
CROSS=$1
# $1 {string} = <Target Triple>
TARGET_TRIPLE=$2
required_arg $CROSS 'CROSS'
required_arg $TARGET_TRIPLE '<Target Triple>'
max_attempts=3
count=0
while [ $count -lt $max_attempts ]; do
$CROSS test --target $TARGET_TRIPLE
status=$?
if [ $status -eq 0 ]; then
echo "Test passed"
break
else
echo "Test failed, attempt $(($count + 1))"
fi
count=$(($count + 1))
done
if [ $status -ne 0 ]; then
echo "Test failed after $max_attempts attempts"
fi
================================================
FILE: src/auth.rs
================================================
//! Auth implementation for bore client and server.
use anyhow::{bail, ensure, Result};
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use tokio::io::{AsyncRead, AsyncWrite};
use uuid::Uuid;
use crate::shared::{ClientMessage, Delimited, ServerMessage};
/// Wrapper around a MAC used for authenticating clients that have a secret.
pub struct Authenticator(Hmac<Sha256>);
impl Authenticator {
/// Generate an authenticator from a secret.
pub fn new(secret: &str) -> Self {
let hashed_secret = Sha256::new().chain_update(secret).finalize();
Self(Hmac::new_from_slice(&hashed_secret).expect("HMAC can take key of any size"))
}
/// Generate a reply message for a challenge.
pub fn answer(&self, challenge: &Uuid) -> String {
let mut hmac = self.0.clone();
hmac.update(challenge.as_bytes());
hex::encode(hmac.finalize().into_bytes())
}
/// Validate a reply to a challenge.
///
/// ```
/// use bore_cli::auth::Authenticator;
/// use uuid::Uuid;
///
/// let auth = Authenticator::new("secret");
/// let challenge = Uuid::new_v4();
///
/// assert!(auth.validate(&challenge, &auth.answer(&challenge)));
/// assert!(!auth.validate(&challenge, "wrong answer"));
/// ```
pub fn validate(&self, challenge: &Uuid, tag: &str) -> bool {
if let Ok(tag) = hex::decode(tag) {
let mut hmac = self.0.clone();
hmac.update(challenge.as_bytes());
hmac.verify_slice(&tag).is_ok()
} else {
false
}
}
/// As the server, send a challenge to the client and validate their response.
pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(
&self,
stream: &mut Delimited<T>,
) -> Result<()> {
let challenge = Uuid::new_v4();
stream.send(ServerMessage::Challenge(challenge)).await?;
match stream.recv_timeout().await? {
Some(ClientMessage::Authenticate(tag)) => {
ensure!(self.validate(&challenge, &tag), "invalid secret");
Ok(())
}
_ => bail!("server requires secret, but no secret was provided"),
}
}
/// As the client, answer a challenge to attempt to authenticate with the server.
pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(
&self,
stream: &mut Delimited<T>,
) -> Result<()> {
let challenge = match stream.recv_timeout().await? {
Some(ServerMessage::Challenge(challenge)) => challenge,
_ => bail!("expected authentication challenge, but no secret was required"),
};
let tag = self.answer(&challenge);
stream.send(ClientMessage::Authenticate(tag)).await?;
Ok(())
}
}
================================================
FILE: src/client.rs
================================================
//! Client implementation for the `bore` service.
use std::sync::Arc;
use anyhow::{bail, Context, Result};
use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};
use tracing::{error, info, info_span, warn, Instrument};
use uuid::Uuid;
use crate::auth::Authenticator;
use crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT};
/// State structure for the client.
pub struct Client {
/// Control connection to the server.
conn: Option<Delimited<TcpStream>>,
/// Destination address of the server.
to: String,
// Local host that is forwarded.
local_host: String,
/// Local port that is forwarded.
local_port: u16,
/// Port that is publicly available on the remote.
remote_port: u16,
/// Optional secret used to authenticate clients.
auth: Option<Authenticator>,
}
impl Client {
/// Create a new client.
pub async fn new(
local_host: &str,
local_port: u16,
to: &str,
port: u16,
secret: Option<&str>,
) -> Result<Self> {
let mut stream = Delimited::new(connect_with_timeout(to, CONTROL_PORT).await?);
let auth = secret.map(Authenticator::new);
if let Some(auth) = &auth {
auth.client_handshake(&mut stream).await?;
}
stream.send(ClientMessage::Hello(port)).await?;
let remote_port = match stream.recv_timeout().await? {
Some(ServerMessage::Hello(remote_port)) => remote_port,
Some(ServerMessage::Error(message)) => bail!("server error: {message}"),
Some(ServerMessage::Challenge(_)) => {
bail!("server requires authentication, but no client secret was provided");
}
Some(_) => bail!("unexpected initial non-hello message"),
None => bail!("unexpected EOF"),
};
info!(remote_port, "connected to server");
info!("listening at {to}:{remote_port}");
Ok(Client {
conn: Some(stream),
to: to.to_string(),
local_host: local_host.to_string(),
local_port,
remote_port,
auth,
})
}
/// Returns the port publicly available on the remote.
pub fn remote_port(&self) -> u16 {
self.remote_port
}
/// Start the client, listening for new connections.
pub async fn listen(mut self) -> Result<()> {
let mut conn = self.conn.take().unwrap();
let this = Arc::new(self);
loop {
match conn.recv().await? {
Some(ServerMessage::Hello(_)) => warn!("unexpected hello"),
Some(ServerMessage::Challenge(_)) => warn!("unexpected challenge"),
Some(ServerMessage::Heartbeat) => (),
Some(ServerMessage::Connection(id)) => {
let this = Arc::clone(&this);
tokio::spawn(
async move {
info!("new connection");
match this.handle_connection(id).await {
Ok(_) => info!("connection exited"),
Err(err) => warn!(%err, "connection exited with error"),
}
}
.instrument(info_span!("proxy", %id)),
);
}
Some(ServerMessage::Error(err)) => error!(%err, "server error"),
None => return Ok(()),
}
}
}
async fn handle_connection(&self, id: Uuid) -> Result<()> {
let mut remote_conn =
Delimited::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?);
if let Some(auth) = &self.auth {
auth.client_handshake(&mut remote_conn).await?;
}
remote_conn.send(ClientMessage::Accept(id)).await?;
let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;
let mut parts = remote_conn.into_parts();
debug_assert!(parts.write_buf.is_empty(), "framed write buffer not empty");
local_conn.write_all(&parts.read_buf).await?; // mostly of the cases, this will be empty
tokio::io::copy_bidirectional(&mut local_conn, &mut parts.io).await?;
Ok(())
}
}
async fn connect_with_timeout(to: &str, port: u16) -> Result<TcpStream> {
match timeout(NETWORK_TIMEOUT, TcpStream::connect((to, port))).await {
Ok(res) => res,
Err(err) => Err(err.into()),
}
.with_context(|| format!("could not connect to {to}:{port}"))
}
================================================
FILE: src/lib.rs
================================================
//! A modern, simple TCP tunnel in Rust that exposes local ports to a remote
//! server, bypassing standard NAT connection firewalls.
//!
//! This is the library crate documentation. If you're looking for usage
//! information about the binary, see the command below.
//!
//! ```shell
//! $ bore help
//! ```
//!
//! There are two components to the crate, offering implementations of the
//! server network daemon and client local forwarding proxy. Both are public
//! members and can be run programmatically with a Tokio 1.0 runtime.
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod auth;
pub mod client;
pub mod server;
pub mod shared;
================================================
FILE: src/main.rs
================================================
use std::net::IpAddr;
use anyhow::Result;
use bore_cli::{client::Client, server::Server};
use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
#[derive(Parser, Debug)]
#[clap(author, version, about)]
struct Args {
#[clap(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
/// Starts a local proxy to the remote server.
Local {
/// The local port to expose.
#[clap(env = "BORE_LOCAL_PORT")]
local_port: u16,
/// The local host to expose.
#[clap(short, long, value_name = "HOST", default_value = "localhost")]
local_host: String,
/// Address of the remote server to expose local ports to.
#[clap(short, long, env = "BORE_SERVER")]
to: String,
/// Optional port on the remote server to select.
#[clap(short, long, default_value_t = 0)]
port: u16,
/// Optional secret for authentication.
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
secret: Option<String>,
},
/// Runs the remote proxy server.
Server {
/// Minimum accepted TCP port number.
#[clap(long, default_value_t = 1024, env = "BORE_MIN_PORT")]
min_port: u16,
/// Maximum accepted TCP port number.
#[clap(long, default_value_t = 65535, env = "BORE_MAX_PORT")]
max_port: u16,
/// Optional secret for authentication.
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
secret: Option<String>,
/// IP address to bind to, clients must reach this.
#[clap(long, default_value = "0.0.0.0")]
bind_addr: IpAddr,
/// IP address where tunnels will listen on, defaults to --bind-addr.
#[clap(long)]
bind_tunnels: Option<IpAddr>,
},
}
#[tokio::main]
async fn run(command: Command) -> Result<()> {
match command {
Command::Local {
local_host,
local_port,
to,
port,
secret,
} => {
let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?;
client.listen().await?;
}
Command::Server {
min_port,
max_port,
secret,
bind_addr,
bind_tunnels,
} => {
let port_range = min_port..=max_port;
if port_range.is_empty() {
Args::command()
.error(ErrorKind::InvalidValue, "port range is empty")
.exit();
}
let mut server = Server::new(port_range, secret.as_deref());
server.set_bind_addr(bind_addr);
server.set_bind_tunnels(bind_tunnels.unwrap_or(bind_addr));
server.listen().await?;
}
}
Ok(())
}
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
run(Args::parse().command)
}
================================================
FILE: src/server.rs
================================================
//! Server implementation for the `bore` service.
use std::net::{IpAddr, Ipv4Addr};
use std::{io, ops::RangeInclusive, sync::Arc, time::Duration};
use anyhow::Result;
use dashmap::DashMap;
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::time::{sleep, timeout};
use tracing::{info, info_span, warn, Instrument};
use uuid::Uuid;
use crate::auth::Authenticator;
use crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT};
/// State structure for the server.
pub struct Server {
/// Range of TCP ports that can be forwarded.
port_range: RangeInclusive<u16>,
/// Optional secret used to authenticate clients.
auth: Option<Authenticator>,
/// Concurrent map of IDs to incoming connections.
conns: Arc<DashMap<Uuid, TcpStream>>,
/// IP address where the control server will bind to.
bind_addr: IpAddr,
/// IP address where tunnels will listen on.
bind_tunnels: IpAddr,
}
impl Server {
/// Create a new server with a specified minimum port number.
pub fn new(port_range: RangeInclusive<u16>, secret: Option<&str>) -> Self {
assert!(!port_range.is_empty(), "must provide at least one port");
Server {
port_range,
conns: Arc::new(DashMap::new()),
auth: secret.map(Authenticator::new),
bind_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
bind_tunnels: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
}
}
/// Set the IP address where tunnels will listen on.
pub fn set_bind_addr(&mut self, bind_addr: IpAddr) {
self.bind_addr = bind_addr;
}
/// Set the IP address where the control server will bind to.
pub fn set_bind_tunnels(&mut self, bind_tunnels: IpAddr) {
self.bind_tunnels = bind_tunnels;
}
/// Start the server, listening for new connections.
pub async fn listen(self) -> Result<()> {
let this = Arc::new(self);
let listener = TcpListener::bind((this.bind_addr, CONTROL_PORT)).await?;
info!(addr = ?this.bind_addr, "server listening");
loop {
let (stream, addr) = listener.accept().await?;
let this = Arc::clone(&this);
tokio::spawn(
async move {
info!("incoming connection");
if let Err(err) = this.handle_connection(stream).await {
warn!(%err, "connection exited with error");
} else {
info!("connection exited");
}
}
.instrument(info_span!("control", ?addr)),
);
}
}
async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> {
let try_bind = |port: u16| async move {
TcpListener::bind((self.bind_tunnels, port))
.await
.map_err(|err| match err.kind() {
io::ErrorKind::AddrInUse => "port already in use",
io::ErrorKind::PermissionDenied => "permission denied",
_ => "failed to bind to port",
})
};
if port > 0 {
// Client requests a specific port number.
if !self.port_range.contains(&port) {
return Err("client port number not in allowed range");
}
try_bind(port).await
} else {
// Client requests any available port in range.
//
// In this case, we bind to 150 random port numbers. We choose this value because in
// order to find a free port with probability at least 1-δ, when ε proportion of the
// ports are currently available, it suffices to check approximately -2 ln(δ) / ε
// independently and uniformly chosen ports (up to a second-order term in ε).
//
// Checking 150 times gives us 99.999% success at utilizing 85% of ports under these
// conditions, when ε=0.15 and δ=0.00001.
for _ in 0..150 {
let port = fastrand::u16(self.port_range.clone());
match try_bind(port).await {
Ok(listener) => return Ok(listener),
Err(_) => continue,
}
}
Err("failed to find an available port")
}
}
async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
let mut stream = Delimited::new(stream);
if let Some(auth) = &self.auth {
if let Err(err) = auth.server_handshake(&mut stream).await {
warn!(%err, "server handshake failed");
stream.send(ServerMessage::Error(err.to_string())).await?;
return Ok(());
}
}
match stream.recv_timeout().await? {
Some(ClientMessage::Authenticate(_)) => {
warn!("unexpected authenticate");
Ok(())
}
Some(ClientMessage::Hello(port)) => {
let listener = match self.create_listener(port).await {
Ok(listener) => listener,
Err(err) => {
stream.send(ServerMessage::Error(err.into())).await?;
return Ok(());
}
};
let host = listener.local_addr()?.ip();
let port = listener.local_addr()?.port();
info!(?host, ?port, "new client");
stream.send(ServerMessage::Hello(port)).await?;
loop {
if stream.send(ServerMessage::Heartbeat).await.is_err() {
// Assume that the TCP connection has been dropped.
return Ok(());
}
const TIMEOUT: Duration = Duration::from_millis(500);
if let Ok(result) = timeout(TIMEOUT, listener.accept()).await {
let (stream2, addr) = result?;
info!(?addr, ?port, "new connection");
let id = Uuid::new_v4();
let conns = Arc::clone(&self.conns);
conns.insert(id, stream2);
tokio::spawn(async move {
// Remove stale entries to avoid memory leaks.
sleep(Duration::from_secs(10)).await;
if conns.remove(&id).is_some() {
warn!(%id, "removed stale connection");
}
});
stream.send(ServerMessage::Connection(id)).await?;
}
}
}
Some(ClientMessage::Accept(id)) => {
info!(%id, "forwarding connection");
match self.conns.remove(&id) {
Some((_, mut stream2)) => {
let mut parts = stream.into_parts();
debug_assert!(parts.write_buf.is_empty(), "framed write buffer not empty");
stream2.write_all(&parts.read_buf).await?;
tokio::io::copy_bidirectional(&mut parts.io, &mut stream2).await?;
}
None => warn!(%id, "missing connection"),
}
Ok(())
}
None => Ok(()),
}
}
}
================================================
FILE: src/shared.rs
================================================
//! Shared data structures, utilities, and protocol definitions.
use std::time::Duration;
use anyhow::{Context, Result};
use futures_util::{SinkExt, StreamExt};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::timeout;
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
use tracing::trace;
use uuid::Uuid;
/// TCP port used for control connections with the server.
pub const CONTROL_PORT: u16 = 7835;
/// Maximum byte length for a JSON frame in the stream.
pub const MAX_FRAME_LENGTH: usize = 256;
/// Timeout for network connections and initial protocol messages.
pub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3);
/// A message from the client on the control connection.
#[derive(Debug, Serialize, Deserialize)]
pub enum ClientMessage {
/// Response to an authentication challenge from the server.
Authenticate(String),
/// Initial client message specifying a port to forward.
Hello(u16),
/// Accepts an incoming TCP connection, using this stream as a proxy.
Accept(Uuid),
}
/// A message from the server on the control connection.
#[derive(Debug, Serialize, Deserialize)]
pub enum ServerMessage {
/// Authentication challenge, sent as the first message, if enabled.
Challenge(Uuid),
/// Response to a client's initial message, with actual public port.
Hello(u16),
/// No-op used to test if the client is still reachable.
Heartbeat,
/// Asks the client to accept a forwarded TCP connection.
Connection(Uuid),
/// Indicates a server error that terminates the connection.
Error(String),
}
/// Transport stream with JSON frames delimited by null characters.
pub struct Delimited<U>(Framed<U, AnyDelimiterCodec>);
impl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {
/// Construct a new delimited stream.
pub fn new(stream: U) -> Self {
let codec = AnyDelimiterCodec::new_with_max_length(vec![0], vec![0], MAX_FRAME_LENGTH);
Self(Framed::new(stream, codec))
}
/// Read the next null-delimited JSON instruction from a stream.
pub async fn recv<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
trace!("waiting to receive json message");
if let Some(next_message) = self.0.next().await {
let byte_message = next_message.context("frame error, invalid byte length")?;
let serialized_obj =
serde_json::from_slice(&byte_message).context("unable to parse message")?;
Ok(serialized_obj)
} else {
Ok(None)
}
}
/// Read the next null-delimited JSON instruction, with a default timeout.
///
/// This is useful for parsing the initial message of a stream for handshake or
/// other protocol purposes, where we do not want to wait indefinitely.
pub async fn recv_timeout<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
timeout(NETWORK_TIMEOUT, self.recv())
.await
.context("timed out waiting for initial message")?
}
/// Send a null-terminated JSON instruction on a stream.
pub async fn send<T: Serialize>(&mut self, msg: T) -> Result<()> {
trace!("sending json message");
self.0.send(serde_json::to_string(&msg)?).await?;
Ok(())
}
/// Consume this object, returning current buffers and the inner transport.
pub fn into_parts(self) -> FramedParts<U, AnyDelimiterCodec> {
self.0.into_parts()
}
}
================================================
FILE: tests/auth_test.rs
================================================
use anyhow::Result;
use bore_cli::{auth::Authenticator, shared::Delimited};
use tokio::io::{self};
#[tokio::test]
async fn auth_handshake() -> Result<()> {
let auth = Authenticator::new("some secret string");
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
let mut client = Delimited::new(client);
let mut server = Delimited::new(server);
tokio::try_join!(
auth.client_handshake(&mut client),
auth.server_handshake(&mut server),
)?;
Ok(())
}
#[tokio::test]
async fn auth_handshake_fail() {
let auth = Authenticator::new("client secret");
let auth2 = Authenticator::new("different server secret");
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
let mut client = Delimited::new(client);
let mut server = Delimited::new(server);
let result = tokio::try_join!(
auth.client_handshake(&mut client),
auth2.server_handshake(&mut server),
);
assert!(result.is_err());
}
================================================
FILE: tests/e2e_test.rs
================================================
use std::net::SocketAddr;
use std::time::Duration;
use anyhow::{anyhow, Result};
use bore_cli::{client::Client, server::Server, shared::CONTROL_PORT};
use lazy_static::lazy_static;
use rstest::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex;
use tokio::time;
lazy_static! {
/// Guard to make sure that tests are run serially, not concurrently.
static ref SERIAL_GUARD: Mutex<()> = Mutex::new(());
}
/// Spawn the server, giving some time for the control port TcpListener to start.
async fn spawn_server(secret: Option<&str>) {
tokio::spawn(Server::new(1024..=65535, secret).listen());
time::sleep(Duration::from_millis(50)).await;
}
/// Spawns a client with randomly assigned ports, returning the listener and remote address.
async fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, SocketAddr)> {
let listener = TcpListener::bind("localhost:0").await?;
let local_port = listener.local_addr()?.port();
let client = Client::new("localhost", local_port, "localhost", 0, secret).await?;
let remote_addr = ([127, 0, 0, 1], client.remote_port()).into();
tokio::spawn(client.listen());
Ok((listener, remote_addr))
}
#[rstest]
#[tokio::test]
async fn basic_proxy(#[values(None, Some(""), Some("abc"))] secret: Option<&str>) -> Result<()> {
let _guard = SERIAL_GUARD.lock().await;
spawn_server(secret).await;
let (listener, addr) = spawn_client(secret).await?;
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await?;
let mut buf = [0u8; 11];
stream.read_exact(&mut buf).await?;
assert_eq!(&buf, b"hello world");
stream.write_all(b"I can send a message too!").await?;
anyhow::Ok(())
});
let mut stream = TcpStream::connect(addr).await?;
stream.write_all(b"hello world").await?;
let mut buf = [0u8; 25];
stream.read_exact(&mut buf).await?;
assert_eq!(&buf, b"I can send a message too!");
// Ensure that the client end of the stream is closed now.
assert_eq!(stream.read(&mut buf).await?, 0);
// Also ensure that additional connections do not produce any data.
let mut stream = TcpStream::connect(addr).await?;
assert_eq!(stream.read(&mut buf).await?, 0);
Ok(())
}
#[rstest]
#[case(None, Some("my secret"))]
#[case(Some("my secret"), None)]
#[tokio::test]
async fn mismatched_secret(
#[case] server_secret: Option<&str>,
#[case] client_secret: Option<&str>,
) {
let _guard = SERIAL_GUARD.lock().await;
spawn_server(server_secret).await;
assert!(spawn_client(client_secret).await.is_err());
}
#[tokio::test]
async fn invalid_address() -> Result<()> {
// We don't need the serial guard for this test because it doesn't create a server.
async fn check_address(to: &str, use_secret: bool) -> Result<()> {
match Client::new("localhost", 5000, to, 0, use_secret.then_some("a secret")).await {
Ok(_) => Err(anyhow!("expected error for {to}, use_secret={use_secret}")),
Err(_) => Ok(()),
}
}
tokio::try_join!(
check_address("google.com", false),
check_address("google.com", true),
check_address("nonexistent.domain.for.demonstration", false),
check_address("nonexistent.domain.for.demonstration", true),
check_address("malformed !$uri$%", false),
check_address("malformed !$uri$%", true),
)?;
Ok(())
}
#[tokio::test]
async fn very_long_frame() -> Result<()> {
let _guard = SERIAL_GUARD.lock().await;
spawn_server(None).await;
let mut attacker = TcpStream::connect(("localhost", CONTROL_PORT)).await?;
// Slowly send a very long frame.
for _ in 0..10 {
let result = attacker.write_all(&[42u8; 100000]).await;
if result.is_err() {
return Ok(());
}
time::sleep(Duration::from_millis(10)).await;
}
panic!("did not exit after a 1 MB frame");
}
#[test]
#[should_panic]
fn empty_port_range() {
let min_port = 5000;
let max_port = 3000;
let _ = Server::new(min_port..=max_port, None);
}
#[tokio::test]
async fn half_closed_tcp_stream() -> Result<()> {
// Check that "half-closed" TCP streams will not result in spontaneous hangups.
let _guard = SERIAL_GUARD.lock().await;
spawn_server(None).await;
let (listener, addr) = spawn_client(None).await?;
let (mut cli, (mut srv, _)) = tokio::try_join!(TcpStream::connect(addr), listener.accept())?;
// Send data before half-closing one of the streams.
let mut buf = b"message before shutdown".to_vec();
cli.write_all(&buf).await?;
// Only close the write half of the stream. This is a half-closed stream. In the
// TCP protocol, it is represented as a FIN packet on one end. The entire stream
// is only closed after two FINs are exchanged and ACKed by the other end.
cli.shutdown().await?;
srv.read_exact(&mut buf).await?;
assert_eq!(buf, b"message before shutdown");
assert_eq!(srv.read(&mut buf).await?, 0); // EOF
// Now make sure that the other stream can still send data, despite
// half-shutdown on client->server side.
let mut buf = b"hello from the other side!".to_vec();
srv.write_all(&buf).await?;
cli.read_exact(&mut buf).await?;
assert_eq!(buf, b"hello from the other side!");
// We don't have to think about CLOSE_RD handling because that's not really
// part of the TCP protocol, just the POSIX streams API. It is implemented by
// the OS ignoring future packets received on that stream.
Ok(())
}
gitextract_o1_hc2gb/
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── ci.yml
│ ├── docker.yml
│ ├── mean_bean_ci.yml
│ └── mean_bean_deploy.yml
├── .gitignore
├── Cargo.toml
├── Dockerfile
├── LICENSE
├── README.md
├── ci/
│ ├── build.bash
│ ├── common.bash
│ ├── set_rust_version.bash
│ └── test.bash
├── src/
│ ├── auth.rs
│ ├── client.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── server.rs
│ └── shared.rs
└── tests/
├── auth_test.rs
└── e2e_test.rs
SYMBOL INDEX (44 symbols across 7 files)
FILE: src/auth.rs
type Authenticator (line 12) | pub struct Authenticator(Hmac<Sha256>);
method new (line 16) | pub fn new(secret: &str) -> Self {
method answer (line 22) | pub fn answer(&self, challenge: &Uuid) -> String {
method validate (line 40) | pub fn validate(&self, challenge: &Uuid, tag: &str) -> bool {
method server_handshake (line 51) | pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(
method client_handshake (line 67) | pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(
FILE: src/client.rs
type Client (line 14) | pub struct Client {
method new (line 36) | pub async fn new(
method remote_port (line 73) | pub fn remote_port(&self) -> u16 {
method listen (line 78) | pub async fn listen(mut self) -> Result<()> {
method handle_connection (line 105) | async fn handle_connection(&self, id: Uuid) -> Result<()> {
function connect_with_timeout (line 121) | async fn connect_with_timeout(to: &str, port: u16) -> Result<TcpStream> {
FILE: src/main.rs
type Args (line 9) | struct Args {
type Command (line 15) | enum Command {
function run (line 64) | async fn run(command: Command) -> Result<()> {
function main (line 99) | fn main() -> Result<()> {
FILE: src/server.rs
type Server (line 18) | pub struct Server {
method new (line 37) | pub fn new(port_range: RangeInclusive<u16>, secret: Option<&str>) -> S...
method set_bind_addr (line 49) | pub fn set_bind_addr(&mut self, bind_addr: IpAddr) {
method set_bind_tunnels (line 54) | pub fn set_bind_tunnels(&mut self, bind_tunnels: IpAddr) {
method listen (line 59) | pub async fn listen(self) -> Result<()> {
method create_listener (line 81) | async fn create_listener(&self, port: u16) -> Result<TcpListener, &'st...
method handle_connection (line 118) | async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
FILE: src/shared.rs
constant CONTROL_PORT (line 15) | pub const CONTROL_PORT: u16 = 7835;
constant MAX_FRAME_LENGTH (line 18) | pub const MAX_FRAME_LENGTH: usize = 256;
constant NETWORK_TIMEOUT (line 21) | pub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3);
type ClientMessage (line 25) | pub enum ClientMessage {
type ServerMessage (line 38) | pub enum ServerMessage {
type Delimited (line 56) | pub struct Delimited<U>(Framed<U, AnyDelimiterCodec>);
function new (line 60) | pub fn new(stream: U) -> Self {
function recv (line 66) | pub async fn recv<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
function recv_timeout (line 82) | pub async fn recv_timeout<T: DeserializeOwned>(&mut self) -> Result<Opti...
function send (line 89) | pub async fn send<T: Serialize>(&mut self, msg: T) -> Result<()> {
function into_parts (line 96) | pub fn into_parts(self) -> FramedParts<U, AnyDelimiterCodec> {
FILE: tests/auth_test.rs
function auth_handshake (line 6) | async fn auth_handshake() -> Result<()> {
function auth_handshake_fail (line 22) | async fn auth_handshake_fail() {
FILE: tests/e2e_test.rs
function spawn_server (line 19) | async fn spawn_server(secret: Option<&str>) {
function spawn_client (line 25) | async fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, Sock...
function basic_proxy (line 36) | async fn basic_proxy(#[values(None, Some(""), Some("abc"))] secret: Opti...
function mismatched_secret (line 73) | async fn mismatched_secret(
function invalid_address (line 84) | async fn invalid_address() -> Result<()> {
function very_long_frame (line 104) | async fn very_long_frame() -> Result<()> {
function empty_port_range (line 123) | fn empty_port_range() {
function half_closed_tcp_stream (line 130) | async fn half_closed_tcp_stream() -> Result<()> {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
{
"path": ".dockerignore",
"chars": 8,
"preview": "/target\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 18,
"preview": "github: [ekzhang]\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 905,
"preview": "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: ub"
},
{
"path": ".github/workflows/docker.yml",
"chars": 1183,
"preview": "name: Docker\n\non:\n push:\n tags:\n - \"v*.*.*\"\n\njobs:\n build_deploy:\n name: Build and Deploy\n runs-on: ubun"
},
{
"path": ".github/workflows/mean_bean_ci.yml",
"chars": 3782,
"preview": "name: Mean Bean CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n install-cross:\n runs-on: ubuntu-l"
},
{
"path": ".github/workflows/mean_bean_deploy.yml",
"chars": 6116,
"preview": "on:\n push:\n # # Sequence of patterns matched against refs/tags\n tags:\n - \"v*\" # Push events to matching v*, "
},
{
"path": ".gitignore",
"chars": 8,
"preview": "/target\n"
},
{
"path": "Cargo.toml",
"chars": 1278,
"preview": "[package]\nname = \"bore-cli\"\nversion = \"0.6.0\"\nauthors = [\"Eric Zhang <ekzhang1@gmail.com>\"]\nlicense = \"MIT\"\ndescription "
},
{
"path": "Dockerfile",
"chars": 218,
"preview": "FROM rust:alpine AS builder\nWORKDIR /home/rust/src\nRUN apk --no-cache add musl-dev\nCOPY . .\nRUN cargo install --path .\n\n"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2022 Eric Zhang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 7114,
"preview": "# bore\n\n[](https://github.com/"
},
{
"path": "ci/build.bash",
"chars": 571,
"preview": "#!/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"
},
{
"path": "ci/common.bash",
"chars": 110,
"preview": "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",
"chars": 66,
"preview": "#!/usr/bin/env bash\nset -e\nrustup default $1\nrustup target add $2\n"
},
{
"path": "ci/test.bash",
"chars": 643,
"preview": "#!/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"
},
{
"path": "src/auth.rs",
"chars": 2800,
"preview": "//! Auth implementation for bore client and server.\n\nuse anyhow::{bail, ensure, Result};\nuse hmac::{Hmac, Mac};\nuse sha2"
},
{
"path": "src/client.rs",
"chars": 4602,
"preview": "//! Client implementation for the `bore` service.\n\nuse std::sync::Arc;\n\nuse anyhow::{bail, Context, Result};\nuse tokio::"
},
{
"path": "src/lib.rs",
"chars": 646,
"preview": "//! A modern, simple TCP tunnel in Rust that exposes local ports to a remote\n//! server, bypassing standard NAT connecti"
},
{
"path": "src/main.rs",
"chars": 2950,
"preview": "use std::net::IpAddr;\n\nuse anyhow::Result;\nuse bore_cli::{client::Client, server::Server};\nuse clap::{error::ErrorKind, "
},
{
"path": "src/server.rs",
"chars": 7465,
"preview": "//! Server implementation for the `bore` service.\n\nuse std::net::{IpAddr, Ipv4Addr};\nuse std::{io, ops::RangeInclusive, "
},
{
"path": "src/shared.rs",
"chars": 3517,
"preview": "//! Shared data structures, utilities, and protocol definitions.\n\nuse std::time::Duration;\n\nuse anyhow::{Context, Result"
},
{
"path": "tests/auth_test.rs",
"chars": 1031,
"preview": "use anyhow::Result;\nuse bore_cli::{auth::Authenticator, shared::Delimited};\nuse tokio::io::{self};\n\n#[tokio::test]\nasync"
},
{
"path": "tests/e2e_test.rs",
"chars": 5620,
"preview": "use std::net::SocketAddr;\nuse std::time::Duration;\n\nuse anyhow::{anyhow, Result};\nuse bore_cli::{client::Client, server:"
}
]
About this extraction
This page contains the full source code of the ekzhang/bore GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (50.5 KB), approximately 13.0k tokens, and a symbol index with 44 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.