9 Commits

Author SHA1 Message Date
Eric Zhang
045324d7dc Bump version to 0.4.0 2022-04-22 18:14:05 -04:00
Praveen Perera
a2b8382681 GitHub actions CI/CD releases (#10)
* Add GitHub CI/CD

* Add instructions for installing bin from GitHub releases

* Will work after a new version is released

* Add windows jobs for CI/CD

* Only run CI on stable rust

* Use `cargo fmt` instead of `rustfmt`, undo change

* Delete install.sh

* Update README.md

Co-authored-by: Eric Zhang <ekzhang1@gmail.com>
2022-04-22 18:04:26 -04:00
Prasanth
9cd43f458a use framed codecs to avoid unbounded buffer (#33)
* using stream

* fix tests

* 💄

* 💄 fix review comments

* clean up buffered data

* 💄 fix review comments

* Refactor Delimited to be its own struct

* Add very_long_frame test to ensure behavior

Co-authored-by: Eric Zhang <ekzhang1@gmail.com>
2022-04-21 23:48:38 -04:00
B.O.S.S
e61362915d Fix Windows cargo test (#35) 2022-04-21 00:17:14 -04:00
Eric Zhang
e25f021505 Bump version to 0.3.0 2022-04-14 15:24:09 -04:00
Orhun Parmaksız
cae08bb3c2 Support reading client/server secret from an environment variable (#18) 2022-04-14 14:40:52 -04:00
Basti Ortiz
b045d8028e Deps: minimize Tokio features (#25)
* Deps: use specific Tokio features

* Deps: run `cargo update`

* Refactor: move CLI-parsing outside Tokio runtime

* Fix: use `parse` over `try_parse`

Clap does special things behind the scenes before it exits.

* Refactor: use `tokio::main` macro for convenience
2022-04-14 14:39:46 -04:00
Eric Zhang
36a56c0d4a Run CI workflow on both push and pull_request 2022-04-14 14:33:31 -04:00
Antonio Mika
99fc4f7ddb Add a configuration to allow forward to a different local host (#15)
* Add a configuration to allow forward to a different local host

* Minor documentation / formatting change

Co-authored-by: Eric Zhang <ekzhang1@gmail.com>
2022-04-11 23:45:07 -04:00
17 changed files with 644 additions and 160 deletions

View File

@@ -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

123
.github/workflows/mean_bean_ci.yml vendored Normal file
View 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
View 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

141
Cargo.lock generated
View File

@@ -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,11 +84,12 @@ dependencies = [
[[package]] [[package]]
name = "bore-cli" name = "bore-cli"
version = "0.2.3" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"dashmap", "dashmap",
"futures-util",
"hex", "hex",
"hmac", "hmac",
"lazy_static", "lazy_static",
@@ -97,6 +98,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"tokio", "tokio",
"tokio-util",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
@@ -122,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",
@@ -150,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"
@@ -191,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"
@@ -278,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"
@@ -309,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]]
@@ -361,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",
] ]
@@ -379,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"
@@ -412,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"
@@ -438,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",
] ]
@@ -561,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"
@@ -599,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",
@@ -643,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",
@@ -664,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",
@@ -688,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",
@@ -709,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",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bore-cli" name = "bore-cli"
version = "0.2.3" 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,16 @@ 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"] }
@@ -32,3 +34,4 @@ uuid = { version = "0.8.2", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0"
rstest = "0.12.0" rstest = "0.12.0"
tokio = { version = "1.17.0", features = ["sync"] }

View File

@@ -8,7 +8,7 @@ A modern, simple TCP tunnel in Rust that exposes local ports to a remote server,
![Video demo](https://i.imgur.com/vDeGsmx.gif) ![Video demo](https://i.imgur.com/vDeGsmx.gif)
```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
@@ -23,7 +23,9 @@ Similar to [localtunnel](https://github.com/localtunnel/localtunnel) and [ngrok]
## Installation ## 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. 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 ```shell
cargo install bore-cli cargo install bore-cli
@@ -47,26 +49,27 @@ 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.3 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
@@ -82,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.3 bore-server 0.4.0
Runs the remote proxy server Runs the remote proxy server
USAGE: USAGE:
@@ -91,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
``` ```
@@ -115,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
View 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
View 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
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -e
rustup default $1
rustup target add $2

16
ci/test.bash Executable file
View 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

View File

@@ -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_timeout, 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_timeout(stream).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_timeout(stream).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(())
} }
} }

View File

@@ -3,24 +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, time::timeout};
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::{ use crate::shared::{
proxy, recv_json, recv_json_timeout, send_json, ClientMessage, ServerMessage, CONTROL_PORT, proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT, NETWORK_TIMEOUT,
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,
@@ -33,16 +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 mut stream = BufReader::new(connect_with_timeout(to, CONTROL_PORT).await?); local_host: &str,
local_port: u16,
to: &str,
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_timeout(&mut stream).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) => (),
@@ -100,14 +108,16 @@ impl Client {
async fn handle_connection(&self, id: Uuid) -> Result<()> { async fn handle_connection(&self, id: Uuid) -> Result<()> {
let mut remote_conn = let mut remote_conn =
BufReader::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?); Delimited::new(connect_with_timeout(&self.to[..], CONTROL_PORT).await?);
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?;
} }
send_json(&mut remote_conn, ClientMessage::Accept(id)).await?; remote_conn.send(ClientMessage::Accept(id)).await?;
let mut local_conn = connect_with_timeout(&self.local_host, self.local_port).await?;
let local_conn = connect_with_timeout("localhost", self.local_port).await?; let parts = remote_conn.into_parts();
proxy(local_conn, remote_conn).await?; 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(())
} }
} }

View File

@@ -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)
}

View File

@@ -6,16 +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::{ use crate::shared::{proxy, ClientMessage, Delimited, ServerMessage, CONTROL_PORT};
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 {
@@ -64,16 +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(());
} }
} }
match recv_json_timeout(&mut stream).await? { match stream.recv_timeout().await? {
Some(ClientMessage::Authenticate(_)) => { Some(ClientMessage::Authenticate(_)) => {
warn!("unexpected authenticate"); warn!("unexpected authenticate");
Ok(()) Ok(())
@@ -84,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(());
} }
@@ -123,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(())

View File

@@ -3,16 +3,22 @@
use std::time::Duration; 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::time::timeout;
use tokio_util::codec::{AnyDelimiterCodec, Framed, FramedParts};
use tracing::trace; 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. /// Timeout for network connections and initial protocol messages.
pub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3); pub const NETWORK_TIMEOUT: Duration = Duration::from_secs(3);
@@ -48,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
@@ -62,41 +114,3 @@ where
}?; }?;
Ok(()) 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>> {
trace!("waiting to receive json message");
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")?)
}
/// Read the next null-delimited JSON instruction, with a default timeout.
///
/// This is useful for parsing the initial message of a stream for handshake or
/// other protocol purposes, where we do not want to wait indefinitely.
pub async fn recv_json_timeout<T: DeserializeOwned>(
reader: &mut (impl AsyncBufRead + Unpin),
) -> Result<Option<T>> {
timeout(NETWORK_TIMEOUT, recv_json(reader, &mut Vec::new()))
.await
.context("timed out waiting for initial message")?
}
/// Send a null-terminated JSON instruction on a stream.
pub async fn send_json<T: Serialize>(writer: &mut (impl AsyncWrite + Unpin), msg: T) -> Result<()> {
trace!("sending json message");
let msg = serde_json::to_vec(&msg)?;
writer.write_all(&msg).await?;
writer.write_all(&[0]).await?;
Ok(())
}

View File

@@ -1,14 +1,14 @@
use anyhow::Result; use anyhow::Result;
use bore_cli::auth::Authenticator; use bore_cli::{auth::Authenticator, shared::Delimited};
use tokio::io::{self, BufReader}; use tokio::io::{self};
#[tokio::test] #[tokio::test]
async fn auth_handshake() -> Result<()> { async fn auth_handshake() -> Result<()> {
let auth = Authenticator::new("some secret string"); let auth = Authenticator::new("some secret string");
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity. let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
let mut client = BufReader::new(client); let mut client = Delimited::new(client);
let mut server = BufReader::new(server); let mut server = Delimited::new(server);
tokio::try_join!( tokio::try_join!(
auth.client_handshake(&mut client), auth.client_handshake(&mut client),
@@ -24,8 +24,8 @@ async fn auth_handshake_fail() {
let auth2 = Authenticator::new("different server secret"); let auth2 = Authenticator::new("different server secret");
let (client, server) = io::duplex(8); // Ensure correctness with limited capacity. let (client, server) = io::duplex(8); // Ensure correctness with limited capacity.
let mut client = BufReader::new(client); let mut client = Delimited::new(client);
let mut server = BufReader::new(server); let mut server = Delimited::new(server);
let result = tokio::try_join!( let result = tokio::try_join!(
auth.client_handshake(&mut client), auth.client_handshake(&mut client),

View File

@@ -2,7 +2,7 @@ use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use bore_cli::{client::Client, server::Server}; use bore_cli::{client::Client, server::Server, shared::CONTROL_PORT};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rstest::*; use rstest::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -24,8 +24,9 @@ async fn spawn_server(secret: Option<&str>) {
/// Spawns a client with randomly assigned ports, returning the listener and remote address. /// Spawns a client with randomly assigned ports, returning the listener and remote address.
async fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, SocketAddr)> { async fn spawn_client(secret: Option<&str>) -> Result<(TcpListener, SocketAddr)> {
let listener = TcpListener::bind("localhost:0").await?; let listener = TcpListener::bind("localhost:0").await?;
let client = Client::new(listener.local_addr()?.port(), "localhost", 0, secret).await?; let local_port = listener.local_addr()?.port();
let remote_addr = ([0, 0, 0, 0], client.remote_port()).into(); 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()); tokio::spawn(client.listen());
Ok((listener, remote_addr)) Ok((listener, remote_addr))
} }
@@ -83,7 +84,7 @@ async fn mismatched_secret(
async fn invalid_address() -> Result<()> { async fn invalid_address() -> Result<()> {
// We don't need the serial guard for this test because it doesn't create a server. // 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<()> { async fn check_address(to: &str, use_secret: bool) -> Result<()> {
match Client::new(5000, to, 0, use_secret.then(|| "a secret")).await { match Client::new("localhost", 5000, to, 0, use_secret.then(|| "a secret")).await {
Ok(_) => Err(anyhow!("expected error for {to}, use_secret={use_secret}")), Ok(_) => Err(anyhow!("expected error for {to}, use_secret={use_secret}")),
Err(_) => Ok(()), Err(_) => Ok(()),
} }
@@ -98,3 +99,21 @@ async fn invalid_address() -> Result<()> {
)?; )?;
Ok(()) 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");
}