20 Commits
v0.5.1 ... main

Author SHA1 Message Date
Eric Zhang
8e059cdaf9 Bump version to 0.6.0 2025-06-09 16:12:10 -04:00
Eric Zhang
7969486d32 Use copy_bidirectional, handle half-closed TCP streams (#165) 2025-06-09 16:10:40 -04:00
Eric Zhang
8ad7ee212b Fix actions/cache action deprecation 2025-04-14 17:52:17 -04:00
Eric Zhang
19e7da1aad Bump version to 0.5.3 2025-04-14 17:48:02 -04:00
Eric Zhang
0128459a50 Minor API changes 2025-04-14 17:44:14 -04:00
confor
299ad61030 default --bind-tunnels to --bind-addr 2025-04-14 17:18:43 -04:00
confor
6a71c9a855 Merge branch 'ekzhang:main' into main 2025-04-14 16:57:24 -04:00
confor
dd954c98e2 rename flags to bind_addr and bind_tunnels 2025-04-14 16:46:31 -04:00
confor
53dad89514 use IpAddr type for validation and add error messages
From: https://github.com/ekzhang/bore/pull/162#discussion_r2042195219
2025-04-14 16:39:09 -04:00
狗娃子
03f2e53f39 docs: update installation instructions for Linux (#152)
* docs: update installation instructions for Linux

* Nits

* More nits

---------

Co-authored-by: Eric Zhang <ekzhang1@gmail.com>
2025-04-14 10:01:33 -04:00
confor
e137357267 Fix tests and run cargo fmt 2025-04-14 01:46:33 -04:00
confor
4bdb00c385 Document new server changes 2025-04-14 01:34:52 -04:00
confor
2a2541e866 Allow configurable tunnel IP address for server
This commit introduces a new option to specify the IP address where tunnels
will listen, which may be different from the control server's IP address.

A new flag `--tunnels-addr` can specify the IP addr to bind to. Default is set
to `0.0.0.0` which is the previous behavior.
2025-04-14 01:18:03 -04:00
confor
aa0d6e0ae5 Allow configurable control server bind address
This commit adds the ability to bind the control server to a specific network
interface via an additional flag `--control-addr`.

By default it listens on `0.0.0.0` which is the previous behaviour.
2025-04-14 00:59:38 -04:00
Stefan M.
b23beb98a2 Fix Dockerfile (#151) 2025-01-10 13:31:56 -05:00
Eric Zhang
baa42d0f7b Bump version to 0.5.2 2024-12-05 13:42:39 -05:00
Silas Alberti
da86f5bbba chore: update tracing-subscriber to 0.3.18 for NO_COLOR support (#146)
This update adds support for the NO_COLOR environment variable in tracing output,
which allows suppressing ANSI color codes in log output. This is particularly
useful when running bore as a Windows service.

Fixes #65

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2024-12-05 13:33:13 -05:00
J/A
0546092ce0 Expose local port as env value (#132)
Co-authored-by: Eric Zhang <ekzhang1@gmail.com>
2024-10-16 12:32:35 -04:00
Ricardo Araújo Paes
7fec9700c4 🔧 Adding minimum and maximum port as environment variable (#136)
* 🔧 Adding minimum and maximum port as environment variable

* 🏗️ Updating `actions/*` to v4 because nodejs version is old
2024-10-15 19:50:33 -04:00
虫子樱桃
a1e1f55a29 feat:add more target in github action flow (#120)
Co-authored-by: czyt <czyt@w.cn>
2024-06-13 09:38:59 -04:00
13 changed files with 159 additions and 68 deletions

View File

@@ -11,7 +11,7 @@ jobs:
name: Build and Test name: Build and Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
@@ -26,7 +26,7 @@ jobs:
name: Rustfmt name: Rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
@@ -40,7 +40,7 @@ jobs:
name: Clippy name: Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Docker meta - name: Docker meta
id: meta id: meta

View File

@@ -10,7 +10,7 @@ jobs:
install-cross: install-cross:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
with: with:
fetch-depth: 50 fetch-depth: 50
@@ -22,7 +22,7 @@ jobs:
matches: ${{ matrix.platform }} matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v4
with: with:
name: cross-${{ matrix.platform }} name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }} path: ${{ steps.cross.outputs.install_path }}
@@ -41,7 +41,7 @@ jobs:
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup | Rust - name: Setup | Rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@@ -61,12 +61,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: install-cross needs: install-cross
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 50 fetch-depth: 50
- name: Download Cross - name: Download Cross
uses: actions/download-artifact@v1 uses: actions/download-artifact@v4
with: with:
name: cross-linux-musl name: cross-linux-musl
path: /tmp/ path: /tmp/
@@ -89,8 +89,11 @@ jobs:
matrix: matrix:
channel: [stable] channel: [stable]
target: target:
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabi
- arm-unknown-linux-gnueabi - arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- i686-unknown-linux-musl - i686-unknown-linux-musl
- x86_64-unknown-linux-musl - x86_64-unknown-linux-musl
@@ -102,7 +105,7 @@ jobs:
# artifacts are downloaded first. # artifacts are downloaded first.
needs: install-cross needs: install-cross
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 50 fetch-depth: 50
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }} - run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}

View File

@@ -16,7 +16,7 @@ jobs:
install-cross: install-cross:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
with: with:
fetch-depth: 50 fetch-depth: 50
@@ -28,7 +28,7 @@ jobs:
matches: ${{ matrix.platform }} matches: ${{ matrix.platform }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v4
with: with:
name: cross-${{ matrix.platform }} name: cross-${{ matrix.platform }}
path: ${{ steps.cross.outputs.install_path }} path: ${{ steps.cross.outputs.install_path }}
@@ -50,11 +50,11 @@ jobs:
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
# Cache files between builds # Cache files between builds
- name: Setup | Cache Cargo - name: Setup | Cache Cargo
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: | path: |
~/.cargo/registry ~/.cargo/registry
@@ -102,8 +102,11 @@ jobs:
strategy: strategy:
matrix: matrix:
target: target:
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabi
- arm-unknown-linux-gnueabi - arm-unknown-linux-gnueabi
- armv7-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf
- armv7-unknown-linux-musleabihf
- i686-unknown-linux-musl - i686-unknown-linux-musl
- x86_64-unknown-linux-musl - x86_64-unknown-linux-musl
steps: steps:
@@ -111,8 +114,8 @@ jobs:
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/download-artifact@v1 - uses: actions/download-artifact@v4
with: with:
name: cross-linux-musl name: cross-linux-musl
path: /tmp/ path: /tmp/
@@ -160,7 +163,7 @@ jobs:
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- run: bash ci/set_rust_version.bash stable ${{ matrix.target }} - run: bash ci/set_rust_version.bash stable ${{ matrix.target }}
- run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE - run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE
- run: | - run: |

12
Cargo.lock generated
View File

@@ -113,7 +113,7 @@ dependencies = [
[[package]] [[package]]
name = "bore-cli" name = "bore-cli"
version = "0.5.1" version = "0.6.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -892,20 +892,20 @@ dependencies = [
[[package]] [[package]]
name = "tracing-log" name = "tracing-log"
version = "0.1.3" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [ dependencies = [
"lazy_static",
"log", "log",
"once_cell",
"tracing-core", "tracing-core",
] ]
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.3.17" version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"sharded-slab", "sharded-slab",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bore-cli" name = "bore-cli"
version = "0.5.1" version = "0.6.0"
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."
@@ -29,7 +29,7 @@ sha2 = "0.10.2"
tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros", "net", "time"] } tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros", "net", "time"] }
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.18"
uuid = { version = "1.2.1", features = ["serde", "v4"] } uuid = { version = "1.2.1", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]

View File

@@ -1,4 +1,4 @@
FROM rust:alpine as builder FROM rust:alpine AS builder
WORKDIR /home/rust/src WORKDIR /home/rust/src
RUN apk --no-cache add musl-dev RUN apk --no-cache add musl-dev
COPY . . COPY . .

View File

@@ -23,20 +23,48 @@ Similar to [localtunnel](https://github.com/localtunnel/localtunnel) and [ngrok]
## Installation ## Installation
If you're on macOS, `bore` is packaged as a Homebrew core formula. ### macOS
`bore` is packaged as a Homebrew core formula.
```shell ```shell
brew install bore-cli 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. 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. 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 ```shell
cargo install bore-cli 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. 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
@@ -65,14 +93,14 @@ Starts a local proxy to the remote server
Usage: bore local [OPTIONS] --to <TO> <LOCAL_PORT> Usage: bore local [OPTIONS] --to <TO> <LOCAL_PORT>
Arguments: Arguments:
<LOCAL_PORT> The local port to expose <LOCAL_PORT> The local port to expose [env: BORE_LOCAL_PORT=]
Options: Options:
-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=] -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]
-h, --help Print help information -h, --help Print help
``` ```
### Self-Hosting ### Self-Hosting
@@ -85,6 +113,8 @@ 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. 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. The full options for the `bore server` command are shown below.
```shell ```shell
@@ -93,10 +123,12 @@ Runs the remote proxy server
Usage: bore server [OPTIONS] Usage: bore server [OPTIONS]
Options: Options:
--min-port <MIN_PORT> Minimum accepted TCP port number [default: 1024] --min-port <MIN_PORT> Minimum accepted TCP port number [env: BORE_MIN_PORT=] [default: 1024]
--max-port <MAX_PORT> Maximum accepted TCP port number [default: 65535] --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] -s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
-h, --help Print help information --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 ## Protocol

View File

@@ -8,9 +8,7 @@ use tracing::{error, info, info_span, warn, Instrument};
use uuid::Uuid; use uuid::Uuid;
use crate::auth::Authenticator; use crate::auth::Authenticator;
use crate::shared::{ use crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT};
proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT,
};
/// State structure for the client. /// State structure for the client.
pub struct Client { pub struct Client {
@@ -112,10 +110,10 @@ impl Client {
} }
remote_conn.send(ClientMessage::Accept(id)).await?; remote_conn.send(ClientMessage::Accept(id)).await?;
let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?; let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;
let parts = remote_conn.into_parts(); let mut parts = remote_conn.into_parts();
debug_assert!(parts.write_buf.is_empty(), "framed write buffer not empty"); 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 local_conn.write_all(&parts.read_buf).await?; // mostly of the cases, this will be empty
proxy(local_conn, parts.io).await?; tokio::io::copy_bidirectional(&mut local_conn, &mut parts.io).await?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,3 +1,5 @@
use std::net::IpAddr;
use anyhow::Result; use anyhow::Result;
use bore_cli::{client::Client, server::Server}; use bore_cli::{client::Client, server::Server};
use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand}; use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
@@ -14,6 +16,7 @@ enum Command {
/// Starts a local proxy to the remote server. /// Starts a local proxy to the remote server.
Local { Local {
/// The local port to expose. /// The local port to expose.
#[clap(env = "BORE_LOCAL_PORT")]
local_port: u16, local_port: u16,
/// The local host to expose. /// The local host to expose.
@@ -36,16 +39,24 @@ enum Command {
/// Runs the remote proxy server. /// Runs the remote proxy server.
Server { Server {
/// Minimum accepted TCP port number. /// Minimum accepted TCP port number.
#[clap(long, default_value_t = 1024)] #[clap(long, default_value_t = 1024, env = "BORE_MIN_PORT")]
min_port: u16, min_port: u16,
/// Maximum accepted TCP port number. /// Maximum accepted TCP port number.
#[clap(long, default_value_t = 65535)] #[clap(long, default_value_t = 65535, env = "BORE_MAX_PORT")]
max_port: u16, 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>,
/// 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>,
}, },
} }
@@ -66,6 +77,8 @@ async fn run(command: Command) -> Result<()> {
min_port, min_port,
max_port, max_port,
secret, secret,
bind_addr,
bind_tunnels,
} => { } => {
let port_range = min_port..=max_port; let port_range = min_port..=max_port;
if port_range.is_empty() { if port_range.is_empty() {
@@ -73,7 +86,10 @@ async fn run(command: Command) -> Result<()> {
.error(ErrorKind::InvalidValue, "port range is empty") .error(ErrorKind::InvalidValue, "port range is empty")
.exit(); .exit();
} }
Server::new(port_range, secret.as_deref()).listen().await?; 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?;
} }
} }

View File

@@ -1,6 +1,7 @@
//! Server implementation for the `bore` service. //! Server implementation for the `bore` service.
use std::{io, net::SocketAddr, ops::RangeInclusive, sync::Arc, time::Duration}; use std::net::{IpAddr, Ipv4Addr};
use std::{io, ops::RangeInclusive, sync::Arc, time::Duration};
use anyhow::Result; use anyhow::Result;
use dashmap::DashMap; use dashmap::DashMap;
@@ -11,7 +12,7 @@ use tracing::{info, info_span, warn, Instrument};
use uuid::Uuid; use uuid::Uuid;
use crate::auth::Authenticator; use crate::auth::Authenticator;
use crate::shared::{proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT}; use crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT};
/// State structure for the server. /// State structure for the server.
pub struct Server { pub struct Server {
@@ -23,6 +24,12 @@ pub struct Server {
/// Concurrent map of IDs to incoming connections. /// Concurrent map of IDs to incoming connections.
conns: Arc<DashMap<Uuid, TcpStream>>, 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 { impl Server {
@@ -33,15 +40,26 @@ impl Server {
port_range, port_range,
conns: Arc::new(DashMap::new()), conns: Arc::new(DashMap::new()),
auth: secret.map(Authenticator::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. /// Start the server, listening for new connections.
pub async fn listen(self) -> Result<()> { pub async fn listen(self) -> Result<()> {
let this = Arc::new(self); let this = Arc::new(self);
let addr = SocketAddr::from(([0, 0, 0, 0], CONTROL_PORT)); let listener = TcpListener::bind((this.bind_addr, CONTROL_PORT)).await?;
let listener = TcpListener::bind(&addr).await?; info!(addr = ?this.bind_addr, "server listening");
info!(?addr, "server listening");
loop { loop {
let (stream, addr) = listener.accept().await?; let (stream, addr) = listener.accept().await?;
@@ -62,7 +80,7 @@ impl Server {
async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> { async fn create_listener(&self, port: u16) -> Result<TcpListener, &'static str> {
let try_bind = |port: u16| async move { let try_bind = |port: u16| async move {
TcpListener::bind(("0.0.0.0", port)) TcpListener::bind((self.bind_tunnels, port))
.await .await
.map_err(|err| match err.kind() { .map_err(|err| match err.kind() {
io::ErrorKind::AddrInUse => "port already in use", io::ErrorKind::AddrInUse => "port already in use",
@@ -120,8 +138,9 @@ impl Server {
return Ok(()); return Ok(());
} }
}; };
let host = listener.local_addr()?.ip();
let port = listener.local_addr()?.port(); let port = listener.local_addr()?.port();
info!(?port, "new client"); info!(?host, ?port, "new client");
stream.send(ServerMessage::Hello(port)).await?; stream.send(ServerMessage::Hello(port)).await?;
loop { loop {
@@ -153,10 +172,10 @@ impl Server {
info!(%id, "forwarding connection"); info!(%id, "forwarding connection");
match self.conns.remove(&id) { match self.conns.remove(&id) {
Some((_, mut stream2)) => { Some((_, mut stream2)) => {
let parts = stream.into_parts(); let mut parts = stream.into_parts();
debug_assert!(parts.write_buf.is_empty(), "framed write buffer not empty"); debug_assert!(parts.write_buf.is_empty(), "framed write buffer not empty");
stream2.write_all(&parts.read_buf).await?; stream2.write_all(&parts.read_buf).await?;
proxy(parts.io, stream2).await? tokio::io::copy_bidirectional(&mut parts.io, &mut stream2).await?;
} }
None => warn!(%id, "missing connection"), None => warn!(%id, "missing connection"),
} }

View File

@@ -5,7 +5,7 @@ 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, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::io::{self, AsyncRead, AsyncWrite}; use tokio::io::{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;
@@ -97,18 +97,3 @@ impl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {
self.0.into_parts() self.0.into_parts()
} }
} }
/// Copy data mutually between two read/write streams.
pub async fn proxy<S1, S2>(stream1: S1, stream2: S2) -> io::Result<()>
where
S1: AsyncRead + AsyncWrite + Unpin,
S2: AsyncRead + AsyncWrite + Unpin,
{
let (mut s1_read, mut s1_write) = io::split(stream1);
let (mut s2_read, mut s2_write) = io::split(stream2);
tokio::select! {
res = io::copy(&mut s1_read, &mut s2_write) => res,
res = io::copy(&mut s2_read, &mut s1_write) => res,
}?;
Ok(())
}

View File

@@ -1,5 +1,3 @@
#![allow(clippy::items_after_test_module)]
use std::net::SocketAddr; use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
@@ -127,3 +125,40 @@ fn empty_port_range() {
let max_port = 3000; let max_port = 3000;
let _ = Server::new(min_port..=max_port, None); 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(())
}