26 Commits
v0.5.0 ... 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
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
14 changed files with 201 additions and 94 deletions

View File

@@ -1,13 +1,17 @@
name: CI
on: [push, pull_request]
on:
push:
branches:
- main
pull_request:
jobs:
rust:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
@@ -22,7 +26,7 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
@@ -36,7 +40,7 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
id: meta
@@ -40,9 +40,7 @@ jobs:
id: docker_build
uses: docker/build-push-action@v3
with:
# This doesn't work now because of an issue in multi-platform Docker builds.
# -> see https://github.com/rust-lang/rust/issues/97520
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

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

View File

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

40
Cargo.lock generated
View File

@@ -113,7 +113,7 @@ dependencies = [
[[package]]
name = "bore-cli"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"anyhow",
"clap",
@@ -409,18 +409,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.2.6"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
@@ -452,7 +443,7 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi 0.3.1",
"hermit-abi",
"libc",
"windows-sys 0.48.0",
]
@@ -463,7 +454,7 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.1",
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys 0.48.0",
@@ -551,11 +542,11 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.2.6",
"hermit-abi",
"libc",
]
@@ -868,10 +859,11 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.38"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -900,20 +892,20 @@ dependencies = [
[[package]]
name = "tracing-log"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"lazy_static",
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",

View File

@@ -1,6 +1,6 @@
[package]
name = "bore-cli"
version = "0.5.0"
version = "0.6.0"
authors = ["Eric Zhang <ekzhang1@gmail.com>"]
license = "MIT"
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-util = { version = "0.7.1", features = ["codec"] }
tracing = "0.1.32"
tracing-subscriber = "0.3.10"
tracing-subscriber = "0.3.18"
uuid = { version = "1.2.1", features = ["serde", "v4"] }
[dev-dependencies]

View File

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

View File

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

View File

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

@@ -8,9 +8,7 @@ use tracing::{error, info, info_span, warn, Instrument};
use uuid::Uuid;
use crate::auth::Authenticator;
use crate::shared::{
proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT,
};
use crate::shared::{ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT};
/// State structure for the client.
pub struct Client {
@@ -112,10 +110,10 @@ impl Client {
}
remote_conn.send(ClientMessage::Accept(id)).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");
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(())
}
}

View File

@@ -1,3 +1,5 @@
use std::net::IpAddr;
use anyhow::Result;
use bore_cli::{client::Client, server::Server};
use clap::{error::ErrorKind, CommandFactory, Parser, Subcommand};
@@ -14,6 +16,7 @@ enum Command {
/// Starts a local proxy to the remote server.
Local {
/// The local port to expose.
#[clap(env = "BORE_LOCAL_PORT")]
local_port: u16,
/// The local host to expose.
@@ -36,16 +39,24 @@ enum Command {
/// Runs the remote proxy server.
Server {
/// Minimum accepted TCP port number.
#[clap(long, default_value_t = 1024)]
#[clap(long, default_value_t = 1024, env = "BORE_MIN_PORT")]
min_port: u16,
/// Maximum accepted TCP port number.
#[clap(long, default_value_t = 65535)]
#[clap(long, default_value_t = 65535, env = "BORE_MAX_PORT")]
max_port: u16,
/// Optional secret for authentication.
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
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,
max_port,
secret,
bind_addr,
bind_tunnels,
} => {
let port_range = min_port..=max_port;
if port_range.is_empty() {
@@ -73,7 +86,10 @@ async fn run(command: Command) -> Result<()> {
.error(ErrorKind::InvalidValue, "port range is empty")
.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.
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 dashmap::DashMap;
@@ -11,7 +12,7 @@ use tracing::{info, info_span, warn, Instrument};
use uuid::Uuid;
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.
pub struct Server {
@@ -23,6 +24,12 @@ pub struct Server {
/// Concurrent map of IDs to incoming connections.
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 {
@@ -33,15 +40,26 @@ impl Server {
port_range,
conns: Arc::new(DashMap::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.
pub async fn listen(self) -> Result<()> {
let this = Arc::new(self);
let addr = SocketAddr::from(([0, 0, 0, 0], CONTROL_PORT));
let listener = TcpListener::bind(&addr).await?;
info!(?addr, "server listening");
let listener = TcpListener::bind((this.bind_addr, CONTROL_PORT)).await?;
info!(addr = ?this.bind_addr, "server listening");
loop {
let (stream, addr) = listener.accept().await?;
@@ -62,7 +80,7 @@ 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))
TcpListener::bind((self.bind_tunnels, port))
.await
.map_err(|err| match err.kind() {
io::ErrorKind::AddrInUse => "port already in use",
@@ -120,8 +138,9 @@ impl Server {
return Ok(());
}
};
let host = listener.local_addr()?.ip();
let port = listener.local_addr()?.port();
info!(?port, "new client");
info!(?host, ?port, "new client");
stream.send(ServerMessage::Hello(port)).await?;
loop {
@@ -153,10 +172,10 @@ impl Server {
info!(%id, "forwarding connection");
match self.conns.remove(&id) {
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");
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"),
}

View File

@@ -5,7 +5,7 @@ use std::time::Duration;
use anyhow::{Context, Result};
use futures_util::{SinkExt, StreamExt};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use tokio::io::{self, AsyncRead, AsyncWrite};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::timeout;
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
use tracing::trace;
@@ -14,7 +14,7 @@ use uuid::Uuid;
/// TCP port used for control connections with the server.
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;
/// Timeout for network connections and initial protocol messages.
@@ -97,18 +97,3 @@ impl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {
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

@@ -125,3 +125,40 @@ fn empty_port_range() {
let max_port = 3000;
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(())
}