mirror of
https://github.com/ekzhang/bore.git
synced 2025-12-16 20:07:51 +01:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e25f021505 | ||
|
|
cae08bb3c2 | ||
|
|
b045d8028e | ||
|
|
36a56c0d4a | ||
|
|
99fc4f7ddb | ||
|
|
634af3f6af | ||
|
|
f6bd20a508 | ||
|
|
c154a846f6 | ||
|
|
d4e7c42949 | ||
|
|
b0bfd52707 | ||
|
|
526d02d789 | ||
|
|
23db4047ff | ||
|
|
2d0dcf9889 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: CI
|
||||
|
||||
on: push
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
rust:
|
||||
|
||||
49
.github/workflows/docker.yml
vendored
Normal file
49
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build_deploy:
|
||||
name: Build and Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ekzhang/bore
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
72
Cargo.lock
generated
72
Cargo.lock
generated
@@ -84,13 +84,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bore-cli"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"hex",
|
||||
"hmac",
|
||||
"lazy_static",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -276,9 +278,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.121"
|
||||
version = "0.2.123"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
|
||||
checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -436,18 +438,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.36"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58"
|
||||
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -461,12 +463,34 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
@@ -479,6 +503,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.136"
|
||||
@@ -530,15 +560,6 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.8.0"
|
||||
@@ -569,9 +590,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.90"
|
||||
version = "1.0.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f"
|
||||
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -613,10 +634,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"winapi",
|
||||
@@ -635,9 +653,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f"
|
||||
checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pin-project-lite",
|
||||
@@ -658,9 +676,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee"
|
||||
checksum = "6dfce9f3241b150f36e8e54bb561a742d5daa1a47b5dd9a5ce369fd4a4db2210"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"valuable",
|
||||
@@ -679,9 +697,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.10"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9df98b037d039d03400d9dd06b0f8ce05486b5f25e9a2d7d36196e142ebbc52"
|
||||
checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"sharded-slab",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bore-cli"
|
||||
version = "0.2.0"
|
||||
version = "0.3.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."
|
||||
@@ -17,14 +17,19 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.56", features = ["backtrace"] }
|
||||
clap = { version = "3.1.8", features = ["derive"] }
|
||||
clap = { version = "3.1.8", features = ["derive", "env"] }
|
||||
dashmap = "5.2.0"
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12.1"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
sha2 = "0.10.2"
|
||||
tokio = { version = "1.17.0", features = ["full"] }
|
||||
tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros", "net", "time"] }
|
||||
tracing = "0.1.32"
|
||||
tracing-subscriber = "0.3.10"
|
||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
rstest = "0.12.0"
|
||||
tokio = { version = "1.17.0", features = ["sync"] }
|
||||
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM rust:alpine as builder
|
||||
WORKDIR /home/rust/src
|
||||
RUN apk --no-cache add musl-dev
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /usr/local/cargo/bin/bore .
|
||||
USER 1000:1000
|
||||
ENTRYPOINT ["./bore"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Eric Zhang
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
README.md
39
README.md
@@ -21,6 +21,20 @@ Similar to [localtunnel](https://github.com/localtunnel/localtunnel) and [ngrok]
|
||||
|
||||
(`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.)
|
||||
|
||||
## Installation
|
||||
|
||||
You can build the `bore` CLI command 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
|
||||
```
|
||||
|
||||
We also publish versioned Docker images for each release. Each image is built for AMD 64-bit and Arm 64-bit architectures. They're tagged with the specific version and allow you to run the statically-linked `bore` binary from a minimal "scratch" container.
|
||||
|
||||
```shell
|
||||
docker run -it --init --rm --network host ekzhang/bore <ARGS>
|
||||
```
|
||||
|
||||
## Detailed Usage
|
||||
|
||||
This section describes detailed usage for the `bore` CLI command.
|
||||
@@ -33,31 +47,32 @@ You can forward a port on your local machine by using the `bore local` command.
|
||||
bore local 5000 --to bore.pub
|
||||
```
|
||||
|
||||
You can optionally pass in a `--port` option to pick a specific port on the remote to expose, although the command will fail if this port is not available.
|
||||
You can optionally pass in a `--port` option to pick a specific port on the remote to expose, although the command will fail if this port is not available. Also, passing `--local-host` allows you to expose a different host on your local area network besides the loopback address `localhost`.
|
||||
|
||||
The full options are shown below.
|
||||
|
||||
```shell
|
||||
bore-local 0.2.0
|
||||
bore-local 0.3.0
|
||||
Starts a local proxy to the remote server
|
||||
|
||||
USAGE:
|
||||
bore local [OPTIONS] --to <TO> <LOCAL_PORT>
|
||||
|
||||
ARGS:
|
||||
<LOCAL_PORT> The local port to listen on
|
||||
<LOCAL_PORT> The local port to expose
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
-p, --port <PORT> Optional port on the remote server to select [default: 0]
|
||||
-s, --secret <SECRET> Optional secret for authentication
|
||||
-t, --to <TO> Address of the remote server to expose local ports to
|
||||
-V, --version Print version information
|
||||
-h, --help Print help information
|
||||
-l, --local-host <HOST> The local host to expose [default: localhost]
|
||||
-p, --port <PORT> Optional port on the remote server to select [default: 0]
|
||||
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||
-t, --to <TO> Address of the remote server to expose local ports to
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
### Self-Hosting
|
||||
|
||||
As mentioned in the startup instructions, there is an public instance of the `bore` server running at `bore.pub`. However, if you want to self-host `bore` on your own network, you can do so with the following command:
|
||||
As mentioned in the startup instructions, there is a public instance of the `bore` server running at `bore.pub`. However, if you want to self-host `bore` on your own network, you can do so with the following command:
|
||||
|
||||
```shell
|
||||
bore server
|
||||
@@ -68,7 +83,7 @@ That's all it takes! After the server starts running at a given address, you can
|
||||
The full options for the `bore server` command are shown below.
|
||||
|
||||
```shell
|
||||
bore-server 0.2.0
|
||||
bore-server 0.3.0
|
||||
Runs the remote proxy server
|
||||
|
||||
USAGE:
|
||||
@@ -77,7 +92,7 @@ USAGE:
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024]
|
||||
-s, --secret <SECRET> Optional secret for authentication
|
||||
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
@@ -101,6 +116,8 @@ bore server --secret my_secret_string
|
||||
bore local <LOCAL_PORT> --to <TO> --secret my_secret_string
|
||||
```
|
||||
|
||||
If a secret is not present in the arguments, `bore` will also attempt to read from the `BORE_SECRET` environment variable.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Created by Eric Zhang ([@ekzhang1](https://twitter.com/ekzhang1)). Licensed under the [MIT license](LICENSE).
|
||||
|
||||
@@ -6,7 +6,7 @@ use sha2::{Digest, Sha256};
|
||||
use tokio::io::{AsyncBufRead, AsyncWrite};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::shared::{recv_json, send_json, ClientMessage, ServerMessage};
|
||||
use crate::shared::{recv_json_timeout, send_json, ClientMessage, ServerMessage};
|
||||
|
||||
/// Wrapper around a MAC used for authenticating clients that have a secret.
|
||||
pub struct Authenticator(Hmac<Sha256>);
|
||||
@@ -54,7 +54,7 @@ impl Authenticator {
|
||||
) -> Result<()> {
|
||||
let challenge = Uuid::new_v4();
|
||||
send_json(stream, ServerMessage::Challenge(challenge)).await?;
|
||||
match recv_json(stream, &mut Vec::new()).await? {
|
||||
match recv_json_timeout(stream).await? {
|
||||
Some(ClientMessage::Authenticate(tag)) => {
|
||||
ensure!(self.validate(&challenge, &tag), "invalid secret");
|
||||
Ok(())
|
||||
@@ -68,7 +68,7 @@ impl Authenticator {
|
||||
&self,
|
||||
stream: &mut (impl AsyncBufRead + AsyncWrite + Unpin),
|
||||
) -> Result<()> {
|
||||
let challenge = match recv_json(stream, &mut Vec::new()).await? {
|
||||
let challenge = match recv_json_timeout(stream).await? {
|
||||
Some(ServerMessage::Challenge(challenge)) => challenge,
|
||||
_ => bail!("expected authentication challenge, but no secret was required"),
|
||||
};
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use tokio::{io::BufReader, net::TcpStream};
|
||||
use tokio::{io::BufReader, net::TcpStream, time::timeout};
|
||||
use tracing::{error, info, info_span, warn, Instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::Authenticator;
|
||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
||||
use crate::shared::{
|
||||
proxy, recv_json, recv_json_timeout, send_json, ClientMessage, ServerMessage, CONTROL_PORT,
|
||||
NETWORK_TIMEOUT,
|
||||
};
|
||||
|
||||
/// State structure for the client.
|
||||
pub struct Client {
|
||||
@@ -18,6 +21,9 @@ pub struct Client {
|
||||
/// Destination address of the server.
|
||||
to: String,
|
||||
|
||||
// Local host that is forwarded.
|
||||
local_host: String,
|
||||
|
||||
/// Local port that is forwarded.
|
||||
local_port: u16,
|
||||
|
||||
@@ -30,11 +36,14 @@ pub struct Client {
|
||||
|
||||
impl Client {
|
||||
/// Create a new client.
|
||||
pub async fn new(local_port: u16, to: &str, port: u16, secret: Option<&str>) -> Result<Self> {
|
||||
let stream = TcpStream::connect((to, CONTROL_PORT))
|
||||
.await
|
||||
.with_context(|| format!("could not connect to {to}:{CONTROL_PORT}"))?;
|
||||
let mut stream = BufReader::new(stream);
|
||||
pub async fn new(
|
||||
local_host: &str,
|
||||
local_port: u16,
|
||||
to: &str,
|
||||
port: u16,
|
||||
secret: Option<&str>,
|
||||
) -> Result<Self> {
|
||||
let mut stream = BufReader::new(connect_with_timeout(to, CONTROL_PORT).await?);
|
||||
|
||||
let auth = secret.map(Authenticator::new);
|
||||
if let Some(auth) = &auth {
|
||||
@@ -42,7 +51,7 @@ impl Client {
|
||||
}
|
||||
|
||||
send_json(&mut stream, ClientMessage::Hello(port)).await?;
|
||||
let remote_port = match recv_json(&mut stream, &mut Vec::new()).await? {
|
||||
let remote_port = match recv_json_timeout(&mut stream).await? {
|
||||
Some(ServerMessage::Hello(remote_port)) => remote_port,
|
||||
Some(ServerMessage::Error(message)) => bail!("server error: {message}"),
|
||||
Some(ServerMessage::Challenge(_)) => {
|
||||
@@ -57,6 +66,7 @@ impl Client {
|
||||
Ok(Client {
|
||||
conn: Some(stream),
|
||||
to: to.to_string(),
|
||||
local_host: local_host.to_string(),
|
||||
local_port,
|
||||
remote_port,
|
||||
auth,
|
||||
@@ -99,21 +109,23 @@ impl Client {
|
||||
}
|
||||
|
||||
async fn handle_connection(&self, id: Uuid) -> Result<()> {
|
||||
let local_conn = TcpStream::connect(("localhost", self.local_port))
|
||||
.await
|
||||
.context("failed TCP connection to local port")?;
|
||||
let mut remote_conn = BufReader::new(
|
||||
TcpStream::connect((&self.to[..], CONTROL_PORT))
|
||||
.await
|
||||
.context("failed TCP connection to remote port")?,
|
||||
);
|
||||
|
||||
let mut remote_conn =
|
||||
BufReader::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?);
|
||||
if let Some(auth) = &self.auth {
|
||||
auth.client_handshake(&mut remote_conn).await?;
|
||||
}
|
||||
|
||||
send_json(&mut remote_conn, ClientMessage::Accept(id)).await?;
|
||||
|
||||
let local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;
|
||||
proxy(local_conn, remote_conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_with_timeout(to: &str, port: u16) -> Result<TcpStream> {
|
||||
match timeout(NETWORK_TIMEOUT, TcpStream::connect((to, port))).await {
|
||||
Ok(res) => res,
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
.with_context(|| format!("could not connect to {to}:{port}"))
|
||||
}
|
||||
|
||||
25
src/main.rs
25
src/main.rs
@@ -14,9 +14,13 @@ struct Args {
|
||||
enum Command {
|
||||
/// Starts a local proxy to the remote server.
|
||||
Local {
|
||||
/// The local port to listen on.
|
||||
/// The local port to expose.
|
||||
local_port: u16,
|
||||
|
||||
/// The local host to expose.
|
||||
#[clap(short, long, value_name = "HOST", default_value = "localhost")]
|
||||
local_host: String,
|
||||
|
||||
/// Address of the remote server to expose local ports to.
|
||||
#[clap(short, long)]
|
||||
to: String,
|
||||
@@ -26,7 +30,7 @@ enum Command {
|
||||
port: u16,
|
||||
|
||||
/// Optional secret for authentication.
|
||||
#[clap(short, long)]
|
||||
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||
secret: Option<String>,
|
||||
},
|
||||
|
||||
@@ -37,24 +41,22 @@ enum Command {
|
||||
min_port: u16,
|
||||
|
||||
/// Optional secret for authentication.
|
||||
#[clap(short, long)]
|
||||
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||
secret: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
async fn run(command: Command) -> Result<()> {
|
||||
match command {
|
||||
Command::Local {
|
||||
local_host,
|
||||
local_port,
|
||||
to,
|
||||
port,
|
||||
secret,
|
||||
} => {
|
||||
let client = Client::new(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?;
|
||||
}
|
||||
Command::Server { min_port, secret } => {
|
||||
@@ -64,3 +66,8 @@ async fn main() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
run(Args::parse().command)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ use tracing::{info, info_span, warn, Instrument};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::Authenticator;
|
||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
||||
use crate::shared::{
|
||||
proxy, recv_json_timeout, send_json, ClientMessage, ServerMessage, CONTROL_PORT,
|
||||
};
|
||||
|
||||
/// State structure for the server.
|
||||
pub struct Server {
|
||||
@@ -71,10 +73,7 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let msg = recv_json(&mut stream, &mut buf).await?;
|
||||
|
||||
match msg {
|
||||
match recv_json_timeout(&mut stream).await? {
|
||||
Some(ClientMessage::Authenticate(_)) => {
|
||||
warn!("unexpected authenticate");
|
||||
Ok(())
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
//! Shared data structures, utilities, and protocol definitions.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{self, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::time::timeout;
|
||||
use tracing::trace;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// TCP port used for control connections with the server.
|
||||
pub const CONTROL_PORT: u16 = 7835;
|
||||
|
||||
/// Timeout for network connections and initial protocol messages.
|
||||
pub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
/// A message from the client on the control connection.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ClientMessage {
|
||||
@@ -49,10 +56,10 @@ where
|
||||
{
|
||||
let (mut s1_read, mut s1_write) = io::split(stream1);
|
||||
let (mut s2_read, mut s2_write) = io::split(stream2);
|
||||
tokio::try_join!(
|
||||
io::copy(&mut s1_read, &mut s2_write),
|
||||
io::copy(&mut s2_read, &mut s1_write),
|
||||
)?;
|
||||
tokio::select! {
|
||||
res = io::copy(&mut s1_read, &mut s2_write) => res,
|
||||
res = io::copy(&mut s2_read, &mut s1_write) => res,
|
||||
}?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,6 +68,7 @@ pub async fn recv_json<T: DeserializeOwned>(
|
||||
reader: &mut (impl AsyncBufRead + Unpin),
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Result<Option<T>> {
|
||||
trace!("waiting to receive json message");
|
||||
buf.clear();
|
||||
reader.read_until(0, buf).await?;
|
||||
if buf.is_empty() {
|
||||
@@ -72,8 +80,21 @@ pub async fn recv_json<T: DeserializeOwned>(
|
||||
Ok(serde_json::from_slice(buf).context("failed to parse JSON")?)
|
||||
}
|
||||
|
||||
/// Read the next null-delimited JSON instruction, with a default timeout.
|
||||
///
|
||||
/// This is useful for parsing the initial message of a stream for handshake or
|
||||
/// other protocol purposes, where we do not want to wait indefinitely.
|
||||
pub async fn recv_json_timeout<T: DeserializeOwned>(
|
||||
reader: &mut (impl AsyncBufRead + Unpin),
|
||||
) -> Result<Option<T>> {
|
||||
timeout(NETWORK_TIMEOUT, recv_json(reader, &mut Vec::new()))
|
||||
.await
|
||||
.context("timed out waiting for initial message")?
|
||||
}
|
||||
|
||||
/// Send a null-terminated JSON instruction on a stream.
|
||||
pub async fn send_json<T: Serialize>(writer: &mut (impl AsyncWrite + Unpin), msg: T) -> Result<()> {
|
||||
trace!("sending json message");
|
||||
let msg = serde_json::to_vec(&msg)?;
|
||||
writer.write_all(&msg).await?;
|
||||
writer.write_all(&[0]).await?;
|
||||
|
||||
35
tests/auth_test.rs
Normal file
35
tests/auth_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use anyhow::Result;
|
||||
use bore_cli::auth::Authenticator;
|
||||
use tokio::io::{self, BufReader};
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_handshake() -> Result<()> {
|
||||
let auth = Authenticator::new("some secret string");
|
||||
|
||||
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
|
||||
let mut client = BufReader::new(client);
|
||||
let mut server = BufReader::new(server);
|
||||
|
||||
tokio::try_join!(
|
||||
auth.client_handshake(&mut client),
|
||||
auth.server_handshake(&mut server),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_handshake_fail() {
|
||||
let auth = Authenticator::new("client secret");
|
||||
let auth2 = Authenticator::new("different server secret");
|
||||
|
||||
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
|
||||
let mut client = BufReader::new(client);
|
||||
let mut server = BufReader::new(server);
|
||||
|
||||
let result = tokio::try_join!(
|
||||
auth.client_handshake(&mut client),
|
||||
auth2.server_handshake(&mut server),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
101
tests/e2e_test.rs
Normal file
101
tests/e2e_test.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bore_cli::{client::Client, server::Server};
|
||||
use lazy_static::lazy_static;
|
||||
use rstest::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time;
|
||||
|
||||
lazy_static! {
|
||||
/// Guard to make sure that tests are run serially, not concurrently.
|
||||
static ref SERIAL_GUARD: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
/// 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());
|
||||
time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
|
||||
/// Spawns a client with randomly assigned ports, returning the listener and remote address.
|
||||
async fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, SocketAddr)> {
|
||||
let listener = TcpListener::bind("localhost:0").await?;
|
||||
let local_port = listener.local_addr()?.port();
|
||||
let client = Client::new("localhost", local_port, "localhost", 0, secret).await?;
|
||||
let remote_addr = ([0, 0, 0, 0], client.remote_port()).into();
|
||||
tokio::spawn(client.listen());
|
||||
Ok((listener, remote_addr))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn basic_proxy(#[values(None, Some(""), Some("abc"))] secret: Option<&str>) -> Result<()> {
|
||||
let _guard = SERIAL_GUARD.lock().await;
|
||||
|
||||
spawn_server(secret).await;
|
||||
let (listener, addr) = spawn_client(secret).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let (mut stream, _) = listener.accept().await?;
|
||||
let mut buf = [0u8; 11];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
assert_eq!(&buf, b"hello world");
|
||||
|
||||
stream.write_all(b"I can send a message too!").await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let mut stream = TcpStream::connect(addr).await?;
|
||||
stream.write_all(b"hello world").await?;
|
||||
|
||||
let mut buf = [0u8; 25];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
assert_eq!(&buf, b"I can send a message too!");
|
||||
|
||||
// Ensure that the client end of the stream is closed now.
|
||||
assert_eq!(stream.read(&mut buf).await?, 0);
|
||||
|
||||
// Also ensure that additional connections do not produce any data.
|
||||
let mut stream = TcpStream::connect(addr).await?;
|
||||
assert_eq!(stream.read(&mut buf).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(None, Some("my secret"))]
|
||||
#[case(Some("my secret"), None)]
|
||||
#[tokio::test]
|
||||
async fn mismatched_secret(
|
||||
#[case] server_secret: Option<&str>,
|
||||
#[case] client_secret: Option<&str>,
|
||||
) {
|
||||
let _guard = SERIAL_GUARD.lock().await;
|
||||
|
||||
spawn_server(server_secret).await;
|
||||
assert!(spawn_client(client_secret).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_address() -> Result<()> {
|
||||
// We don't need the serial guard for this test because it doesn't create a server.
|
||||
async fn check_address(to: &str, use_secret: bool) -> Result<()> {
|
||||
match Client::new("localhost", 5000, to, 0, use_secret.then(|| "a secret")).await {
|
||||
Ok(_) => Err(anyhow!("expected error for {to}, use_secret={use_secret}")),
|
||||
Err(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
tokio::try_join!(
|
||||
check_address("google.com", false),
|
||||
check_address("google.com", true),
|
||||
check_address("nonexistent.domain.for.demonstration", false),
|
||||
check_address("nonexistent.domain.for.demonstration", true),
|
||||
check_address("malformed !$uri$%", false),
|
||||
check_address("malformed !$uri$%", true),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user