mirror of https://github.com/ekzhang/bore.git
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
This commit is contained in:
parent
931f2aa20b
commit
0860c6e018
|
@ -80,6 +80,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"fastrand",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
@ -193,6 +194,15 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.25"
|
version = "0.3.25"
|
||||||
|
@ -300,6 +310,15 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
|
@ -19,6 +19,7 @@ path = "src/main.rs"
|
||||||
anyhow = { version = "1.0.56", features = ["backtrace"] }
|
anyhow = { version = "1.0.56", features = ["backtrace"] }
|
||||||
clap = { version = "4.0.22", 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"
|
||||||
|
|
|
@ -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.
|
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
|
||||||
|
|
||||||
|
@ -93,7 +93,8 @@ Runs the remote proxy server
|
||||||
Usage: bore server [OPTIONS]
|
Usage: bore server [OPTIONS]
|
||||||
|
|
||||||
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]
|
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
```
|
```
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -1,6 +1,6 @@
|
||||||
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)]
|
||||||
|
@ -35,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>,
|
||||||
|
@ -58,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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ use futures_util::{SinkExt, StreamExt};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{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;
|
||||||
|
|
|
@ -17,7 +17,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,3 +117,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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue