Skip to content

Commit 0860c6e

Browse files
authored
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
1 parent 931f2aa commit 0860c6e

File tree

8 files changed

+99
-39
lines changed

8 files changed

+99
-39
lines changed

Cargo.lock

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ path = "src/main.rs"
1919
anyhow = { version = "1.0.56", features = ["backtrace"] }
2020
clap = { version = "4.0.22", features = ["derive", "env"] }
2121
dashmap = "5.2.0"
22+
fastrand = "1.9.0"
2223
futures-util = { version = "0.3.21", features = ["sink"] }
2324
hex = "0.4.3"
2425
hmac = "0.12.1"

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This will expose your local port at `localhost:8000` to the public internet at `
1919

2020
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.
2121

22-
(`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.)
22+
(`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.)
2323

2424
## Installation
2525

@@ -93,7 +93,8 @@ Runs the remote proxy server
9393
Usage: bore server [OPTIONS]
9494
9595
Options:
96-
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024]
96+
--min-port <MIN_PORT> Minimum accepted TCP port number [default: 1024]
97+
--max-port <MAX_PORT> Maximum accepted TCP port number [default: 65535]
9798
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
9899
-h, --help Print help information
99100
```

src/client.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
use std::sync::Arc;
44

55
use anyhow::{bail, Context, Result};
6-
7-
use tokio::io::AsyncWriteExt;
8-
use tokio::{net::TcpStream, time::timeout};
6+
use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};
97
use tracing::{error, info, info_span, warn, Instrument};
108
use uuid::Uuid;
119

src/main.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Result;
22
use bore_cli::{client::Client, server::Server};
3-
use clap::{Parser, Subcommand};
3+
use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
44

55
#[derive(Parser, Debug)]
66
#[clap(author, version, about)]
@@ -35,10 +35,14 @@ enum Command {
3535

3636
/// Runs the remote proxy server.
3737
Server {
38-
/// Minimum TCP port number to accept.
38+
/// Minimum accepted TCP port number.
3939
#[clap(long, default_value_t = 1024)]
4040
min_port: u16,
4141

42+
/// Maximum accepted TCP port number.
43+
#[clap(long, default_value_t = 65535)]
44+
max_port: u16,
45+
4246
/// Optional secret for authentication.
4347
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
4448
secret: Option<String>,
@@ -58,8 +62,18 @@ async fn run(command: Command) -> Result<()> {
5862
let client = Client::new(&local_host, local_port, &to, port, secret.as_deref()).await?;
5963
client.listen().await?;
6064
}
61-
Command::Server { min_port, secret } => {
62-
Server::new(min_port, secret.as_deref()).listen().await?;
65+
Command::Server {
66+
min_port,
67+
max_port,
68+
secret,
69+
} => {
70+
let port_range = min_port..=max_port;
71+
if port_range.is_empty() {
72+
Args::command()
73+
.error(ErrorKind::InvalidValue, "port range is empty")
74+
.exit();
75+
}
76+
Server::new(port_range, secret.as_deref()).listen().await?;
6377
}
6478
}
6579

src/server.rs

+48-28
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Server implementation for the `bore` service.
22
3-
use std::net::SocketAddr;
4-
use std::sync::Arc;
5-
use std::time::Duration;
3+
use std::{io, net::SocketAddr, ops::RangeInclusive, sync::Arc, time::Duration};
64

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

1816
/// State structure for the server.
1917
pub struct Server {
20-
/// The minimum TCP port that can be forwarded.
21-
min_port: u16,
18+
/// Range of TCP ports that can be forwarded.
19+
port_range: RangeInclusive<u16>,
2220

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

3028
impl Server {
3129
/// Create a new server with a specified minimum port number.
32-
pub fn new(min_port: u16, secret: Option<&str>) -> Self {
30+
pub fn new(port_range: RangeInclusive<u16>, secret: Option<&str>) -> Self {
31+
assert!(!port_range.is_empty(), "must provide at least one port");
3332
Server {
34-
min_port,
33+
port_range,
3534
conns: Arc::new(DashMap::new()),
3635
auth: secret.map(Authenticator::new),
3736
}
@@ -61,6 +60,43 @@ impl Server {
6160
}
6261
}
6362

63+
async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> {
64+
let try_bind = |port: u16| async move {
65+
TcpListener::bind(("0.0.0.0", port))
66+
.await
67+
.map_err(|err| match err.kind() {
68+
io::ErrorKind::AddrInUse => "port already in use",
69+
io::ErrorKind::PermissionDenied => "permission denied",
70+
_ => "failed to bind to port",
71+
})
72+
};
73+
if port > 0 {
74+
// Client requests a specific port number.
75+
if !self.port_range.contains(&port) {
76+
return Err("client port number not in allowed range");
77+
}
78+
try_bind(port).await
79+
} else {
80+
// Client requests any available port in range.
81+
//
82+
// In this case, we bind to 150 random port numbers. We choose this value because in
83+
// order to find a free port with probability at least 1-δ, when ε proportion of the
84+
// ports are currently available, it suffices to check approximately -2 ln(δ) / ε
85+
// independently and uniformly chosen ports (up to a second-order term in ε).
86+
//
87+
// Checking 150 times gives us 99.999% success at utilizing 85% of ports under these
88+
// conditions, when ε=0.15 and δ=0.00001.
89+
for _ in 0..150 {
90+
let port = fastrand::u16(self.port_range.clone());
91+
match try_bind(port).await {
92+
Ok(listener) => return Ok(listener),
93+
Err(_) => continue,
94+
}
95+
}
96+
Err("failed to find an available port")
97+
}
98+
}
99+
64100
async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
65101
let mut stream = Delimited::new(stream);
66102
if let Some(auth) = &self.auth {
@@ -77,22 +113,15 @@ impl Server {
77113
Ok(())
78114
}
79115
Some(ClientMessage::Hello(port)) => {
80-
if port != 0 && port < self.min_port {
81-
warn!(?port, "client port number too low");
82-
return Ok(());
83-
}
84-
info!(?port, "new client");
85-
let listener = match TcpListener::bind(("0.0.0.0", port)).await {
116+
let listener = match self.create_listener(port).await {
86117
Ok(listener) => listener,
87-
Err(_) => {
88-
warn!(?port, "could not bind to local port");
89-
stream
90-
.send(ServerMessage::Error("port already in use".into()))
91-
.await?;
118+
Err(err) => {
119+
stream.send(ServerMessage::Error(err.into())).await?;
92120
return Ok(());
93121
}
94122
};
95123
let port = listener.local_addr()?.port();
124+
info!(?port, "new client");
96125
stream.send(ServerMessage::Hello(port)).await?;
97126

98127
loop {
@@ -133,16 +162,7 @@ impl Server {
133162
}
134163
Ok(())
135164
}
136-
None => {
137-
warn!("unexpected EOF");
138-
Ok(())
139-
}
165+
None => Ok(()),
140166
}
141167
}
142168
}
143-
144-
impl Default for Server {
145-
fn default() -> Self {
146-
Server::new(1024, None)
147-
}
148-
}

src/shared.rs

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use futures_util::{SinkExt, StreamExt};
77
use serde::de::DeserializeOwned;
88
use serde::{Deserialize, Serialize};
99
use tokio::io::{self, AsyncRead, AsyncWrite};
10-
1110
use tokio::time::timeout;
1211
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
1312
use tracing::trace;

tests/e2e_test.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ lazy_static! {
1717

1818
/// Spawn the server, giving some time for the control port TcpListener to start.
1919
async fn spawn_server(secret: Option<&str>) {
20-
tokio::spawn(Server::new(1024, secret).listen());
20+
tokio::spawn(Server::new(1024..=65535, secret).listen());
2121
time::sleep(Duration::from_millis(50)).await;
2222
}
2323

@@ -117,3 +117,11 @@ async fn very_long_frame() -> Result<()> {
117117
}
118118
panic!("did not exit after a 1 MB frame");
119119
}
120+
121+
#[test]
122+
#[should_panic]
123+
fn empty_port_range() {
124+
let min_port = 5000;
125+
let max_port = 3000;
126+
let _ = Server::new(min_port..=max_port, None);
127+
}

0 commit comments

Comments
 (0)