mirror of
https://github.com/ekzhang/bore.git
synced 2025-12-16 20:07:51 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e25f021505 | ||
|
|
cae08bb3c2 | ||
|
|
b045d8028e | ||
|
|
36a56c0d4a | ||
|
|
99fc4f7ddb | ||
|
|
634af3f6af | ||
|
|
f6bd20a508 | ||
|
|
c154a846f6 | ||
|
|
d4e7c42949 | ||
|
|
b0bfd52707 | ||
|
|
526d02d789 | ||
|
|
23db4047ff | ||
|
|
2d0dcf9889 | ||
|
|
c1efefeddf | ||
|
|
e84bd34bd9 | ||
|
|
d5089cab2a | ||
|
|
ebae01417d |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -1,9 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on: [push, pull_request]
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust:
|
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 }}
|
||||||
164
Cargo.lock
generated
164
Cargo.lock
generated
@@ -73,15 +73,29 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bore-cli"
|
name = "bore-cli"
|
||||||
version = "0.1.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"hex",
|
||||||
|
"hmac",
|
||||||
|
"lazy_static",
|
||||||
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -136,6 +150,25 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.2.0"
|
version = "5.2.0"
|
||||||
@@ -147,6 +180,27 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@@ -185,6 +239,21 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -209,9 +278,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.121"
|
version = "0.2.123"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
|
checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -369,18 +438,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.36"
|
version = "1.0.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58"
|
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -394,12 +463,34 @@ dependencies = [
|
|||||||
"bitflags",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
|
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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -412,6 +503,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.136"
|
version = "1.0.136"
|
||||||
@@ -443,6 +540,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -452,15 +560,6 @@ dependencies = [
|
|||||||
"lazy_static",
|
"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]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
@@ -484,10 +583,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "subtle"
|
||||||
version = "1.0.90"
|
version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f"
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -529,10 +634,7 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"winapi",
|
"winapi",
|
||||||
@@ -551,9 +653,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.32"
|
version = "0.1.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f"
|
checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -574,9 +676,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.24"
|
version = "0.1.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee"
|
checksum = "6dfce9f3241b150f36e8e54bb561a742d5daa1a47b5dd9a5ce369fd4a4db2210"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"valuable",
|
"valuable",
|
||||||
@@ -595,9 +697,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.10"
|
version = "0.3.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9df98b037d039d03400d9dd06b0f8ce05486b5f25e9a2d7d36196e142ebbc52"
|
checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
@@ -607,6 +709,12 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bore-cli"
|
name = "bore-cli"
|
||||||
version = "0.1.1"
|
version = "0.3.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."
|
||||||
@@ -17,11 +17,19 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.56", features = ["backtrace"] }
|
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"
|
dashmap = "5.2.0"
|
||||||
|
hex = "0.4.3"
|
||||||
|
hmac = "0.12.1"
|
||||||
serde = { version = "1.0.136", features = ["derive"] }
|
serde = { version = "1.0.136", features = ["derive"] }
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
tokio = { version = "1.17.0", features = ["full"] }
|
sha2 = "0.10.2"
|
||||||
|
tokio = { version = "1.17.0", features = ["rt-multi-thread", "io-util", "macros", "net", "time"] }
|
||||||
tracing = "0.1.32"
|
tracing = "0.1.32"
|
||||||
tracing-subscriber = "0.3.10"
|
tracing-subscriber = "0.3.10"
|
||||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
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.
|
||||||
51
README.md
51
README.md
@@ -19,7 +19,21 @@ 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 300 lines of safe, async Rust code and is trivial to set up — just run a single binary for the client and server.)
|
(`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
|
## Detailed Usage
|
||||||
|
|
||||||
@@ -33,30 +47,32 @@ You can forward a port on your local machine by using the `bore local` command.
|
|||||||
bore local 5000 --to bore.pub
|
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.
|
The full options are shown below.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
bore-local 0.1.0
|
bore-local 0.3.0
|
||||||
Starts a local proxy to the remote server
|
Starts a local proxy to the remote server
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
bore local [OPTIONS] --to <TO> <LOCAL_PORT>
|
bore local [OPTIONS] --to <TO> <LOCAL_PORT>
|
||||||
|
|
||||||
ARGS:
|
ARGS:
|
||||||
<LOCAL_PORT> The local port to listen on
|
<LOCAL_PORT> The local port to expose
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
-p, --port <PORT> Optional port on the remote server to select [default: 0]
|
-l, --local-host <HOST> The local host to expose [default: localhost]
|
||||||
-t, --to <TO> Address of the remote server to expose local ports to
|
-p, --port <PORT> Optional port on the remote server to select [default: 0]
|
||||||
-V, --version Print version information
|
-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
|
### 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
|
```shell
|
||||||
bore server
|
bore server
|
||||||
@@ -67,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.
|
The full options for the `bore server` command are shown below.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
bore-server 0.1.0
|
bore-server 0.3.0
|
||||||
Runs the remote proxy server
|
Runs the remote proxy server
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
@@ -76,6 +92,7 @@ USAGE:
|
|||||||
OPTIONS:
|
OPTIONS:
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024]
|
--min-port <MIN_PORT> Minimum TCP port number to accept [default: 1024]
|
||||||
|
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||||
-V, --version Print version information
|
-V, --version Print version information
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,6 +104,20 @@ Whenever the server obtains a connection on the remote port, it generates a secu
|
|||||||
|
|
||||||
For correctness reasons and to avoid memory leaks, incoming connections are only stored by the server for up to 10 seconds before being discarded if the client does not accept them.
|
For correctness reasons and to avoid memory leaks, incoming connections are only stored by the server for up to 10 seconds before being discarded if the client does not accept them.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
On a custom deployment of `bore server`, you can optionally require a _secret_ to prevent the server from being used by others. The protocol requires clients to verify possession of the secret on each TCP connection by answering random challenges in the form of HMAC codes. (This secret is only used for the initial handshake, and no further traffic is encrypted by default.)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# on the server
|
||||||
|
bore server --secret my_secret_string
|
||||||
|
|
||||||
|
# on the client
|
||||||
|
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
|
## Acknowledgements
|
||||||
|
|
||||||
Created by Eric Zhang ([@ekzhang1](https://twitter.com/ekzhang1)). Licensed under the [MIT license](LICENSE).
|
Created by Eric Zhang ([@ekzhang1](https://twitter.com/ekzhang1)). Licensed under the [MIT license](LICENSE).
|
||||||
|
|||||||
79
src/auth.rs
Normal file
79
src/auth.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
//! Auth implementation for bore client and server.
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure, Result};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use tokio::io::{AsyncBufRead, AsyncWrite};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
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>);
|
||||||
|
|
||||||
|
impl Authenticator {
|
||||||
|
/// Generate an authenticator from a secret.
|
||||||
|
pub fn new(secret: &str) -> Self {
|
||||||
|
let hashed_secret = Sha256::new().chain_update(secret).finalize();
|
||||||
|
Self(Hmac::new_from_slice(&hashed_secret).expect("HMAC can take key of any size"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a reply message for a challenge.
|
||||||
|
pub fn answer(&self, challenge: &Uuid) -> String {
|
||||||
|
let mut hmac = self.0.clone();
|
||||||
|
hmac.update(challenge.as_bytes());
|
||||||
|
hex::encode(hmac.finalize().into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a reply to a challenge.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use bore_cli::auth::Authenticator;
|
||||||
|
/// use uuid::Uuid;
|
||||||
|
///
|
||||||
|
/// let auth = Authenticator::new("secret");
|
||||||
|
/// let challenge = Uuid::new_v4();
|
||||||
|
///
|
||||||
|
/// assert!(auth.validate(&challenge, &auth.answer(&challenge)));
|
||||||
|
/// assert!(!auth.validate(&challenge, "wrong answer"));
|
||||||
|
/// ```
|
||||||
|
pub fn validate(&self, challenge: &Uuid, tag: &str) -> bool {
|
||||||
|
if let Ok(tag) = hex::decode(tag) {
|
||||||
|
let mut hmac = self.0.clone();
|
||||||
|
hmac.update(challenge.as_bytes());
|
||||||
|
hmac.verify_slice(&tag).is_ok()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As the server, send a challenge to the client and validate their response.
|
||||||
|
pub async fn server_handshake(
|
||||||
|
&self,
|
||||||
|
stream: &mut (impl AsyncBufRead + AsyncWrite + Unpin),
|
||||||
|
) -> Result<()> {
|
||||||
|
let challenge = Uuid::new_v4();
|
||||||
|
send_json(stream, ServerMessage::Challenge(challenge)).await?;
|
||||||
|
match recv_json_timeout(stream).await? {
|
||||||
|
Some(ClientMessage::Authenticate(tag)) => {
|
||||||
|
ensure!(self.validate(&challenge, &tag), "invalid secret");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => bail!("server requires secret, but no secret was provided"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As the client, answer a challenge to attempt to authenticate with the server.
|
||||||
|
pub async fn client_handshake(
|
||||||
|
&self,
|
||||||
|
stream: &mut (impl AsyncBufRead + AsyncWrite + Unpin),
|
||||||
|
) -> Result<()> {
|
||||||
|
let challenge = match recv_json_timeout(stream).await? {
|
||||||
|
Some(ServerMessage::Challenge(challenge)) => challenge,
|
||||||
|
_ => bail!("expected authentication challenge, but no secret was required"),
|
||||||
|
};
|
||||||
|
let tag = self.answer(&challenge);
|
||||||
|
send_json(stream, ClientMessage::Authenticate(tag)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,15 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
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 tracing::{error, info, info_span, warn, Instrument};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
use crate::auth::Authenticator;
|
||||||
|
use crate::shared::{
|
||||||
|
proxy, recv_json, recv_json_timeout, send_json, ClientMessage, ServerMessage, CONTROL_PORT,
|
||||||
|
NETWORK_TIMEOUT,
|
||||||
|
};
|
||||||
|
|
||||||
/// State structure for the client.
|
/// State structure for the client.
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
@@ -17,23 +21,42 @@ pub struct Client {
|
|||||||
/// Destination address of the server.
|
/// Destination address of the server.
|
||||||
to: String,
|
to: String,
|
||||||
|
|
||||||
|
// Local host that is forwarded.
|
||||||
|
local_host: String,
|
||||||
|
|
||||||
/// Local port that is forwarded.
|
/// Local port that is forwarded.
|
||||||
local_port: u16,
|
local_port: u16,
|
||||||
|
|
||||||
/// Port that is publicly available on the remote.
|
/// Port that is publicly available on the remote.
|
||||||
remote_port: u16,
|
remote_port: u16,
|
||||||
|
|
||||||
|
/// Optional secret used to authenticate clients.
|
||||||
|
auth: Option<Authenticator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client.
|
/// Create a new client.
|
||||||
pub async fn new(local_port: u16, to: &str, port: u16) -> Result<Self> {
|
pub async fn new(
|
||||||
let stream = TcpStream::connect((to, CONTROL_PORT)).await?;
|
local_host: &str,
|
||||||
let mut stream = BufReader::new(stream);
|
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 {
|
||||||
|
auth.client_handshake(&mut stream).await?;
|
||||||
|
}
|
||||||
|
|
||||||
send_json(&mut stream, ClientMessage::Hello(port)).await?;
|
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::Hello(remote_port)) => remote_port,
|
||||||
Some(ServerMessage::Error(message)) => bail!("server error: {message}"),
|
Some(ServerMessage::Error(message)) => bail!("server error: {message}"),
|
||||||
|
Some(ServerMessage::Challenge(_)) => {
|
||||||
|
bail!("server requires authentication, but no client secret was provided");
|
||||||
|
}
|
||||||
Some(_) => bail!("unexpected initial non-hello message"),
|
Some(_) => bail!("unexpected initial non-hello message"),
|
||||||
None => bail!("unexpected EOF"),
|
None => bail!("unexpected EOF"),
|
||||||
};
|
};
|
||||||
@@ -43,8 +66,10 @@ impl Client {
|
|||||||
Ok(Client {
|
Ok(Client {
|
||||||
conn: Some(stream),
|
conn: Some(stream),
|
||||||
to: to.to_string(),
|
to: to.to_string(),
|
||||||
|
local_host: local_host.to_string(),
|
||||||
local_port,
|
local_port,
|
||||||
remote_port,
|
remote_port,
|
||||||
|
auth,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +87,7 @@ impl Client {
|
|||||||
let msg = recv_json(&mut conn, &mut buf).await?;
|
let msg = recv_json(&mut conn, &mut buf).await?;
|
||||||
match msg {
|
match msg {
|
||||||
Some(ServerMessage::Hello(_)) => warn!("unexpected hello"),
|
Some(ServerMessage::Hello(_)) => warn!("unexpected hello"),
|
||||||
|
Some(ServerMessage::Challenge(_)) => warn!("unexpected challenge"),
|
||||||
Some(ServerMessage::Heartbeat) => (),
|
Some(ServerMessage::Heartbeat) => (),
|
||||||
Some(ServerMessage::Connection(id)) => {
|
Some(ServerMessage::Connection(id)) => {
|
||||||
let this = Arc::clone(&this);
|
let this = Arc::clone(&this);
|
||||||
@@ -83,15 +109,23 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_connection(&self, id: Uuid) -> Result<()> {
|
async fn handle_connection(&self, id: Uuid) -> Result<()> {
|
||||||
let local_conn = TcpStream::connect(("localhost", self.local_port))
|
let mut remote_conn =
|
||||||
.await
|
BufReader::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?);
|
||||||
.context("failed TCP connection to local port")?;
|
if let Some(auth) = &self.auth {
|
||||||
let mut remote_conn = TcpStream::connect((&self.to[..], CONTROL_PORT))
|
auth.client_handshake(&mut remote_conn).await?;
|
||||||
.await
|
}
|
||||||
.context("failed TCP connection to remote port")?;
|
|
||||||
|
|
||||||
send_json(&mut remote_conn, ClientMessage::Accept(id)).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?;
|
proxy(local_conn, remote_conn).await?;
|
||||||
Ok(())
|
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}"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod shared;
|
pub mod shared;
|
||||||
|
|||||||
34
src/main.rs
34
src/main.rs
@@ -14,9 +14,13 @@ struct Args {
|
|||||||
enum Command {
|
enum Command {
|
||||||
/// Starts a local proxy to the remote server.
|
/// Starts a local proxy to the remote server.
|
||||||
Local {
|
Local {
|
||||||
/// The local port to listen on.
|
/// The local port to expose.
|
||||||
local_port: u16,
|
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.
|
/// Address of the remote server to expose local ports to.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
to: String,
|
to: String,
|
||||||
@@ -24,6 +28,10 @@ enum Command {
|
|||||||
/// Optional port on the remote server to select.
|
/// Optional port on the remote server to select.
|
||||||
#[clap(short, long, default_value_t = 0)]
|
#[clap(short, long, default_value_t = 0)]
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
|
/// Optional secret for authentication.
|
||||||
|
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||||
|
secret: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Runs the remote proxy server.
|
/// Runs the remote proxy server.
|
||||||
@@ -31,27 +39,35 @@ enum Command {
|
|||||||
/// Minimum TCP port number to accept.
|
/// Minimum TCP port number to accept.
|
||||||
#[clap(long, default_value_t = 1024)]
|
#[clap(long, default_value_t = 1024)]
|
||||||
min_port: u16,
|
min_port: u16,
|
||||||
|
|
||||||
|
/// Optional secret for authentication.
|
||||||
|
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||||
|
secret: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn run(command: Command) -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
match command {
|
||||||
|
|
||||||
let args = Args::parse();
|
|
||||||
match args.command {
|
|
||||||
Command::Local {
|
Command::Local {
|
||||||
|
local_host,
|
||||||
local_port,
|
local_port,
|
||||||
to,
|
to,
|
||||||
port,
|
port,
|
||||||
|
secret,
|
||||||
} => {
|
} => {
|
||||||
let client = Client::new(local_port, &to, port).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 } => {
|
Command::Server { min_port, secret } => {
|
||||||
Server::new(min_port).listen().await?;
|
Server::new(min_port, secret.as_deref()).listen().await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
run(Args::parse().command)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,23 +12,30 @@ use tokio::time::{sleep, timeout};
|
|||||||
use tracing::{info, info_span, warn, Instrument};
|
use tracing::{info, info_span, warn, Instrument};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
use crate::auth::Authenticator;
|
||||||
|
use crate::shared::{
|
||||||
|
proxy, recv_json_timeout, send_json, ClientMessage, 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.
|
/// The minimum TCP port that can be forwarded.
|
||||||
min_port: u16,
|
min_port: u16,
|
||||||
|
|
||||||
|
/// Optional secret used to authenticate clients.
|
||||||
|
auth: Option<Authenticator>,
|
||||||
|
|
||||||
/// Concurrent map of IDs to incoming connections.
|
/// Concurrent map of IDs to incoming connections.
|
||||||
conns: Arc<DashMap<Uuid, TcpStream>>,
|
conns: Arc<DashMap<Uuid, TcpStream>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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) -> Self {
|
pub fn new(min_port: u16, secret: Option<&str>) -> Self {
|
||||||
Server {
|
Server {
|
||||||
min_port,
|
min_port,
|
||||||
conns: Arc::new(DashMap::new()),
|
conns: Arc::new(DashMap::new()),
|
||||||
|
auth: secret.map(Authenticator::new),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,11 +65,19 @@ impl Server {
|
|||||||
|
|
||||||
async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
|
async fn handle_connection(&self, stream: TcpStream) -> Result<()> {
|
||||||
let mut stream = BufReader::new(stream);
|
let mut stream = BufReader::new(stream);
|
||||||
|
if let Some(auth) = &self.auth {
|
||||||
|
if let Err(err) = auth.server_handshake(&mut stream).await {
|
||||||
|
warn!(%err, "server handshake failed");
|
||||||
|
send_json(&mut stream, ServerMessage::Error(err.to_string())).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
match recv_json_timeout(&mut stream).await? {
|
||||||
let msg = recv_json(&mut stream, &mut buf).await?;
|
Some(ClientMessage::Authenticate(_)) => {
|
||||||
|
warn!("unexpected authenticate");
|
||||||
match msg {
|
Ok(())
|
||||||
|
}
|
||||||
Some(ClientMessage::Hello(port)) => {
|
Some(ClientMessage::Hello(port)) => {
|
||||||
if port != 0 && port < self.min_port {
|
if port != 0 && port < self.min_port {
|
||||||
warn!(?port, "client port number too low");
|
warn!(?port, "client port number too low");
|
||||||
@@ -99,6 +114,7 @@ impl Server {
|
|||||||
|
|
||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let conns = Arc::clone(&self.conns);
|
let conns = Arc::clone(&self.conns);
|
||||||
|
|
||||||
conns.insert(id, stream2);
|
conns.insert(id, stream2);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Remove stale entries to avoid memory leaks.
|
// Remove stale entries to avoid memory leaks.
|
||||||
@@ -129,6 +145,6 @@ impl Server {
|
|||||||
|
|
||||||
impl Default for Server {
|
impl Default for Server {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Server::new(1024)
|
Server::new(1024, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
//! Shared data structures, utilities, and protocol definitions.
|
//! Shared data structures, utilities, and protocol definitions.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{self, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{self, AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tracing::trace;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// TCP port used for control connections with the server.
|
/// TCP port used for control connections with the server.
|
||||||
pub const CONTROL_PORT: u16 = 7835;
|
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.
|
/// A message from the client on the control connection.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
|
/// Response to an authentication challenge from the server.
|
||||||
|
Authenticate(String),
|
||||||
|
|
||||||
/// Initial client message specifying a port to forward.
|
/// Initial client message specifying a port to forward.
|
||||||
Hello(u16),
|
Hello(u16),
|
||||||
|
|
||||||
@@ -20,8 +30,11 @@ pub enum ClientMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A message from the server on the control connection.
|
/// A message from the server on the control connection.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum ServerMessage {
|
pub enum ServerMessage {
|
||||||
|
/// Authentication challenge, sent as the first message, if enabled.
|
||||||
|
Challenge(Uuid),
|
||||||
|
|
||||||
/// Response to a client's initial message, with actual public port.
|
/// Response to a client's initial message, with actual public port.
|
||||||
Hello(u16),
|
Hello(u16),
|
||||||
|
|
||||||
@@ -43,10 +56,10 @@ where
|
|||||||
{
|
{
|
||||||
let (mut s1_read, mut s1_write) = io::split(stream1);
|
let (mut s1_read, mut s1_write) = io::split(stream1);
|
||||||
let (mut s2_read, mut s2_write) = io::split(stream2);
|
let (mut s2_read, mut s2_write) = io::split(stream2);
|
||||||
tokio::try_join!(
|
tokio::select! {
|
||||||
io::copy(&mut s1_read, &mut s2_write),
|
res = io::copy(&mut s1_read, &mut s2_write) => res,
|
||||||
io::copy(&mut s2_read, &mut s1_write),
|
res = io::copy(&mut s2_read, &mut s1_write) => res,
|
||||||
)?;
|
}?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +68,7 @@ pub async fn recv_json<T: DeserializeOwned>(
|
|||||||
reader: &mut (impl AsyncBufRead + Unpin),
|
reader: &mut (impl AsyncBufRead + Unpin),
|
||||||
buf: &mut Vec<u8>,
|
buf: &mut Vec<u8>,
|
||||||
) -> Result<Option<T>> {
|
) -> Result<Option<T>> {
|
||||||
|
trace!("waiting to receive json message");
|
||||||
buf.clear();
|
buf.clear();
|
||||||
reader.read_until(0, buf).await?;
|
reader.read_until(0, buf).await?;
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
@@ -66,8 +80,21 @@ pub async fn recv_json<T: DeserializeOwned>(
|
|||||||
Ok(serde_json::from_slice(buf).context("failed to parse JSON")?)
|
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.
|
/// Send a null-terminated JSON instruction on a stream.
|
||||||
pub async fn send_json<T: Serialize>(writer: &mut (impl AsyncWrite + Unpin), msg: T) -> Result<()> {
|
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)?;
|
let msg = serde_json::to_vec(&msg)?;
|
||||||
writer.write_all(&msg).await?;
|
writer.write_all(&msg).await?;
|
||||||
writer.write_all(&[0]).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