16 Commits

Author SHA1 Message Date
Eric Zhang
a6045fb1a7 Bump version to 0.5.1 (#119)
* Update dependencies

* Bump version to 0.5.1

* Try to deflake tests in GitHub Actions

* Add some retries to e2e_test

* Revert cargo.lock changes

* Update only a couple deps

* Fix CI running twice :(

* Fix typo
2024-06-10 21:50:23 -04:00
Eric Zhang
82acea3477 Fix CI runners (#118)
macos-latest has been switched to use arm64 mac instances
2024-06-10 21:04:14 -04:00
kennycallado
1f81f01fe2 arm64 docker image (#114)
* Update docker.yml

Testing aarch64 build works

* Update docker.yml

Revert event to trigger the action

* Update docker.yml

reassign proper images name
2024-06-10 20:54:40 -04:00
Eric Zhang
32a7233c9d Fix clippy lint on recent Rust versions 2023-07-27 11:26:01 -04:00
Kian-Meng Ang
3ae14209a4 Fix typo, Maxmium -> Maximum (#81)
Found via `codespell -L crate`
2023-04-29 11:08:35 -04:00
Eric Zhang
f8ccbae378 Update Homebrew installation instructions (#80)
Thanks @Moulick for making the Homebrew formula and contributing the
package to Homebrew's core tap!
2023-04-28 09:57:45 -04:00
Eric Zhang
6512ca4770 chore: Release bore-cli version 0.5.0 2023-04-27 22:02:18 -04:00
Eric Zhang
d630b47d38 Update dependencies 2023-04-27 21:55:44 -04:00
Eric Zhang
0860c6e018 Use random ports when port number is 0 (#79)
* Use random ports when port number is 0

* Add support for --max-port CLI option

* Fix typo

* Fix another typo

* Update README
2023-04-27 21:48:47 -04:00
Eric Zhang
931f2aa20b Fix badge in README
See <https://github.com/badges/shields/issues/8671>.
2023-01-01 15:06:59 -06:00
Eric Zhang
b65481abb0 Update docker deploy, turn off arm64 for now 2022-11-11 01:42:04 -05:00
Eric Zhang
3bf7a665c1 (cargo-release) version 0.4.1 2022-11-11 01:14:42 -05:00
Eric Zhang
d3d5d434ad Update dependencies, including clap v4
Also fix Clippy lints for rustc 1.65 along the way.
2022-11-11 01:08:50 -05:00
calfzhou
f1ed0b2ecb Support reading remote server address from an environment variable (#45) (#46) 2022-11-11 00:52:58 -05:00
Eric Zhang
664723cba5 Create FUNDING.yml 2022-06-26 18:01:08 -04:00
Eric Zhang
fd83d4a207 Add Homebrew installation instructions 2022-06-05 13:00:16 -04:00
13 changed files with 634 additions and 359 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [ekzhang]

View File

@@ -1,6 +1,10 @@
name: CI name: CI
on: [push, pull_request] on:
push:
branches:
- main
pull_request:
jobs: jobs:
rust: rust:

View File

@@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: ekzhang/bore images: ekzhang/bore
tags: | tags: |
@@ -23,22 +23,22 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
with: with:
platforms: arm64 platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true

View File

@@ -1,6 +1,10 @@
name: Mean Bean CI name: Mean Bean CI
on: [push, pull_request] on:
push:
branches:
- main
pull_request:
jobs: jobs:
install-cross: install-cross:
@@ -33,7 +37,7 @@ jobs:
matrix: matrix:
channel: [stable] channel: [stable]
target: target:
- x86_64-apple-darwin - aarch64-apple-darwin
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout

759
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bore-cli" name = "bore-cli"
version = "0.4.0" version = "0.5.1"
authors = ["Eric Zhang <ekzhang1@gmail.com>"] authors = ["Eric Zhang <ekzhang1@gmail.com>"]
license = "MIT" license = "MIT"
description = "A modern, simple TCP tunnel in Rust that exposes local ports to a remote server, bypassing standard NAT connection firewalls." description = "A modern, simple TCP tunnel in Rust that exposes local ports to a remote server, bypassing standard NAT connection firewalls."
@@ -17,8 +17,9 @@ path = "src/main.rs"
[dependencies] [dependencies]
anyhow = { version = "1.0.56", features = ["backtrace"] } anyhow = { version = "1.0.56", features = ["backtrace"] }
clap = { version = "3.1.8", features = ["derive", "env"] } clap = { version = "4.0.22", features = ["derive", "env"] }
dashmap = "5.2.0" dashmap = "5.2.0"
fastrand = "1.9.0"
futures-util = { version = "0.3.21", features = ["sink"] } futures-util = { version = "0.3.21", features = ["sink"] }
hex = "0.4.3" hex = "0.4.3"
hmac = "0.12.1" hmac = "0.12.1"
@@ -29,9 +30,9 @@ tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros"
tokio-util = { version = "0.7.1", features = ["codec"] } tokio-util = { version = "0.7.1", features = ["codec"] }
tracing = "0.1.32" tracing = "0.1.32"
tracing-subscriber = "0.3.10" tracing-subscriber = "0.3.10"
uuid = { version = "0.8.2", features = ["serde", "v4"] } uuid = { version = "1.2.1", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0"
rstest = "0.12.0" rstest = "0.15.0"
tokio = { version = "1.17.0", features = ["sync"] } tokio = { version = "1.17.0", features = ["sync"] }

View File

@@ -1,6 +1,6 @@
# bore # bore
[![Build status](https://img.shields.io/github/workflow/status/ekzhang/bore/CI)](https://github.com/ekzhang/bore/actions) [![Build status](https://img.shields.io/github/actions/workflow/status/ekzhang/bore/ci.yml)](https://github.com/ekzhang/bore/actions)
[![Crates.io](https://img.shields.io/crates/v/bore-cli.svg)](https://crates.io/crates/bore-cli) [![Crates.io](https://img.shields.io/crates/v/bore-cli.svg)](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.** 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.**
@@ -19,11 +19,17 @@ This will expose your local port at `localhost:8000` to the public internet at `
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. 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 less than 400 lines of safe, async Rust code and is trivial to set up — just run a single binary for the client and server.) (`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 ## Installation
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. If you're on macOS, `bore` is packaged as a Homebrew core formula.
```shell
brew install bore-cli
```
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.
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. 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.
@@ -31,7 +37,7 @@ You also can build `bore` from source using [Cargo](https://doc.rust-lang.org/ca
cargo install bore-cli cargo install bore-cli
``` ```
We also publish versioned Docker images for each release. Each image is built for AMD 64-bit and Arm 64-bit architectures. They're tagged with the specific version and allow you to run the statically-linked `bore` binary from a minimal "scratch" container. 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 ```shell
docker run -it --init --rm --network host ekzhang/bore <ARGS> docker run -it --init --rm --network host ekzhang/bore <ARGS>
@@ -54,22 +60,19 @@ You can optionally pass in a `--port` option to pick a specific port on the remo
The full options are shown below. The full options are shown below.
```shell ```shell
bore-local 0.4.0
Starts a local proxy to the remote server Starts a local proxy to the remote server
USAGE: Usage: bore local [OPTIONS] --to <TO> <LOCAL_PORT>
bore local [OPTIONS] --to <TO> <LOCAL_PORT>
ARGS: Arguments:
<LOCAL_PORT> The local port to expose <LOCAL_PORT> The local port to expose
OPTIONS: Options:
-h, --help Print help information -l, --local-host <HOST> The local host to expose [default: localhost]
-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] -p, --port <PORT> Optional port on the remote server to select [default: 0]
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET] -s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
-t, --to <TO> Address of the remote server to expose local ports to -h, --help Print help information
-V, --version Print version information
``` ```
### Self-Hosting ### Self-Hosting
@@ -85,17 +88,15 @@ That's all it takes! After the server starts running at a given address, you can
The full options for the `bore server` command are shown below. The full options for the `bore server` command are shown below.
```shell ```shell
bore-server 0.4.0
Runs the remote proxy server Runs the remote proxy server
USAGE: Usage: bore server [OPTIONS]
bore server [OPTIONS]
OPTIONS: Options:
-h, --help Print help information --min-port <MIN_PORT> Minimum accepted TCP port number [default: 1024]
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024] --max-port <MAX_PORT> Maximum accepted TCP port number [default: 65535]
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET] -s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
-V, --version Print version information -h, --help Print help information
``` ```
## Protocol ## Protocol

View File

@@ -12,5 +12,21 @@ TARGET_TRIPLE=$2
required_arg $CROSS 'CROSS' required_arg $CROSS 'CROSS'
required_arg $TARGET_TRIPLE '<Target Triple>' required_arg $TARGET_TRIPLE '<Target Triple>'
$CROSS test --target $TARGET_TRIPLE max_attempts=3
$CROSS test --target $TARGET_TRIPLE --all-features 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

View File

@@ -3,9 +3,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};
use tokio::io::AsyncWriteExt;
use tokio::{net::TcpStream, time::timeout};
use tracing::{error, info, info_span, warn, Instrument}; use tracing::{error, info, info_span, warn, Instrument};
use uuid::Uuid; use uuid::Uuid;

View File

@@ -1,10 +1,9 @@
use anyhow::Result; use anyhow::Result;
use bore_cli::{client::Client, server::Server}; use bore_cli::{client::Client, server::Server};
use clap::{Parser, Subcommand}; use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(author, version, about)] #[clap(author, version, about)]
#[clap(propagate_version = true)]
struct Args { struct Args {
#[clap(subcommand)] #[clap(subcommand)]
command: Command, command: Command,
@@ -22,7 +21,7 @@ enum Command {
local_host: String, local_host: String,
/// Address of the remote server to expose local ports to. /// Address of the remote server to expose local ports to.
#[clap(short, long)] #[clap(short, long, env = "BORE_SERVER")]
to: String, to: String,
/// Optional port on the remote server to select. /// Optional port on the remote server to select.
@@ -36,10 +35,14 @@ enum Command {
/// Runs the remote proxy server. /// Runs the remote proxy server.
Server { Server {
/// Minimum TCP port number to accept. /// Minimum accepted TCP port number.
#[clap(long, default_value_t = 1024)] #[clap(long, default_value_t = 1024)]
min_port: u16, min_port: u16,
/// Maximum accepted TCP port number.
#[clap(long, default_value_t = 65535)]
max_port: u16,
/// Optional secret for authentication. /// Optional secret for authentication.
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)] #[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
secret: Option<String>, secret: Option<String>,
@@ -59,8 +62,18 @@ async fn run(command: Command) -> Result<()> {
let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?; let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?;
client.listen().await?; client.listen().await?;
} }
Command::Server { min_port, secret } => { Command::Server {
Server::new(min_port, secret.as_deref()).listen().await?; min_port,
max_port,
secret,
} => {
let port_range = min_port..=max_port;
if port_range.is_empty() {
Args::command()
.error(ErrorKind::InvalidValue, "port range is empty")
.exit();
}
Server::new(port_range, secret.as_deref()).listen().await?;
} }
} }

View File

@@ -1,8 +1,6 @@
//! Server implementation for the `bore` service. //! Server implementation for the `bore` service.
use std::net::SocketAddr; use std::{io, net::SocketAddr, ops::RangeInclusive, sync::Arc, time::Duration};
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
@@ -17,8 +15,8 @@ use crate::shared::{proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT
/// State structure for the server. /// State structure for the server.
pub struct Server { pub struct Server {
/// The minimum TCP port that can be forwarded. /// Range of TCP ports that can be forwarded.
min_port: u16, port_range: RangeInclusive<u16>,
/// Optional secret used to authenticate clients. /// Optional secret used to authenticate clients.
auth: Option<Authenticator>, auth: Option<Authenticator>,
@@ -29,9 +27,10 @@ pub struct Server {
impl Server { impl Server {
/// Create a new server with a specified minimum port number. /// Create a new server with a specified minimum port number.
pub fn new(min_port: u16, secret: Option<&str>) -> Self { pub fn new(port_range: RangeInclusive<u16>, secret: Option<&str>) -> Self {
assert!(!port_range.is_empty(), "must provide at least one port");
Server { Server {
min_port, port_range,
conns: Arc::new(DashMap::new()), conns: Arc::new(DashMap::new()),
auth: secret.map(Authenticator::new), auth: secret.map(Authenticator::new),
} }
@@ -61,6 +60,43 @@ impl Server {
} }
} }
async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> {
let try_bind = |port: u16| async move {
TcpListener::bind(("0.0.0.0", 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<()> { async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
let mut stream = Delimited::new(stream); let mut stream = Delimited::new(stream);
if let Some(auth) = &self.auth { if let Some(auth) = &self.auth {
@@ -77,22 +113,15 @@ impl Server {
Ok(()) Ok(())
} }
Some(ClientMessage::Hello(port)) => { Some(ClientMessage::Hello(port)) => {
if port != 0 && port < self.min_port { let listener = match self.create_listener(port).await {
warn!(?port, "client port number too low");
return Ok(());
}
info!(?port, "new client");
let listener = match TcpListener::bind(("0.0.0.0", port)).await {
Ok(listener) => listener, Ok(listener) => listener,
Err(_) => { Err(err) => {
warn!(?port, "could not bind to local port"); stream.send(ServerMessage::Error(err.into())).await?;
stream
.send(ServerMessage::Error("port already in use".into()))
.await?;
return Ok(()); return Ok(());
} }
}; };
let port = listener.local_addr()?.port(); let port = listener.local_addr()?.port();
info!(?port, "new client");
stream.send(ServerMessage::Hello(port)).await?; stream.send(ServerMessage::Hello(port)).await?;
loop { loop {
@@ -133,16 +162,7 @@ impl Server {
} }
Ok(()) Ok(())
} }
None => { None => Ok(()),
warn!("unexpected EOF");
Ok(())
}
} }
} }
} }
impl Default for Server {
fn default() -> Self {
Server::new(1024, None)
}
}

View File

@@ -4,10 +4,8 @@ use std::time::Duration;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use serde::de::DeserializeOwned; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use tokio::io::{self, AsyncRead, AsyncWrite}; use tokio::io::{self, AsyncRead, AsyncWrite};
use tokio::time::timeout; use tokio::time::timeout;
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts}; use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
use tracing::trace; use tracing::trace;
@@ -16,7 +14,7 @@ use uuid::Uuid;
/// TCP port used for control connections with the server. /// TCP port used for control connections with the server.
pub const CONTROL_PORT: u16 = 7835; pub const CONTROL_PORT: u16 = 7835;
/// Maxmium byte length for a JSON frame in the stream. /// Maximum byte length for a JSON frame in the stream.
pub const MAX_FRAME_LENGTH: usize = 256; pub const MAX_FRAME_LENGTH: usize = 256;
/// Timeout for network connections and initial protocol messages. /// Timeout for network connections and initial protocol messages.
@@ -69,8 +67,8 @@ impl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {
trace!("waiting to receive json message"); trace!("waiting to receive json message");
if let Some(next_message) = self.0.next().await { if let Some(next_message) = self.0.next().await {
let byte_message = next_message.context("frame error, invalid byte length")?; let byte_message = next_message.context("frame error, invalid byte length")?;
let serialized_obj = serde_json::from_slice(&byte_message.to_vec()) let serialized_obj =
.context("unable to parse message")?; serde_json::from_slice(&byte_message).context("unable to parse message")?;
Ok(serialized_obj) Ok(serialized_obj)
} else { } else {
Ok(None) Ok(None)

View File

@@ -1,3 +1,5 @@
#![allow(clippy::items_after_test_module)]
use std::net::SocketAddr; use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
@@ -17,7 +19,7 @@ lazy_static! {
/// Spawn the server, giving some time for the control port TcpListener to start. /// Spawn the server, giving some time for the control port TcpListener to start.
async fn spawn_server(secret: Option<&str>) { async fn spawn_server(secret: Option<&str>) {
tokio::spawn(Server::new(1024, secret).listen()); tokio::spawn(Server::new(1024..=65535, secret).listen());
time::sleep(Duration::from_millis(50)).await; time::sleep(Duration::from_millis(50)).await;
} }
@@ -84,7 +86,7 @@ async fn mismatched_secret(
async fn invalid_address() -> Result<()> { async fn invalid_address() -> Result<()> {
// We don't need the serial guard for this test because it doesn't create a server. // 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<()> { async fn check_address(to: &str, use_secret: bool) -> Result<()> {
match Client::new("localhost", 5000, to, 0, use_secret.then(|| "a secret")).await { 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}")), Ok(_) => Err(anyhow!("expected error for {to}, use_secret={use_secret}")),
Err(_) => Ok(()), Err(_) => Ok(()),
} }
@@ -117,3 +119,11 @@ async fn very_long_frame() -> Result<()> {
} }
panic!("did not exit after a 1 MB frame"); 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);
}