Closes the M4 plan. When `OPTION_USE_RAW_TCP_FOR_API=Y` (typical in
locked-down networks where direct HTTPS to port 21114 is blocked), the
client wraps every /api/* request in an HttpProxyRequest protobuf and
ships it over the already-encrypted rendezvous TCP channel. We now decode
those messages on hbbs and dispatch them through the *same* axum Router
the HTTPS listener uses — so every existing handler (login, AB, audit,
TOTP, OIDC, devices/cli, plugin-sign, …) is reachable through this path
with zero per-route plumbing.
Components
==========
- libs/hbb_common (submodule, pro-features-httpproxy branch): backports
HeaderEntry / HttpProxyRequest / HttpProxyResponse + union tags 27/28
from upstream @87b11a7 onto our pinned @83419b6. Proto-only — the rest
of hbb_common is unchanged so we keep the tokio 1.x / axum 0.5 / pinned
reqwest fork intact (a full submodule bump risked breaking those).
- src/api/http_proxy.rs: the dispatch shim. Holds a `Mutex<Option<Router>>`
populated by `api::serve` before the HTTPS listener starts, builds an
`http::Request<Body>` from the proto fields (sanitizing hop-by-hop
headers, defaulting Content-Type: application/json), runs it through
`router.oneshot(req)`, and serializes the response into HttpProxyResponse.
Tower added as a direct dep with the `util` feature for ServiceExt.
- src/api/mod.rs: pub mod http_proxy; install_router(app.clone()) before
axum::Server::bind to share the router.
- src/rendezvous_server.rs::handle_tcp: new match arm right before the
catch-all that decodes HttpProxyRequest and replies with an
HttpProxyResponse via the existing Sink::TcpStream(..., Encrypt) path.
The reply is automatically secretbox-sealed by `send_to_sink`, so the
end-to-end channel is encrypted symmetrically with secure_tcp.
- examples/http_proxy_test.rs: end-to-end smoke test that opens a TCP
connection, walks the secure_tcp handshake by hand (read server's
signed box pubkey, derive symmetric key, send sealed reply), then
ships an HttpProxyRequest GET /api/login-options and verifies the
response is 200 + ["account"]. Used as the validation gate.
New crate deps
==============
- tower = "0.4" (features = ["util"]) — for ServiceExt::oneshot
- http-body = "0.4" — for the Body trait import in dispatch
Verification
============
1. cargo build --release — clean.
2. examples/http_proxy_test against a fresh hbbs:
[ok] secure_tcp handshake complete
[ok] sent HttpProxyRequest GET /api/login-options
[ok] response status = 200
[ok] response body = ["account"]
[pass] full HTTP-over-rendezvous round trip verified
3. hbbs log confirms the secure_tcp handshake completed and the dispatch
went through the standard axum router.
Notes on cherry-pick vs submodule bump
======================================
The plan flagged the bump as the riskiest M4 item because newer
hbb_common pulls newer tokio that breaks axum 0.5. The proto-only cherry
pick keeps everything stable; the upstream-divergence cost is one extra
commit in the hbb_common submodule that we own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>