Skip to content

feat: multi-tenant serve, core library split, crates.io publishing - #2

Merged
single9 merged 24 commits into
mainfrom
feat/multi-tenant-serve
Oct 23, 2026
Merged

single9 merged 24 commits into
mainfrom
feat/multi-tenant-serve

Conversation

@single9

@single9 single9 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a multi-tenant mode to serve: many users on one server, each with their own token and hostname, routed by TLS SNI without decrypting the traffic. Splits the repo into an erbridge-core library and an erbridge binary crate, and adds CI plus crates.io publishing to the release workflow.

  • Multi-tenant serve (serve --tenants FILE --sni-listen ADDR): the Noise handshake tries each tenant's PSK, so a client never states who it is and the wire format is unchanged. The tunnel name is chosen by serve, so a client cannot reach another tenant's target. Per-tenant max_conns, live reload of the tenant file, and a new session replaces the tenant's old one.
  • Enrollment: serve-admin issue makes a single-use code, enroll trades it for credentials, connect --credentials uses them. serve-admin also has list, disable, enable and revoke.
  • Library: erbridge-core has no CLI/TUI dependencies; run_serve_with_shutdown / run_connect_with_shutdown stop cleanly on a CancellationToken; Registry::subscribe gives structured events; TenantStore lets a host keep tenants elsewhere than a file.
  • Release: ci.yml (fmt, clippy, tests on Linux and macOS, MSRV, publish dry run, release-script tests); release.yml picks the next version from Conventional Commits (auto, still overridable) and publishes both crates to crates.io after the GitHub release.

Design and the ten tickets are in docs/multi-tenant/.

Compatibility

Old and new builds interoperate; I checked v0.2.0 binaries against this branch in both directions for serve/connect (single-tenant and multi-tenant) and forward/client over noise. The CLI and config files are unchanged. Library users: the crate is now erbridge_core, and ServeConfig gained two fields (see "Upgrading" in the README). Release as minor (0.3.0).

Test plan

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace (core integration tests with real TLS clients and servers, plus end-to-end tests that run the real binary)
  • cargo publish --workspace --dry-run
  • Manual run of enroll / route / limit / disable / revoke with openssl and curl
  • CI on GitHub (not run yet: macOS, the 1.89 MSRV job, shellcheck)
  • Release workflow with publish unticked, after CARGO_REGISTRY_TOKEN is set

…nnect

The clap, ratatui and crossterm dependencies now sit behind a default-on
`cli` feature so embedders can build the tunnel library without them.

run_serve_with_shutdown and run_connect_with_shutdown take a
CancellationToken and return once every listener, session and tunnelled
connection has been stopped. run_serve and run_connect are unchanged.
…crate

The command-line interface and terminal UI move to the erbridge crate, so
embedders depend on erbridge-core and pull in neither clap nor ratatui.
This replaces the `cli` feature gate, which could be re-enabled by feature
unification elsewhere in a build graph.

Version now lives in [workspace.package]; the release workflow bumps both
crates' Cargo.lock entries. Install with `cargo install --path crates/erbridge`.
… tenant file write

- time out the Noise handshake after 5 s and cap unauthenticated
  control connections at 256
- compare PSKs in constant time everywhere
- Tenant, EnrollCode and Credentials no longer print their secret in Debug
- remove a stale or failed temp file before and after writing the tenant file
- refuse to issue an enrollment code for a host a tenant already holds
- tenant_serve locks recover from poisoning instead of panicking every later connection
- connections refused for lack of handshake slots are logged once and then every thousandth
…disable race, pin actions

- one address may hold at most 16 handshake slots
- a tenant whose credentials could not be delivered is withdrawn again
- a session registered after its tenant was removed or disabled is ended at once
- redeeming a code also drops expired codes
- pin every action to a commit SHA, add concurrency and an actionlint step
- spec: describe reload as a once-a-second re-read
… constant time

Tenant, EnrollCode, the candidate list and the noise API take a Psk instead of
a bare [u8; 32]; the hand-written Debug impls and constant_time_eq calls for
PSKs are no longer needed.
@single9
single9 merged commit 2936ebd into main Oct 23, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant