mirror of
https://github.com/ekzhang/bore.git
synced 2025-12-16 20:07:51 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
045324d7dc | ||
|
|
a2b8382681 | ||
|
|
9cd43f458a | ||
|
|
e61362915d | ||
|
|
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
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: push
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust:
|
rust:
|
||||||
@@ -44,4 +44,4 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- run: cargo clippy
|
- run: cargo clippy -- -D warnings
|
||||||
|
|||||||
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 }}
|
||||||
123
.github/workflows/mean_bean_ci.yml
vendored
Normal file
123
.github/workflows/mean_bean_ci.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
name: Mean Bean CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
install-cross:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
|
||||||
|
- uses: XAMPPRocky/get-github-release@v1
|
||||||
|
id: cross
|
||||||
|
with:
|
||||||
|
owner: rust-embedded
|
||||||
|
repo: cross
|
||||||
|
matches: ${{ matrix.platform }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cross-${{ matrix.platform }}
|
||||||
|
path: ${{ steps.cross.outputs.install_path }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [linux-musl]
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
channel: [stable]
|
||||||
|
target:
|
||||||
|
- x86_64-apple-darwin
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Setup | Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup | Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
|
||||||
|
- name: Test
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
use-cross: false
|
||||||
|
|
||||||
|
linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: install-cross
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
|
||||||
|
- name: Download Cross
|
||||||
|
uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cross-linux-musl
|
||||||
|
path: /tmp/
|
||||||
|
- run: chmod +x /tmp/cross
|
||||||
|
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
|
||||||
|
- run: ci/build.bash /tmp/cross ${{ matrix.target }}
|
||||||
|
# These targets have issues with being tested so they are disabled
|
||||||
|
# by default. You can try disabling to see if they work for
|
||||||
|
# your project.
|
||||||
|
- run: ci/test.bash /tmp/cross ${{ matrix.target }}
|
||||||
|
if: |
|
||||||
|
!contains(matrix.target, 'android') &&
|
||||||
|
!contains(matrix.target, 'bsd') &&
|
||||||
|
!contains(matrix.target, 'solaris') &&
|
||||||
|
matrix.target != 'armv5te-unknown-linux-musleabi' &&
|
||||||
|
matrix.target != 'sparc64-unknown-linux-gnu'
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
channel: [stable]
|
||||||
|
target:
|
||||||
|
- arm-unknown-linux-gnueabi
|
||||||
|
- armv7-unknown-linux-gnueabihf
|
||||||
|
- i686-unknown-linux-musl
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
# Windows technically doesn't need this, but if we don't block windows on it
|
||||||
|
# some of the windows jobs could fill up the concurrent job queue before
|
||||||
|
# one of the install-cross jobs has started, so this makes sure all
|
||||||
|
# artifacts are downloaded first.
|
||||||
|
needs: install-cross
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
- run: ci/set_rust_version.bash ${{ matrix.channel }} ${{ matrix.target }}
|
||||||
|
shell: bash
|
||||||
|
- run: ci/build.bash cargo ${{ matrix.target }}
|
||||||
|
shell: bash
|
||||||
|
- run: ci/test.bash cargo ${{ matrix.target }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
channel: [stable]
|
||||||
|
target:
|
||||||
|
# MSVC
|
||||||
|
- i686-pc-windows-msvc
|
||||||
|
- x86_64-pc-windows-msvc
|
||||||
|
# GNU: You typically only need to test Windows GNU if you're
|
||||||
|
# specifically targetting it, and it can cause issues with some
|
||||||
|
# dependencies if you're not so it's disabled by self.
|
||||||
|
# - i686-pc-windows-gnu
|
||||||
|
# - x86_64-pc-windows-gnu
|
||||||
192
.github/workflows/mean_bean_deploy.yml
vendored
Normal file
192
.github/workflows/mean_bean_deploy.yml
vendored
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
# # Sequence of patterns matched against refs/tags
|
||||||
|
tags:
|
||||||
|
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
|
name: Mean Bean Deploy
|
||||||
|
env:
|
||||||
|
BIN: bore
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# This job downloads and stores `cross` as an artifact, so that it can be
|
||||||
|
# redownloaded across all of the jobs. Currently this copied pasted between
|
||||||
|
# `mean_bean_ci.yml` and `mean_bean_deploy.yml`. Make sure to update both places when making
|
||||||
|
# changes.
|
||||||
|
install-cross:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
|
||||||
|
- uses: XAMPPRocky/get-github-release@v1
|
||||||
|
id: cross
|
||||||
|
with:
|
||||||
|
owner: rust-embedded
|
||||||
|
repo: cross
|
||||||
|
matches: ${{ matrix.platform }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cross-${{ matrix.platform }}
|
||||||
|
path: ${{ steps.cross.outputs.install_path }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [linux-musl]
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
# macOS
|
||||||
|
- x86_64-apple-darwin
|
||||||
|
- aarch64-apple-darwin
|
||||||
|
steps:
|
||||||
|
- name: Get tag
|
||||||
|
id: tag
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
|
||||||
|
- name: Setup | Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Cache files between builds
|
||||||
|
- name: Setup | Cache Cargo
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Setup | Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build | Build
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
|
||||||
|
- uses: XAMPPRocky/create-release@v1.0.2
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- uses: actions/upload-release-asset@v1
|
||||||
|
id: upload-release-asset
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.BIN }}.tar.gz
|
||||||
|
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
|
||||||
|
asset_content_type: application/gzip
|
||||||
|
|
||||||
|
linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: install-cross
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- arm-unknown-linux-gnueabi
|
||||||
|
- armv7-unknown-linux-gnueabihf
|
||||||
|
- i686-unknown-linux-musl
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
steps:
|
||||||
|
- name: Get tag
|
||||||
|
id: tag
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: cross-linux-musl
|
||||||
|
path: /tmp/
|
||||||
|
- run: chmod +x /tmp/cross
|
||||||
|
|
||||||
|
- run: ci/set_rust_version.bash stable ${{ matrix.target }}
|
||||||
|
- run: ci/build.bash /tmp/cross ${{ matrix.target }} RELEASE
|
||||||
|
- run: tar -czvf ${{ env.BIN }}.tar.gz --directory=target/${{ matrix.target }}/release ${{ env.BIN }}
|
||||||
|
|
||||||
|
- uses: XAMPPRocky/create-release@v1.0.2
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.BIN }}.tar.gz
|
||||||
|
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.tar.gz
|
||||||
|
asset_content_type: application/gzip
|
||||||
|
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs: install-cross
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
# MSVC
|
||||||
|
- i686-pc-windows-msvc
|
||||||
|
- x86_64-pc-windows-msvc
|
||||||
|
# GNU
|
||||||
|
# - i686-pc-windows-gnu
|
||||||
|
# - x86_64-pc-windows-gnu
|
||||||
|
steps:
|
||||||
|
- name: Get tag
|
||||||
|
id: tag
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: bash ci/set_rust_version.bash stable ${{ matrix.target }}
|
||||||
|
- run: bash ci/build.bash cargo ${{ matrix.target }} RELEASE
|
||||||
|
- run: |
|
||||||
|
cd ./target/${{ matrix.target }}/release/
|
||||||
|
7z a "${{ env.BIN }}.zip" "${{ env.BIN }}.exe"
|
||||||
|
mv "${{ env.BIN }}.zip" $GITHUB_WORKSPACE
|
||||||
|
shell: bash
|
||||||
|
# We're using using a fork of `actions/create-release` that detects
|
||||||
|
# whether a release is already available or not first.
|
||||||
|
- uses: XAMPPRocky/create-release@v1.0.2
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
# Draft should **always** be false. GitHub doesn't provide a way to
|
||||||
|
# get draft releases from its API, so there's no point using it.
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
- uses: actions/upload-release-asset@v1
|
||||||
|
id: upload-release-asset
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{ env.BIN }}.zip
|
||||||
|
asset_name: ${{ env.BIN }}-${{steps.tag.outputs.tag}}-${{ matrix.target }}.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
171
Cargo.lock
generated
171
Cargo.lock
generated
@@ -54,9 +54,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.64"
|
version = "0.3.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
|
checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"addr2line",
|
"addr2line",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -84,17 +84,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bore-cli"
|
name = "bore-cli"
|
||||||
version = "0.2.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
"lazy_static",
|
||||||
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -120,16 +124,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.1.8"
|
version = "3.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
|
checksum = "6aad2534fad53df1cc12519c5cda696dd3e20e6118a027e24054aea14a0bdcbe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
"clap_lex",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"os_str_bytes",
|
|
||||||
"strsim",
|
"strsim",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
@@ -148,6 +152,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669"
|
||||||
|
dependencies = [
|
||||||
|
"os_str_bytes",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -189,6 +202,50 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -276,9 +333,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.121"
|
version = "0.2.124"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f"
|
checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -307,12 +364,11 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.4.4"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
|
checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
"autocfg",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -359,9 +415,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.27.1"
|
version = "0.28.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9"
|
checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -377,9 +433,6 @@ name = "os_str_bytes"
|
|||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
@@ -410,6 +463,12 @@ version = "0.2.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error"
|
name = "proc-macro-error"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -436,18 +495,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",
|
||||||
]
|
]
|
||||||
@@ -461,12 +520,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"
|
||||||
@@ -479,6 +560,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"
|
||||||
@@ -531,13 +618,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "slab"
|
||||||
version = "1.4.0"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
@@ -569,9 +653,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.90"
|
version = "1.0.91"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "704df27628939572cd88d33f171cd6f896f4eaca85252c6e0a72d8d8287ee86f"
|
checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -613,10 +697,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",
|
||||||
@@ -634,10 +715,24 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tokio-util"
|
||||||
version = "0.1.32"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f"
|
checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -658,9 +753,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.24"
|
version = "0.1.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee"
|
checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"valuable",
|
"valuable",
|
||||||
@@ -679,9 +774,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",
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bore-cli"
|
name = "bore-cli"
|
||||||
version = "0.2.0"
|
version = "0.4.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,14 +17,21 @@ 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"
|
||||||
|
futures-util = { version = "0.3.21", features = ["sink"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hmac = "0.12.1"
|
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"
|
||||||
sha2 = "0.10.2"
|
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"] }
|
||||||
|
tokio-util = { version = "0.7.1", features = ["codec"] }
|
||||||
tracing = "0.1.32"
|
tracing = "0.1.32"
|
||||||
tracing-subscriber = "0.3.10"
|
tracing-subscriber = "0.3.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.
|
||||||
43
README.md
43
README.md
@@ -8,7 +8,7 @@ A modern, simple TCP tunnel in Rust that exposes local ports to a remote server,
|
|||||||

|

|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Installation (requires Rust)
|
# Installation (requires Rust, see alternatives below)
|
||||||
cargo install bore-cli
|
cargo install bore-cli
|
||||||
|
|
||||||
# On your local machine
|
# On your local machine
|
||||||
@@ -21,6 +21,22 @@ 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.)
|
(`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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
This section describes detailed usage for the `bore` CLI command.
|
This section describes detailed usage for the `bore` CLI command.
|
||||||
@@ -33,31 +49,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.2.0
|
bore-local 0.4.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]
|
||||||
-s, --secret <SECRET> Optional secret for authentication
|
-p, --port <PORT> Optional port on the remote server to select [default: 0]
|
||||||
-t, --to <TO> Address of the remote server to expose local ports to
|
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||||
-V, --version Print version information
|
-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
|
||||||
@@ -68,7 +85,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.2.0
|
bore-server 0.4.0
|
||||||
Runs the remote proxy server
|
Runs the remote proxy server
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
@@ -77,7 +94,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
|
-s, --secret <SECRET> Optional secret for authentication [env: BORE_SECRET]
|
||||||
-V, --version Print version information
|
-V, --version Print version information
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -101,6 +118,8 @@ bore server --secret my_secret_string
|
|||||||
bore local <LOCAL_PORT> --to <TO> --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
|
## 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).
|
||||||
|
|||||||
22
ci/build.bash
Executable file
22
ci/build.bash
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script for building your rust projects.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
source ci/common.bash
|
||||||
|
|
||||||
|
# $1 {path} = Path to cross/cargo executable
|
||||||
|
CROSS=$1
|
||||||
|
# $1 {string} = <Target Triple> e.g. x86_64-pc-windows-msvc
|
||||||
|
TARGET_TRIPLE=$2
|
||||||
|
# $3 {boolean} = Are we building for deployment?
|
||||||
|
RELEASE_BUILD=$3
|
||||||
|
|
||||||
|
required_arg $CROSS 'CROSS'
|
||||||
|
required_arg $TARGET_TRIPLE '<Target Triple>'
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_BUILD" ]; then
|
||||||
|
$CROSS build --target $TARGET_TRIPLE
|
||||||
|
$CROSS build --target $TARGET_TRIPLE --all-features
|
||||||
|
else
|
||||||
|
$CROSS build --target $TARGET_TRIPLE --all-features --release
|
||||||
|
fi
|
||||||
6
ci/common.bash
Normal file
6
ci/common.bash
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
required_arg() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Required argument $2 missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
4
ci/set_rust_version.bash
Executable file
4
ci/set_rust_version.bash
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
rustup default $1
|
||||||
|
rustup target add $2
|
||||||
16
ci/test.bash
Executable file
16
ci/test.bash
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Script for building your rust projects.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
source ci/common.bash
|
||||||
|
|
||||||
|
# $1 {path} = Path to cross/cargo executable
|
||||||
|
CROSS=$1
|
||||||
|
# $1 {string} = <Target Triple>
|
||||||
|
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
|
||||||
20
src/auth.rs
20
src/auth.rs
@@ -3,10 +3,10 @@
|
|||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::io::{AsyncBufRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::shared::{recv_json, send_json, ClientMessage, ServerMessage};
|
use crate::shared::{ClientMessage, Delimited, ServerMessage};
|
||||||
|
|
||||||
/// Wrapper around a MAC used for authenticating clients that have a secret.
|
/// Wrapper around a MAC used for authenticating clients that have a secret.
|
||||||
pub struct Authenticator(Hmac<Sha256>);
|
pub struct Authenticator(Hmac<Sha256>);
|
||||||
@@ -48,13 +48,13 @@ impl Authenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// As the server, send a challenge to the client and validate their response.
|
/// As the server, send a challenge to the client and validate their response.
|
||||||
pub async fn server_handshake(
|
pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
&self,
|
&self,
|
||||||
stream: &mut (impl AsyncBufRead + AsyncWrite + Unpin),
|
stream: &mut Delimited<T>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let challenge = Uuid::new_v4();
|
let challenge = Uuid::new_v4();
|
||||||
send_json(stream, ServerMessage::Challenge(challenge)).await?;
|
stream.send(ServerMessage::Challenge(challenge)).await?;
|
||||||
match recv_json(stream, &mut Vec::new()).await? {
|
match stream.recv_timeout().await? {
|
||||||
Some(ClientMessage::Authenticate(tag)) => {
|
Some(ClientMessage::Authenticate(tag)) => {
|
||||||
ensure!(self.validate(&challenge, &tag), "invalid secret");
|
ensure!(self.validate(&challenge, &tag), "invalid secret");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -64,16 +64,16 @@ impl Authenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// As the client, answer a challenge to attempt to authenticate with the server.
|
/// As the client, answer a challenge to attempt to authenticate with the server.
|
||||||
pub async fn client_handshake(
|
pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
&self,
|
&self,
|
||||||
stream: &mut (impl AsyncBufRead + AsyncWrite + Unpin),
|
stream: &mut Delimited<T>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let challenge = match recv_json(stream, &mut Vec::new()).await? {
|
let challenge = match stream.recv_timeout().await? {
|
||||||
Some(ServerMessage::Challenge(challenge)) => challenge,
|
Some(ServerMessage::Challenge(challenge)) => challenge,
|
||||||
_ => bail!("expected authentication challenge, but no secret was required"),
|
_ => bail!("expected authentication challenge, but no secret was required"),
|
||||||
};
|
};
|
||||||
let tag = self.answer(&challenge);
|
let tag = self.answer(&challenge);
|
||||||
send_json(stream, ClientMessage::Authenticate(tag)).await?;
|
stream.send(ClientMessage::Authenticate(tag)).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,28 @@
|
|||||||
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::AsyncWriteExt;
|
||||||
|
use tokio::{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::auth::Authenticator;
|
use crate::auth::Authenticator;
|
||||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
use crate::shared::{
|
||||||
|
proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT,
|
||||||
|
};
|
||||||
|
|
||||||
/// State structure for the client.
|
/// State structure for the client.
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
/// Control connection to the server.
|
/// Control connection to the server.
|
||||||
conn: Option<BufReader<TcpStream>>,
|
conn: Option<Delimited<TcpStream>>,
|
||||||
|
|
||||||
/// 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,
|
||||||
|
|
||||||
@@ -30,19 +37,21 @@ pub struct Client {
|
|||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client.
|
/// Create a new client.
|
||||||
pub async fn new(local_port: u16, to: &str, port: u16, secret: Option<&str>) -> Result<Self> {
|
pub async fn new(
|
||||||
let stream = TcpStream::connect((to, CONTROL_PORT))
|
local_host: &str,
|
||||||
.await
|
local_port: u16,
|
||||||
.with_context(|| format!("could not connect to {to}:{CONTROL_PORT}"))?;
|
to: &str,
|
||||||
let mut stream = BufReader::new(stream);
|
port: u16,
|
||||||
|
secret: Option<&str>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut stream = Delimited::new(connect_with_timeout(to, CONTROL_PORT).await?);
|
||||||
let auth = secret.map(Authenticator::new);
|
let auth = secret.map(Authenticator::new);
|
||||||
if let Some(auth) = &auth {
|
if let Some(auth) = &auth {
|
||||||
auth.client_handshake(&mut stream).await?;
|
auth.client_handshake(&mut stream).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_json(&mut stream, ClientMessage::Hello(port)).await?;
|
stream.send(ClientMessage::Hello(port)).await?;
|
||||||
let remote_port = match recv_json(&mut stream, &mut Vec::new()).await? {
|
let remote_port = match stream.recv_timeout().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(_)) => {
|
Some(ServerMessage::Challenge(_)) => {
|
||||||
@@ -57,6 +66,7 @@ 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,
|
auth,
|
||||||
@@ -72,10 +82,8 @@ impl Client {
|
|||||||
pub async fn listen(mut self) -> Result<()> {
|
pub async fn listen(mut self) -> Result<()> {
|
||||||
let mut conn = self.conn.take().unwrap();
|
let mut conn = self.conn.take().unwrap();
|
||||||
let this = Arc::new(self);
|
let this = Arc::new(self);
|
||||||
let mut buf = Vec::new();
|
|
||||||
loop {
|
loop {
|
||||||
let msg = recv_json(&mut conn, &mut buf).await?;
|
match conn.recv().await? {
|
||||||
match msg {
|
|
||||||
Some(ServerMessage::Hello(_)) => warn!("unexpected hello"),
|
Some(ServerMessage::Hello(_)) => warn!("unexpected hello"),
|
||||||
Some(ServerMessage::Challenge(_)) => warn!("unexpected challenge"),
|
Some(ServerMessage::Challenge(_)) => warn!("unexpected challenge"),
|
||||||
Some(ServerMessage::Heartbeat) => (),
|
Some(ServerMessage::Heartbeat) => (),
|
||||||
@@ -99,21 +107,25 @@ 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
|
Delimited::new(connect_with_timeout(&self.to[..], CONTROL_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")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(auth) = &self.auth {
|
if let Some(auth) = &self.auth {
|
||||||
auth.client_handshake(&mut remote_conn).await?;
|
auth.client_handshake(&mut remote_conn).await?;
|
||||||
}
|
}
|
||||||
|
remote_conn.send(ClientMessage::Accept(id)).await?;
|
||||||
send_json(&mut remote_conn, ClientMessage::Accept(id)).await?;
|
let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;
|
||||||
proxy(local_conn, remote_conn).await?;
|
let 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?;
|
||||||
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}"))
|
||||||
|
}
|
||||||
|
|||||||
25
src/main.rs
25
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,
|
||||||
@@ -26,7 +30,7 @@ enum Command {
|
|||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
/// Optional secret for authentication.
|
/// Optional secret for authentication.
|
||||||
#[clap(short, long)]
|
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||||
secret: Option<String>,
|
secret: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -37,24 +41,22 @@ enum Command {
|
|||||||
min_port: u16,
|
min_port: u16,
|
||||||
|
|
||||||
/// Optional secret for authentication.
|
/// Optional secret for authentication.
|
||||||
#[clap(short, long)]
|
#[clap(short, long, env = "BORE_SECRET", hide_env_values = true)]
|
||||||
secret: Option<String>,
|
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,
|
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?;
|
client.listen().await?;
|
||||||
}
|
}
|
||||||
Command::Server { min_port, secret } => {
|
Command::Server { min_port, secret } => {
|
||||||
@@ -64,3 +66,8 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
run(Args::parse().command)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use tokio::io::BufReader;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::time::{sleep, timeout};
|
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::auth::Authenticator;
|
use crate::auth::Authenticator;
|
||||||
use crate::shared::{proxy, recv_json, send_json, ClientMessage, ServerMessage, CONTROL_PORT};
|
use crate::shared::{proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT};
|
||||||
|
|
||||||
/// State structure for the server.
|
/// State structure for the server.
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
@@ -62,19 +62,16 @@ 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 = Delimited::new(stream);
|
||||||
if let Some(auth) = &self.auth {
|
if let Some(auth) = &self.auth {
|
||||||
if let Err(err) = auth.server_handshake(&mut stream).await {
|
if let Err(err) = auth.server_handshake(&mut stream).await {
|
||||||
warn!(%err, "server handshake failed");
|
warn!(%err, "server handshake failed");
|
||||||
send_json(&mut stream, ServerMessage::Error(err.to_string())).await?;
|
stream.send(ServerMessage::Error(err.to_string())).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
match stream.recv_timeout().await? {
|
||||||
let msg = recv_json(&mut stream, &mut buf).await?;
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
Some(ClientMessage::Authenticate(_)) => {
|
Some(ClientMessage::Authenticate(_)) => {
|
||||||
warn!("unexpected authenticate");
|
warn!("unexpected authenticate");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -85,26 +82,21 @@ impl Server {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
info!(?port, "new client");
|
info!(?port, "new client");
|
||||||
let listener = match TcpListener::bind(("::", port)).await {
|
let listener = match TcpListener::bind(("0.0.0.0", port)).await {
|
||||||
Ok(listener) => listener,
|
Ok(listener) => listener,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
warn!(?port, "could not bind to local port");
|
warn!(?port, "could not bind to local port");
|
||||||
send_json(
|
stream
|
||||||
&mut stream,
|
.send(ServerMessage::Error("port already in use".into()))
|
||||||
ServerMessage::Error("port already in use".into()),
|
.await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let port = listener.local_addr()?.port();
|
let port = listener.local_addr()?.port();
|
||||||
send_json(&mut stream, ServerMessage::Hello(port)).await?;
|
stream.send(ServerMessage::Hello(port)).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if send_json(&mut stream, ServerMessage::Heartbeat)
|
if stream.send(ServerMessage::Heartbeat).await.is_err() {
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
// Assume that the TCP connection has been dropped.
|
// Assume that the TCP connection has been dropped.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -124,14 +116,19 @@ impl Server {
|
|||||||
warn!(%id, "removed stale connection");
|
warn!(%id, "removed stale connection");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
send_json(&mut stream, ServerMessage::Connection(id)).await?;
|
stream.send(ServerMessage::Connection(id)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(ClientMessage::Accept(id)) => {
|
Some(ClientMessage::Accept(id)) => {
|
||||||
info!(%id, "forwarding connection");
|
info!(%id, "forwarding connection");
|
||||||
match self.conns.remove(&id) {
|
match self.conns.remove(&id) {
|
||||||
Some((_, stream2)) => proxy(stream, stream2).await?,
|
Some((_, mut stream2)) => {
|
||||||
|
let 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?
|
||||||
|
}
|
||||||
None => warn!(%id, "missing connection"),
|
None => warn!(%id, "missing connection"),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,14 +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 futures_util::{SinkExt, StreamExt};
|
||||||
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, AsyncRead, AsyncWrite};
|
||||||
|
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Maxmium byte length for a JSON frame in the stream.
|
||||||
|
pub const MAX_FRAME_LENGTH: usize = 256;
|
||||||
|
|
||||||
|
/// 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(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
@@ -41,6 +54,52 @@ pub enum ServerMessage {
|
|||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transport stream with JSON frames delimited by null characters.
|
||||||
|
pub struct Delimited<U>(Framed<U, AnyDelimiterCodec>);
|
||||||
|
|
||||||
|
impl<U: AsyncRead + AsyncWrite + Unpin> Delimited<U> {
|
||||||
|
/// Construct a new delimited stream.
|
||||||
|
pub fn new(stream: U) -> Self {
|
||||||
|
let codec = AnyDelimiterCodec::new_with_max_length(vec![0], vec![0], MAX_FRAME_LENGTH);
|
||||||
|
Self(Framed::new(stream, codec))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next null-delimited JSON instruction from a stream.
|
||||||
|
pub async fn recv<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
|
||||||
|
trace!("waiting to receive json message");
|
||||||
|
if let Some(next_message) = self.0.next().await {
|
||||||
|
let byte_message = next_message.context("frame error, invalid byte length")?;
|
||||||
|
let serialized_obj = serde_json::from_slice(&byte_message.to_vec())
|
||||||
|
.context("unable to parse message")?;
|
||||||
|
Ok(serialized_obj)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_timeout<T: DeserializeOwned>(&mut self) -> Result<Option<T>> {
|
||||||
|
timeout(NETWORK_TIMEOUT, self.recv())
|
||||||
|
.await
|
||||||
|
.context("timed out waiting for initial message")?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a null-terminated JSON instruction on a stream.
|
||||||
|
pub async fn send<T: Serialize>(&mut self, msg: T) -> Result<()> {
|
||||||
|
trace!("sending json message");
|
||||||
|
self.0.send(serde_json::to_string(&msg)?).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this object, returning current buffers and the inner transport.
|
||||||
|
pub fn into_parts(self) -> FramedParts<U, AnyDelimiterCodec> {
|
||||||
|
self.0.into_parts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy data mutually between two read/write streams.
|
/// Copy data mutually between two read/write streams.
|
||||||
pub async fn proxy<S1, S2>(stream1: S1, stream2: S2) -> io::Result<()>
|
pub async fn proxy<S1, S2>(stream1: S1, stream2: S2) -> io::Result<()>
|
||||||
where
|
where
|
||||||
@@ -49,33 +108,9 @@ 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the next null-delimited JSON instruction from a stream.
|
|
||||||
pub async fn recv_json<T: DeserializeOwned>(
|
|
||||||
reader: &mut (impl AsyncBufRead + Unpin),
|
|
||||||
buf: &mut Vec<u8>,
|
|
||||||
) -> Result<Option<T>> {
|
|
||||||
buf.clear();
|
|
||||||
reader.read_until(0, buf).await?;
|
|
||||||
if buf.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
if buf.last() == Some(&0) {
|
|
||||||
buf.pop();
|
|
||||||
}
|
|
||||||
Ok(serde_json::from_slice(buf).context("failed to parse JSON")?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a null-terminated JSON instruction on a stream.
|
|
||||||
pub async fn send_json<T: Serialize>(writer: &mut (impl AsyncWrite + Unpin), msg: T) -> Result<()> {
|
|
||||||
let msg = serde_json::to_vec(&msg)?;
|
|
||||||
writer.write_all(&msg).await?;
|
|
||||||
writer.write_all(&[0]).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
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, shared::Delimited};
|
||||||
|
use tokio::io::{self};
|
||||||
|
|
||||||
|
#[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 = Delimited::new(client);
|
||||||
|
let mut server = Delimited::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 = Delimited::new(client);
|
||||||
|
let mut server = Delimited::new(server);
|
||||||
|
|
||||||
|
let result = tokio::try_join!(
|
||||||
|
auth.client_handshake(&mut client),
|
||||||
|
auth2.server_handshake(&mut server),
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
119
tests/e2e_test.rs
Normal file
119
tests/e2e_test.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use bore_cli::{client::Client, server::Server, shared::CONTROL_PORT};
|
||||||
|
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 = ([127, 0, 0, 1], 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn very_long_frame() -> Result<()> {
|
||||||
|
let _guard = SERIAL_GUARD.lock().await;
|
||||||
|
|
||||||
|
spawn_server(None).await;
|
||||||
|
let mut attacker = TcpStream::connect(("localhost", CONTROL_PORT)).await?;
|
||||||
|
|
||||||
|
// Slowly send a very long frame.
|
||||||
|
for _ in 0..10 {
|
||||||
|
let result = attacker.write_all(&[42u8; 100000]).await;
|
||||||
|
if result.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
panic!("did not exit after a 1 MB frame");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user