[Add] TCP server to core MTP (HTTP/1.1 & HTTP/2) compatibility
All checks were successful
CI / checks (push) Successful in 5m29s

This commit is contained in:
Alex Emmet 2026-07-21 00:43:00 +02:00
commit 00f0aaeeff
21 changed files with 1627 additions and 677 deletions

View file

@ -24,7 +24,7 @@ MTP separates wire encoding, QUIC transport, connection policy, protocol negotia
┌─────────────────┴─────────────────┐
│ │
MTPHost MTPWebServer
native QUIC HTTP/3 + WebTransport
native QUIC HTTPS + HTTP/3 + WebTransport
│ │
└──────────────┬────────────────────┘
@ -40,10 +40,10 @@ Both clients exchange the same MTP frames with a host.
The middle row is shared protocol machinery. The type map determines numeric IDs, the codec serializes values, and transport framing places each serialized frame on a QUIC stream. This is why a type-map change must be compiled into both peers before the new message can be exchanged.
The bottom row shows the two server entry points. `MTPHost` is a native QUIC endpoint for native MTP clients. `MTPWebServer` is an HTTP/3 server that reuses `HostConfig` and provides the same `accept()`-based MTP session API, adding web routing and WebTransport support for browser clients. Because they rely on different QUIC ALPN protocols (native MTP vs. `h3`), they must bind to different IP/port pairs and should not be enabled as Cargo features in the same binary. Choose `MTPHost` when you only serve native clients; choose `MTPWebServer` when you need HTTP/3 routes or browser-based MTP clients.
The bottom row shows the two server entry points. `MTPHost` is a native QUIC endpoint for native MTP clients. `MTPWebServer` owns TCP HTTPS and UDP HTTP/3/WebTransport listeners on the same numeric port, reuses one `HostConfig` and router, and provides the same `accept()`-based MTP session API. Its QUIC listener still uses only the `h3` ALPN, so it cannot share its UDP address with the native MTP ALPN endpoint. Choose `MTPHost` for native clients and `MTPWebServer` for browser-facing HTTP and WebTransport.
`mtp-crypto` is an optional cross-cutting layer used by authenticated native connections, WebTransport connections, and browser E2EE; TLS remains the transport security layer in both paths.
`mtp-host` performs version negotiation and native authentication before returning an `MTPConnection`. `mtp-webserver` routes HTTP/3 requests and WebTransport sessions through its endpoint. WebTransport MTP sessions support the same optional cryptographic authentication as native hosts when the `crypto` feature is enabled.
`mtp-host` performs version negotiation and native authentication before returning an `MTPConnection`. `mtp-webserver` routes HTTP/1.1, HTTP/2, and HTTP/3 requests through one route table and surfaces WebTransport sessions through `accept()`. WebTransport MTP sessions support the same optional cryptographic authentication as native hosts when the `crypto` feature is enabled.
The [native client](NATIVE-CLIENT.md), [WASM client](WASM-CLIENT.md), [native host](NATIVE-HOST.md), and [web server](NATIVE-HOST-WEB-SERVER.md) guides cover the public APIs for each boundary. The web server guide should be read as the host API for browser-facing deployments; it accepts the same `HostConfig` and authentication callbacks as the native host.

View file

@ -16,7 +16,7 @@ Native clients and hosts share the same connection shape after the opening hands
`WebMTPConnection`, returned by `MTPWebServer::accept()`, exposes the same members as the native host connection plus `request_path`, which contains the HTTP/3 path used for the WebTransport extended CONNECT request.
Server-side MTP connections expose `remote_addr`, the peer address observed by
QUIC. HTTP/3 route handlers receive the same address as `Http3Request::remote_addr`.
QUIC. HTTP route handlers receive the peer address as `HttpRequest::remote_addr`.
It is transport metadata and should not be treated as an authenticated identity;
behind a proxy, use the proxy's trusted forwarding mechanism separately.
The host connection also exposes a version-scoped `codec` and, for an authenticated client, its `client_public_key`. The native client connection also exposes these methods:

View file

@ -1,27 +1,28 @@
# MTP Web Server
`MTPWebServer` serves ordinary HTTP/3 routes and WebTransport MTP sessions through one QUIC endpoint. HTTP/3 requests are handled inside the server;
WebTransport sessions are returned by `accept()` for application messages.
`MTPWebServer` and `MTPHost` cannot bind the same IP and port.
`MTPWebServer` is a complete browser-facing HTTPS server. TCP TLS serves HTTP/1.1 and HTTP/2, while UDP QUIC serves HTTP/3 and WebTransport. Both listeners use the same certificate, router, IP address, and numeric port. Ordinary HTTP requests are handled inside the server; WebTransport MTP sessions are returned by `accept()` for application messages.
The repository's combined server example registers `/` on `MTPWebServer` and
returns `OK` while the process is running. The route is served over HTTP/3 at
`https://localhost:8080/` on the same QUIC endpoint as WebTransport MTP sessions.
`MTPWebServer` and the native `MTPHost` cannot bind the same IP and port. The TCP integration does not add the native MTP QUIC ALPN protocol to `MTPWebServer`.
The repository's server example serves the compiled web client at `/`, exposes status at `/health`, and accepts WebTransport sessions at the same origin. No second TCP server is required.
## WebServerConfig
| Builder | Default | Purpose |
| --- | --- | --- |
| `route(path, handler)` | None | Register an exact-path HTTP/3 handler. |
| `route(path, handler)` | None | Register an exact-path HTTP handler. |
| `route_method(method, path, handler)` | None | Register a method-specific handler. |
| `route_pattern(pattern, handler)` | None | Register a route with `{name}` single-segment parameters. |
| `route_pattern_method(method, pattern, handler)` | None | Register a method-specific parameterized route. |
| `fallback(handler)` | None | Handle requests that match no route. |
| `mtp_path(path)` | `/` | Path for WebTransport extended CONNECT. |
| `max_request_body(bytes)` | 4 MiB | Maximum buffered HTTP/3 request body. |
| `max_connections(count)` | 256 | Maximum concurrent HTTP/3 connections. |
| `request_timeout(duration)` | 30 seconds | HTTP/3 request handling timeout. |
| `drain_timeout(duration)` | 10 seconds | Shutdown drain period. |
| `serve_tcp_https(enabled)` | `true` | Enable the TCP TLS listener for HTTP/1.1 and HTTP/2. |
| `max_tcp_connections(count)` | 256 | Maximum concurrent TCP TLS connections. |
| `tls_handshake_timeout(duration)` | 10 seconds | Maximum TCP TLS handshake duration. |
| `max_request_body(bytes)` | 4 MiB | Maximum request body across all HTTP versions. |
| `max_connections(count)` | 256 | Maximum concurrent QUIC/HTTP/3 connections. |
| `request_timeout(duration)` | 30 seconds | Handler timeout across all HTTP versions. |
| `drain_timeout(duration)` | 5 seconds | Graceful shutdown period across both transports. |
| `with_metrics(metrics)` | None | Receive connection, request, and error callbacks. |
The route and fallback builders return `Result` because duplicate routes and duplicate fallback handlers are rejected.
@ -33,13 +34,13 @@ method-specific and more-specific routes take precedence.
```rust
use http::{Method, StatusCode};
use mtp::webserver::{Http3Request, Http3Response, RouteParams, WebServerConfig};
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig};
async fn profile(
_request: Http3Request,
response: Http3Response,
_request: HttpRequest,
response: HttpResponse,
params: RouteParams,
) -> Http3Response {
) -> HttpResponse {
let Some(userid) = params.get("userid") else {
return response.status(StatusCode::BAD_REQUEST);
};
@ -64,25 +65,25 @@ UTF-8 decoded before being passed to the handler. Malformed encoded values do
not match the route. Query strings remain available through
`request.uri.query()` and are not part of route matching.
## HTTP/3 Requests and Responses
## HTTP Requests and Responses
`Http3Request` contains `method`, `uri`, `headers`, the connecting `remote_addr`, and an optional buffered `body` represented by `bytes::Bytes`. `Http3Response::status`, `header`, and `body` build a buffered response. `try_header` returns an error for invalid header names or values. `stream` takes a `tokio::sync::mpsc::Receiver<Bytes>` for incremental response chunks.
`HttpRequest` contains `method`, `uri`, `headers`, the connecting `remote_addr`, and an optional buffered `body` represented by `bytes::Bytes`. `HttpResponse::status`, `header`, and `body` build a buffered response. `try_header` returns an error for invalid header names or values. `stream` takes a `tokio::sync::mpsc::Receiver<Bytes>` for incremental response chunks. The deprecated `Http3Request` and `Http3Response` aliases remain available for source compatibility.
```rust
use bytes::Bytes;
use http::{Method, StatusCode};
use tokio::sync::mpsc;
use mtp::webserver::{Http3Request, Http3Response, WebServerConfig};
use mtp::webserver::{HttpRequest, HttpResponse, WebServerConfig};
async fn health(_request: Http3Request, response: Http3Response) -> Http3Response {
async fn health(_request: HttpRequest, response: HttpResponse) -> HttpResponse {
response.status(StatusCode::OK).body("ok")
}
async fn whoami(request: Http3Request, response: Http3Response) -> Http3Response {
async fn whoami(request: HttpRequest, response: HttpResponse) -> HttpResponse {
response.body(format!("client: {}", request.remote_addr))
}
async fn stream_numbers(_request: Http3Request, response: Http3Response) -> Http3Response {
async fn stream_numbers(_request: HttpRequest, response: HttpResponse) -> HttpResponse {
let (tx, rx) = mpsc::channel::<Bytes>(10);
tokio::spawn(async move {
for number in 0..10 {
@ -129,7 +130,15 @@ while let Some(connection) = server.accept().await? {
```
> `MTPWebServer::new` consumes a `HostConfig` (not an `MTPHost` instance). It creates its own QUIC endpoint and does not share a port with a running `MTPHost`.
`server.accept()` returns `Option<WebMTPConnection>` for each WebTransport session. HTTP/3 routes do not surface through `accept()` because the server dispatches them internally. `WebMTPConnection` retains the negotiated version, codec, request path, remote address, description, sender, and receiver used by native MTP connections.
`server.accept()` returns `Option<WebMTPConnection>` for each WebTransport session. Ordinary HTTP routes do not surface through `accept()` because the server dispatches them internally. `WebMTPConnection` retains the negotiated version, codec, request path, remote address, description, sender, and receiver used by native MTP connections.
## Deployment
For direct browser access, leave `serve_tcp_https(true)` enabled. The server advertises `h2` and `http/1.1` on TCP TLS and `h3` on UDP QUIC; WebTransport extended CONNECT is available only over HTTP/3. Both transports must present the certificate supplied by the same `HostConfig` and use the same origin port.
When a reverse proxy or another process owns TCP, use `WebServerConfig::new().serve_tcp_https(false)`. This retains the UDP HTTP/3/WebTransport endpoint and its shared router without claiming the TCP port.
With port `0` and TCP enabled, construction binds TCP first and binds UDP to the selected TCP port, so `local_addr()` reports the common address. With TCP disabled, Quinn selects the UDP port as before. `shutdown()` stops both accept loops, gracefully finishes active HTTP requests until `drain_timeout`, closes Quinn, and then aborts remaining work. `close()` and dropping the server stop both listeners immediately.
### Authentication
@ -166,4 +175,4 @@ fn request_completed(&self, path: &str, status: u16, duration: Duration)
fn error_occurred(&self, error: &WebServerError)
```
Errors include route misses, invalid requests, body-limit failures, handler timeouts, response construction failures, and transport failures. Supply the metrics object with `WebServerConfig::with_metrics`.
Errors include invalid requests, body-limit failures, handler timeouts, response write failures, TLS failures, and transport failures. Completion callbacks include the final HTTP status for every supported HTTP version. Supply the metrics object with `WebServerConfig::with_metrics`.

View file

@ -2,7 +2,7 @@
The native host is a Rust library (`mtp-host`) that runs a QUIC server, accepts MTP client connections, negotiates protocol versions, and optionally performs a mutual-authentication handshake (login/register) using Ed25519 and ML-DSA-65 signatures.
> **Note:** `MTPHost` serves native MTP clients over raw QUIC. If you need to serve HTTP/3 routes on the same endpoint, use [`MTPWebServer`](NATIVE-HOST-WEB-SERVER.md) instead. `MTPWebServer` accepts the same `HostConfig` but binds an HTTP/3 endpoint rather than a native QUIC endpoint.
> **Note:** `MTPHost` serves native MTP clients over raw QUIC. For browser-facing HTTP/1.1, HTTP/2, HTTP/3, and WebTransport, use [`MTPWebServer`](NATIVE-HOST-WEB-SERVER.md). It accepts the same `HostConfig` but its UDP endpoint uses HTTP/3 rather than the native MTP QUIC ALPN.
## Cargo Dependency

View file

@ -15,7 +15,7 @@ Expose counters and gauges around the host and transport callbacks:
| Ping round-trip time and missed pings | Peer reachability and path latency. |
| Pipe accept, reject, EOF, and reset counts | Application admission and stream completion behavior. |
Implement `WebServerMetrics` for HTTP/3 request and error callbacks. Record the request path, status, duration, and `WebServerError` category without logging credentials, private keys, or message contents. Export host callback results through the application's metrics system for native deployments.
Implement `WebServerMetrics` for HTTP/1.1, HTTP/2, HTTP/3, TLS, and WebTransport callbacks. Record the request path, status, duration, and `WebServerError` category without logging credentials, private keys, or message contents. Export host callback results through the application's metrics system for native deployments.
## Tuning
@ -39,4 +39,4 @@ Back up host keyrings and client keyrings as protected secrets. Test restoring a
### Graceful Shutdown
Stop accepting new connections, reject new work at the application layer, and allow active requests and pipe writers to finish. Send a normal connection close, wait for the configured drain period, then force-close remaining QUIC sessions. For `MTPWebServer`, call `shutdown()` after the accept loop stops; Headits `drain_timeout` controls the drain period.
Stop accepting new connections, reject new work at the application layer, and allow active requests and pipe writers to finish. For `MTPWebServer`, call `shutdown()`; its `drain_timeout` controls graceful TCP HTTP completion and the QUIC drain period before remaining connection tasks are terminated.