Skip to content

Commit

Permalink
Use random ports when port number is 0 (#79)
Browse files Browse the repository at this point in the history
* Use random ports when port number is 0

* Add support for --max-port CLI option

* Fix typo

* Fix another typo

* Update README
  • Loading branch information
ekzhang authored Apr 28, 2023
1 parent 931f2aa commit 0860c6e
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 39 deletions.
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ path = "src/main.rs"
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"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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.

(`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

Expand Down Expand Up @@ -93,7 +93,8 @@ Runs the remote proxy server
Usage: bore server [OPTIONS]
Options:
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024]
--min-port <MIN_PORT> Minimum accepted TCP port number [default: 1024]
--max-port <MAX_PORT> Maximum accepted TCP port number [default: 65535]
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
-h, --help Print help information
```
Expand Down
4 changes: 1 addition & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
use std::sync::Arc;

use anyhow::{bail, Context, Result};

use tokio::io::AsyncWriteExt;
use tokio::{net::TcpStream, time::timeout};
use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};
use tracing::{error, info, info_span, warn, Instrument};
use uuid::Uuid;

Expand Down
22 changes: 18 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::Result;
use bore_cli::{client::Client, server::Server};
use clap::{Parser, Subcommand};
use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};

#[derive(Parser, Debug)]
#[clap(author, version, about)]
Expand Down Expand Up @@ -35,10 +35,14 @@ enum Command {

/// Runs the remote proxy server.
Server {
/// Minimum TCP port number to accept.
/// Minimum accepted TCP port number.
#[clap(long, default_value_t = 1024)]
min_port: u16,

/// Maximum accepted TCP port number.
#[clap(long, default_value_t = 65535)]
max_port: u16,

/// Optional secret for authentication.
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
secret: Option<String>,
Expand All @@ -58,8 +62,18 @@ async fn run(command: Command) -> Result<()> {
let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?;
client.listen().await?;
}
Command::Server { min_port, secret } => {
Server::new(min_port, secret.as_deref()).listen().await?;
Command::Server {
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?;
}
}

Expand Down
76 changes: 48 additions & 28 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! Server implementation for the `bore` service.
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use std::{io, net::SocketAddr, ops::RangeInclusive, sync::Arc, time::Duration};

use anyhow::Result;
use dashmap::DashMap;
Expand All @@ -17,8 +15,8 @@ use crate::shared::{proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT

/// State structure for the server.
pub struct Server {
/// The minimum TCP port that can be forwarded.
min_port: u16,
/// Range of TCP ports that can be forwarded.
port_range: RangeInclusive<u16>,

/// Optional secret used to authenticate clients.
auth: Option<Authenticator>,
Expand All @@ -29,9 +27,10 @@ pub struct Server {

impl Server {
/// 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 {
min_port,
port_range,
conns: Arc::new(DashMap::new()),
auth: secret.map(Authenticator::new),
}
Expand Down Expand Up @@ -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<()> {
let mut stream = Delimited::new(stream);
if let Some(auth) = &self.auth {
Expand All @@ -77,22 +113,15 @@ impl Server {
Ok(())
}
Some(ClientMessage::Hello(port)) => {
if port != 0 && port < self.min_port {
warn!(?port, "client port number too low");
return Ok(());
}
info!(?port, "new client");
let listener = match TcpListener::bind(("0.0.0.0", port)).await {
let listener = match self.create_listener(port).await {
Ok(listener) => listener,
Err(_) => {
warn!(?port, "could not bind to local port");
stream
.send(ServerMessage::Error("port already in use".into()))
.await?;
Err(err) => {
stream.send(ServerMessage::Error(err.into())).await?;
return Ok(());
}
};
let port = listener.local_addr()?.port();
info!(?port, "new client");
stream.send(ServerMessage::Hello(port)).await?;

loop {
Expand Down Expand Up @@ -133,16 +162,7 @@ impl Server {
}
Ok(())
}
None => {
warn!("unexpected EOF");
Ok(())
}
None => Ok(()),
}
}
}

impl Default for Server {
fn default() -> Self {
Server::new(1024, None)
}
}
1 change: 0 additions & 1 deletion src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use futures_util::{SinkExt, StreamExt};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use tokio::io::{self, AsyncRead, AsyncWrite};

use tokio::time::timeout;
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
use tracing::trace;
Expand Down
10 changes: 9 additions & 1 deletion tests/e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ lazy_static! {

/// 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, secret).listen());
tokio::spawn(Server::new(1024..=65535, secret).listen());
time::sleep(Duration::from_millis(50)).await;
}

Expand Down Expand Up @@ -117,3 +117,11 @@ async fn very_long_frame() -> Result<()> {
}
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);
}

0 comments on commit 0860c6e

Please sign in to comment.