diff --git a/crates/mq-integration/tests/pair_mitm.rs b/crates/mq-integration/tests/pair_mitm.rs index ebe48ac1..0a24013c 100644 --- a/crates/mq-integration/tests/pair_mitm.rs +++ b/crates/mq-integration/tests/pair_mitm.rs @@ -473,7 +473,7 @@ fn mitm_survives_tunnel_reconnect() { // SP4 spec §7.3 route step 3: no `h2` in ALPN → opaque. #[test] fn opaque_no_h2_alpn() { - let log = Logged::start("opaque(NoH2)"); + let log = Logged::start("opaque(no_h2)"); let o = origin(Proto::H2Tls, Handler::Status(200)); let p = proxy(o.addr, p256()); let b = browser(&p, o.addr, Path::new(ORIGIN_CA)); @@ -485,7 +485,7 @@ fn opaque_no_h2_alpn() { // SP4 spec §7.3 route step 4: an ignored host → opaque; h2 then runs with the origin. #[test] fn opaque_ignored_host() { - let log = Logged::start("opaque(Ignored)"); + let log = Logged::start("opaque(ignored)"); let o = origin(Proto::H2Tls, Handler::Status(200)); let p = proxy( o.addr, @@ -502,7 +502,7 @@ fn opaque_ignored_host() { // SP4 spec §7.3 "Peek": bytes that are not TLS → opaque, relayed as they are. #[test] fn opaque_non_tls_bytes() { - let log = Logged::start("opaque(NotTls)"); + let log = Logged::start("opaque(not_tls)"); let o = origin(Proto::H1Plain, Handler::Status(200)); let p = proxy(o.addr, p256()); let mut s = TcpStream::connect(p.mitm_addr()).unwrap(); @@ -519,7 +519,7 @@ fn opaque_non_tls_bytes() { // SP4 spec §7.3 route step 5: an SNI outside a NameConstraints CA's scope → opaque. #[test] fn opaque_out_of_scope_constrained_ca() { - let log = Logged::start("opaque(OutOfCaScope)"); + let log = Logged::start("opaque(ca_scope)"); let o = origin(Proto::H2Tls, Handler::Status(200)); let mitm = mitm_cfg("ca-dns-constraint", &[], MitmTuning::default()); let p = proxy(o.addr, mitm); @@ -533,7 +533,7 @@ fn opaque_out_of_scope_constrained_ca() { // afterwards reaches the origin. #[test] fn opaque_peek_timeout() { - let log = Logged::start("opaque(Timeout)"); + let log = Logged::start("opaque(timeout)"); let o = origin(Proto::H2Tls, Handler::Status(200)); let tuning = MitmTuning { peek: Duration::from_millis(200), @@ -566,7 +566,7 @@ fn echo_to_eof() -> (SocketAddr, JoinHandle>) { // the relay carries the bytes and the EOF, and the answer still comes back. #[test] fn opaque_peek_eof_half_close() { - let log = Logged::start("opaque(Eof)"); + let log = Logged::start("opaque(eof)"); let (addr, origin) = echo_to_eof(); let p = proxy(addr, p256()); let mut s = TcpStream::connect(p.mitm_addr()).unwrap(); @@ -586,7 +586,7 @@ fn opaque_peek_eof_half_close() { // SP4 spec §7.3 "Accept" step 2: at `max_conns` (lowered to 1) a new flow goes opaque. #[test] fn opaque_at_capacity() { - let log = Logged::start("opaque(AtCapacity)"); + let log = Logged::start("opaque(capacity)"); let o = origin(Proto::H2Tls, Handler::Status(200)); let tuning = MitmTuning { max_conns: 1, diff --git a/crates/mq-linux/README.md b/crates/mq-linux/README.md index 46702676..118ff6e4 100644 --- a/crates/mq-linux/README.md +++ b/crates/mq-linux/README.md @@ -2,7 +2,7 @@ The single home for Linux syscalls that need `unsafe` or raw fds (spec §2.2). -## GSO/GRO: hand-written `libc`, not `quinn-udp` (Task 5.1) +## GSO/GRO: hand-written `libc`, not `quinn-udp` I evaluated `quinn-udp` 0.5.16 (`UdpSocketState` over a `std::net::UdpSocket`) in a throwaway spike on Linux 7.0 loopback. Functionally it works: a GSO send diff --git a/crates/mq-proxy/src/client/mitm/leaf.rs b/crates/mq-proxy/src/client/mitm/leaf.rs index 451a90a0..2cc04d84 100644 --- a/crates/mq-proxy/src/client/mitm/leaf.rs +++ b/crates/mq-proxy/src/client/mitm/leaf.rs @@ -64,6 +64,8 @@ struct Entry { } impl LeafStore { + /// # Panics + /// If generating the shard's P-256 leaf key fails. pub fn new(ca: Arc, clock: fn() -> SystemTime) -> Self { Self { ca, diff --git a/crates/mq-proxy/src/client/mitm/mod.rs b/crates/mq-proxy/src/client/mitm/mod.rs index e67ffe6b..9a4e1122 100644 --- a/crates/mq-proxy/src/client/mitm/mod.rs +++ b/crates/mq-proxy/src/client/mitm/mod.rs @@ -47,7 +47,7 @@ pub struct Handoff { struct Stats { mitm: u64, /// By `Why as usize`. - opaque: [u64; 11], + opaque: [u64; Why::ALL.len()], tls_fail: u64, h2_fail: u64, dead: u64, @@ -55,21 +55,6 @@ struct Stats { rejects: u64, } -/// The `opaque_*` keys of the `mq.mitm` line, in `Why` order. -const WHY_KEYS: [&str; 11] = [ - "not_tls", - "no_sni", - "bad_sni", - "no_h2", - "ignored", - "ca_scope", - "tls_incompat", - "timeout", - "too_large", - "eof", - "capacity", -]; - /// SP4 spec §2.2: the front's timers, by id → their conn. #[derive(Default)] struct Timers(HashMap); @@ -127,7 +112,7 @@ impl Mitm { h2, stats: Stats::default(), timers: Timers::default(), - pump_cap: crate::server::origin::PUMP_CAP, + pump_cap: crate::tls_pipe::PUMP_CAP, } } @@ -309,8 +294,8 @@ impl Mitm { } let leaf = self.leaf.stats(); let mut line = format!("mq.mitm conns={live} streams={streams} mitm={}", s.mitm); - for (k, n) in WHY_KEYS.iter().zip(s.opaque) { - line += &format!(" opaque_{k}={n}"); + for (w, n) in Why::ALL.iter().zip(s.opaque) { + line += &format!(" opaque_{}={n}", w.key()); } line += &format!( " tls_fail={} h2_fail={} dead={} leaf_hit={} leaf_miss={} reqs={} rejects={}", @@ -352,7 +337,7 @@ impl Mitm { fn count_opaque(&mut self, why: Why, sni: Option<&str>) { self.stats.opaque[why as usize] += 1; - log::debug!("mq_mitm: {} → opaque({why:?})", sni.unwrap_or("-")); + log::debug!("mq_mitm: {} → opaque({})", sni.unwrap_or("-"), why.key()); } } diff --git a/crates/mq-proxy/src/client/mitm/policy.rs b/crates/mq-proxy/src/client/mitm/policy.rs index 745b37b0..527e2183 100644 --- a/crates/mq-proxy/src/client/mitm/policy.rs +++ b/crates/mq-proxy/src/client/mitm/policy.rs @@ -143,6 +143,40 @@ pub enum Why { AtCapacity, } +impl Why { + /// Every variant, in `mq.mitm` line order (`opaque` counters index by `as usize`). + pub const ALL: [Why; 11] = [ + Why::NotTls, + Why::NoSni, + Why::BadSni, + Why::NoH2, + Why::Ignored, + Why::OutOfCaScope, + Why::TlsIncompatible, + Why::Timeout, + Why::TooLarge, + Why::Eof, + Why::AtCapacity, + ]; + + /// The `opaque_` name in the `mq.mitm` line. + pub fn key(self) -> &'static str { + match self { + Why::NotTls => "not_tls", + Why::NoSni => "no_sni", + Why::BadSni => "bad_sni", + Why::NoH2 => "no_h2", + Why::Ignored => "ignored", + Why::OutOfCaScope => "ca_scope", + Why::TlsIncompatible => "tls_incompat", + Why::Timeout => "timeout", + Why::TooLarge => "too_large", + Why::Eof => "eof", + Why::AtCapacity => "capacity", + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum Route { Mitm(Sni), @@ -189,6 +223,13 @@ mod tests { Sni::canonical(s.as_bytes()).unwrap() } + #[test] + fn why_all_is_in_discriminant_order() { + for (i, w) in Why::ALL.iter().enumerate() { + assert_eq!(*w as usize, i, "{w:?}"); + } + } + fn dom(s: &str) -> DnsSubtree { DnsSubtree::Domain(sni(s)) } diff --git a/crates/mq-proxy/src/client/mitm/stream.rs b/crates/mq-proxy/src/client/mitm/stream.rs index 9210c8ee..7b501077 100644 --- a/crates/mq-proxy/src/client/mitm/stream.rs +++ b/crates/mq-proxy/src/client/mitm/stream.rs @@ -122,7 +122,7 @@ impl MStream { /// One §7.6 step: the upload, then the download, which always runs after /// a `SendOut::Done` (a send error leaves the core `Failed` with no - /// readiness to follow, Task 5.1 ruling). `room`: the TLS output is below + /// readiness to follow). `room`: the TLS output is below /// its caps. `true` when anything moved. pub(super) fn step( &mut self, diff --git a/crates/mq-proxy/src/config.rs b/crates/mq-proxy/src/config.rs index bdc1d0c5..1ecea418 100644 --- a/crates/mq-proxy/src/config.rs +++ b/crates/mq-proxy/src/config.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2026 mp0rta and mqproxy contributors -//! spec §6.2, §6.3, §6.5: client and server settings; the CLI maps onto these (Task 9.1). +//! spec §6.2, §6.3, §6.5: client and server settings; the CLI maps onto these. use crate::client::mitm::MitmTuning; use crate::client::mitm::ca::Ca; diff --git a/crates/mq-proxy/src/server/origin/mod.rs b/crates/mq-proxy/src/server/origin/mod.rs index 4c0cfff4..d0af05a9 100644 --- a/crates/mq-proxy/src/server/origin/mod.rs +++ b/crates/mq-proxy/src/server/origin/mod.rs @@ -51,8 +51,7 @@ pub const UPLOAD_CAP: usize = 256 * 1024; pub const IDLE_MAX: Duration = Duration::from_secs(118); /// spec §7.7: the idle sweep interval. pub const SWEEP: Duration = Duration::from_secs(10); -/// spec §7.3 step 4: pump iterations per callback. -pub const PUMP_CAP: usize = 16; +pub use crate::tls_pipe::PUMP_CAP; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Scheme { Http, @@ -261,8 +260,6 @@ type ConnKey = (Scheme, String, u16); /// Reusable conns by key, several per key (§7.7). type Pool = HashMap>; -// The §7.1 state below is filled by Tasks 5.2–5.6c. - /// Where a request's record lives (§7.1 lookups). #[derive(Copy, Clone, Debug)] enum Where { diff --git a/crates/mq-proxy/src/tls_pipe/exec.rs b/crates/mq-proxy/src/tls_pipe/exec.rs index bf2d3c0f..0bc4a789 100644 --- a/crates/mq-proxy/src/tls_pipe/exec.rs +++ b/crates/mq-proxy/src/tls_pipe/exec.rs @@ -94,7 +94,7 @@ impl ShardExec { } } - /// Tasks spawned and not yet finished (test-support accessor, Task 5.1b). + /// Tasks spawned and not yet finished (test-support accessor). #[cfg(any(test, feature = "test-support"))] pub fn len(&self) -> usize { self.tasks.len() + self.spawned.borrow().len() diff --git a/crates/mq-proxy/src/tls_pipe/mod.rs b/crates/mq-proxy/src/tls_pipe/mod.rs index 171771b2..26cd429f 100644 --- a/crates/mq-proxy/src/tls_pipe/mod.rs +++ b/crates/mq-proxy/src/tls_pipe/mod.rs @@ -8,6 +8,9 @@ mod exec; mod pipe; mod tls_io; +/// Pump iterations per callback (SP3 spec §7.3 step 4; SP4 R2 for the MITM front). +pub const PUMP_CAP: usize = 16; + pub use exec::{Dirty, ShardExec}; #[cfg(any(test, feature = "test-support"))] pub use pipe::pipe_pair; diff --git a/crates/mq-proxy/tests/client_mitm_stream.rs b/crates/mq-proxy/tests/client_mitm_stream.rs index 6ef748fc..080565ef 100644 --- a/crates/mq-proxy/tests/client_mitm_stream.rs +++ b/crates/mq-proxy/tests/client_mitm_stream.rs @@ -634,7 +634,7 @@ fn reject_before_head_xmq_error_response() { assert_eq!(t.streams(), 0); } -/// Task 5.1 ruling: a send error leaves the core `Failed` with no readiness +/// A send error leaves the core `Failed` with no readiness /// to follow, so the front pulls the download side after `SendOut::Done`. #[test] fn send_error_surfaces_without_an_h3_event() { diff --git a/crates/mq-proxy/tests/server_origin_connect.rs b/crates/mq-proxy/tests/server_origin_connect.rs index 9813b16e..c502c09e 100644 --- a/crates/mq-proxy/tests/server_origin_connect.rs +++ b/crates/mq-proxy/tests/server_origin_connect.rs @@ -141,10 +141,7 @@ fn plain_http_connect_ms_is_dial_duration() { ); let conn = oh.with_host(|h, _| h.origin().conn_of(h3)).unwrap(); assert_eq!(oh.with_host(|h, _| h.origin().connect_ms(conn)), Some(250)); - assert!( - oh.events().is_empty(), - "the request is not sent here (Task 5.4)" - ); + assert!(oh.events().is_empty(), "the request is not sent here"); } #[test] diff --git a/crates/mq-transport/src/conn.rs b/crates/mq-transport/src/conn.rs index c0cb2c80..505285c5 100644 --- a/crates/mq-transport/src/conn.rs +++ b/crates/mq-transport/src/conn.rs @@ -44,7 +44,7 @@ pub(crate) fn recv_datagram( }) } -/// spec §4.2 `drive`, §4.3, §4.7; order per plan Task 4.6. +/// spec §4.2 `drive`, §4.3, §4.7. pub(crate) fn drive(t: &mut Transport, now: Time) { let inner = &mut *t.inner; inner.deadline = None; // spec §4.3: cleared before the engine runs diff --git a/crates/mq-transport/src/ffi/trampolines.rs b/crates/mq-transport/src/ffi/trampolines.rs index 8de77a29..f03bd8ef 100644 --- a/crates/mq-transport/src/ffi/trampolines.rs +++ b/crates/mq-transport/src/ffi/trampolines.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2026 mp0rta and mqproxy contributors -//! Transport and ALPN callbacks (spec §4.4, §4.7, §4.8; plan Task 4.6 table). +//! Transport and ALPN callbacks (spec §4.4, §4.7, §4.8). //! //! Every trampoline runs inside `guard` (a panic aborts) and reaches `Inner` only through //! `clock::current()`. The bookkeeping lives in plain `fn on_*(inner: &mut Inner, ..)` diff --git a/crates/mq-transport/tests/fabric_basic.rs b/crates/mq-transport/tests/fabric_basic.rs index 4d181988..e26208c8 100644 --- a/crates/mq-transport/tests/fabric_basic.rs +++ b/crates/mq-transport/tests/fabric_basic.rs @@ -55,7 +55,7 @@ fn handshake_completes() { assert!(server.call(T0, move |t, _| t.conn_stats(srv_conn)).is_ok()); } -/// Smoke test of the stream methods and trampolines (the full suite is Task 4.7). +/// Smoke test of the stream methods and trampolines. #[test] fn stream_round_trip_and_reset_release_slots() { let (client, server, conn, srv_conn) = connected(); diff --git a/crates/mqproxy/src/cli.rs b/crates/mqproxy/src/cli.rs index c4d39f0a..f3a44ac9 100644 --- a/crates/mqproxy/src/cli.rs +++ b/crates/mqproxy/src/cli.rs @@ -356,9 +356,7 @@ fn server(a: ServerArgs, f: FileConfig) -> Result { ); } let startup_lines = match gateway { - Some(_) => vec![format!( - "mq_origin: hyper {HYPER_VERSION} + rustls (HTTP3=no)" - )], + Some(_) => vec![format!("mq_origin: hyper {HYPER_VERSION} + rustls")], None => Vec::new(), }; let cc = cc(a.cc.or(f.cc).as_deref())?; diff --git a/crates/mqproxy/tests/cli.rs b/crates/mqproxy/tests/cli.rs index cf8e1a2e..e1e9cadd 100644 --- a/crates/mqproxy/tests/cli.rs +++ b/crates/mqproxy/tests/cli.rs @@ -238,7 +238,7 @@ fn server_gateway_on_by_default() { assert!(cli::wants_h3(&r)); assert_eq!( r.startup_lines, - vec!["mq_origin: hyper 1.10 + rustls (HTTP3=no)".to_string()] + vec!["mq_origin: hyper 1.10 + rustls".to_string()] ); // The gateway flags reach `GatewayConfig`. let r = parse( @@ -1045,7 +1045,7 @@ fn spawn(args: &[String]) -> Proc { #[test] fn gateway_on_and_udp_lines_logged() { let mut p = spawn(&server_args(&format!("127.0.0.1:{}", free_udp()), &[])); - p.wait_line("[INFO] mq_origin: hyper 1.10 + rustls (HTTP3=no)"); + p.wait_line("[INFO] mq_origin: hyper 1.10 + rustls"); p.wait_line("gateway=on, udp=on, udp-idle=60s)"); assert_eq!(p.term(), 0, "{:#?}", p.lines); } diff --git a/crates/mqproxy/tests/smoke.rs b/crates/mqproxy/tests/smoke.rs index 391e8594..d66b474d 100644 --- a/crates/mqproxy/tests/smoke.rs +++ b/crates/mqproxy/tests/smoke.rs @@ -174,7 +174,7 @@ fn startup_lines_golden() { assert_eq!( lines_until(&mut p, "listening on"), vec![ - "[INFO] mq_origin: hyper 1.10 + rustls (HTTP3=no)".to_string(), + "[INFO] mq_origin: hyper 1.10 + rustls".to_string(), format!( "[INFO] mqproxy server listening on {listen} \ (cc=bbr, sched=minrtt, gateway=on, udp=on, udp-idle=60s)" diff --git a/website/guide/configuration.md b/website/guide/configuration.md index 927f1f1b..97d7a96a 100644 --- a/website/guide/configuration.md +++ b/website/guide/configuration.md @@ -40,7 +40,7 @@ Config keys map to the CLI flags in the [Options Reference](/reference/options): | `[Auth]` | `Key` (token) | `Key` (token) | | `[Multipath]` | `CC`, `Scheduler` | `CC`, `Scheduler`, `Path` (repeatable) | | `[Ingress]` | — | `Socks5`, `HttpConnect`, `Gateway`, `TProxy`, `Mode`, `Fwmark`, `Table`, `Dport`, `SetupRedirect`, `SkipUid` | -| `[Gateway]` | `Enabled`, `Masquerade`, `OriginCA`, `CacheMaxBytes` | — | +| `[Gateway]` | `Enabled`, `Masquerade`, `OriginCA` | — | | `[Mitm]` | — | `Enabled`, `CACert`, `CAKey`, `IgnoreHosts` (repeatable) | | `[UDP]` | `Enabled`, `IdleTimeout` | — | | `[Metrics]` | `Interval`, `PerRequest` | `Interval` | diff --git a/website/guide/http-gateway.md b/website/guide/http-gateway.md index d1c271a0..ee19cd83 100644 --- a/website/guide/http-gateway.md +++ b/website/guide/http-gateway.md @@ -5,8 +5,9 @@ Application (curl / SDK) │ POST /_mqproxy/fetch (X-Mq-Auth / X-Mq-Target / X-Mq-Method + raw body) ▼ mqproxy-client ──────── HTTP/3 over MPQUIC (ALPN h3) ────────► mqproxy-server - 1 HTTP request = 1 H3 request stream = 1 MPQUIC stream │ libcurl (h2→h1, - spread across paths │ TLS verify ON) + 1 HTTP request = 1 H3 request stream = 1 MPQUIC stream │ hyper + rustls + spread across paths │ (h2 or HTTP/1.1, + │ TLS verify ON) ▼ Origin server ``` @@ -17,10 +18,10 @@ Instead of tunneling opaque bytes, the gateway **executes delegated HTTP request Per-request `X-Mq-*` controls let a caller opt into gateway features without changing the API: -- `X-Mq-Origin-Protocol` pins the upstream HTTP version (`h1`/`h2`/`h3`). +- `X-Mq-Origin-Protocol`: `h1` forces HTTP/1.1; `h2`, `h3` or no header lets the server negotiate h2 or HTTP/1.1 via ALPN (there is no HTTP/3 origin client). Any other value → `400` with `x-mq-error: bad-origin-protocol`. - `X-Mq-Accept-Encoding` requests download compression. - `X-Mq-Forward-Cookie` forwards the `Cookie` header upstream (otherwise withheld). -- `X-Mq-Cache` opts the response into the in-memory origin cache (`--cache-max-bytes`). +- `X-Mq-Cache` is still validated (a malformed value → `400` with `x-mq-error: bad-cache-ttl`), but a valid value has no effect: the origin response cache was removed. The server also pools and reuses origin connections across requests. @@ -49,4 +50,4 @@ curl -X POST http://127.0.0.1:8080/_mqproxy/fetch \ `--path` works the same way here: the gateway's MPQUIC connection aggregates each request across all bound paths. -See the [Options Reference](/reference/options#http-gateway-mode) for server-side flags (`--no-gateway`, `--origin-ca`, `--request-metrics`, `--cache-max-bytes`, `--masquerade`). +See the [Options Reference](/reference/options#http-gateway-mode) for server-side flags (`--no-gateway`, `--origin-ca`, `--request-metrics`, `--masquerade`). diff --git a/website/guide/tls-mitm.md b/website/guide/tls-mitm.md index fae935d2..4b8fa61e 100644 --- a/website/guide/tls-mitm.md +++ b/website/guide/tls-mitm.md @@ -144,7 +144,7 @@ With `--metrics-interval`, the client also prints a `mq.mitm` line on every tick mq.mitm conns= streams= mitm= opaque_not_tls= opaque_no_sni= opaque_bad_sni= opaque_no_h2= opaque_ignored= opaque_ca_scope= opaque_tls_incompat= opaque_timeout= opaque_too_large= opaque_eof= opaque_capacity= tls_fail= h2_fail= dead= leaf_hit= leaf_miss= reqs= rejects= ``` -`conns` and `streams` are the MITM connections and h2 streams open right now; everything else is cumulative. `mitm` counts connections that were terminated, and each `opaque_*` counter counts connections relayed opaquely for one of the reasons listed [above](#what-is-relayed-opaquely). `tls_fail`, `h2_fail` and `dead` count connections that ended on a TLS error, an h2 error, or a dead peer. `leaf_hit`/`leaf_miss` count forged-certificate cache hits and misses, and `reqs` counts every h2 request received, and `rejects` counts those refused before a tunnel request was opened: an `x-mq-error` or 421 answer from the request mapping, an unavailable or refused tunnel, an `RST_STREAM` for a malformed request, or a `REFUSED_STREAM` past the per-connection stream limit. At debug log level each routing decision is logged as `mq_mitm: → mitm|opaque()`. Header and body values are never logged. +`conns` and `streams` are the MITM connections and h2 streams open right now; everything else is cumulative. `mitm` counts connections that were terminated, and each `opaque_*` counter counts connections relayed opaquely for one of the reasons listed [above](#what-is-relayed-opaquely). `tls_fail`, `h2_fail` and `dead` count connections that ended on a TLS error, an h2 error, or a dead peer. `leaf_hit`/`leaf_miss` count forged-certificate cache hits and misses, and `reqs` counts every h2 request received, and `rejects` counts those refused before a tunnel request was opened: an `x-mq-error` or 421 answer from the request mapping, an unavailable or refused tunnel, an `RST_STREAM` for a malformed request, or a `REFUSED_STREAM` past the per-connection stream limit. At debug log level each routing decision is logged as `mq_mitm: → mitm|opaque()`, where `` is the `opaque_*` counter name without the prefix (e.g. `opaque(no_h2)`). Header and body values are never logged. ## Security posture diff --git a/website/guide/udp-relay.md b/website/guide/udp-relay.md index a7823e6a..4a4f1642 100644 --- a/website/guide/udp-relay.md +++ b/website/guide/udp-relay.md @@ -34,7 +34,7 @@ UDP relay is exposed on the **same `--socks5` listener** — any SOCKS5 client t --socks5 127.0.0.1:1080 ``` -`curl` does not speak SOCKS5 UDP ASSOCIATE, so use a UDP-capable SOCKS5 client. The repo ships `udpsocks`, a minimal test client (built by the test suite), which is handy for a quick check: +`curl` does not speak SOCKS5 UDP ASSOCIATE, so use a UDP-capable SOCKS5 client. The repo ships `udpsocks`, a minimal test client (build it with `cargo build --release --locked -p mqproxy --examples`), which is handy for a quick check: ```bash # Relay a UDP packet to a target through the client's SOCKS5 listener diff --git a/website/ja/guide/configuration.md b/website/ja/guide/configuration.md index 08d34081..fcd0ead0 100644 --- a/website/ja/guide/configuration.md +++ b/website/ja/guide/configuration.md @@ -40,7 +40,7 @@ mqproxy server --config /etc/mqproxy/edge1.conf | `[Auth]` | `Key` (トークン) | `Key` (トークン) | | `[Multipath]` | `CC`, `Scheduler` | `CC`, `Scheduler`, `Path` (繰り返し可) | | `[Ingress]` | — | `Socks5`, `HttpConnect`, `Gateway`, `TProxy`, `Mode`, `Fwmark`, `Table`, `Dport`, `SetupRedirect`, `SkipUid` | -| `[Gateway]` | `Enabled`, `Masquerade`, `OriginCA`, `CacheMaxBytes` | — | +| `[Gateway]` | `Enabled`, `Masquerade`, `OriginCA` | — | | `[Mitm]` | — | `Enabled`, `CACert`, `CAKey`, `IgnoreHosts` (繰り返し可) | | `[UDP]` | `Enabled`, `IdleTimeout` | — | | `[Metrics]` | `Interval`, `PerRequest` | `Interval` | diff --git a/website/ja/guide/http-gateway.md b/website/ja/guide/http-gateway.md index 6baeb3a8..c6065f55 100644 --- a/website/ja/guide/http-gateway.md +++ b/website/ja/guide/http-gateway.md @@ -5,8 +5,9 @@ │ POST /_mqproxy/fetch (X-Mq-Auth / X-Mq-Target / X-Mq-Method + 生ボディ) ▼ mqproxy-client ──────── MPQUIC 上の HTTP/3 (ALPN h3) ────────► mqproxy-server - 1 HTTP リクエスト = 1 H3 リクエストストリーム = 1 MPQUIC ストリーム │ libcurl (h2→h1, - パスにわたって分散 │ TLS 検証 ON) + 1 HTTP リクエスト = 1 H3 リクエストストリーム = 1 MPQUIC ストリーム │ hyper + rustls + パスにわたって分散 │ (h2 または HTTP/1.1, + │ TLS 検証 ON) ▼ オリジンサーバー ``` @@ -17,10 +18,10 @@ mqproxy-client ──────── MPQUIC 上の HTTP/3 (ALPN h3) ─── リクエストごとの `X-Mq-*` 制御により、API を変更せずにゲートウェイ機能をオプトインできます。 -- `X-Mq-Origin-Protocol` は上流の HTTP バージョンを固定します(`h1`/`h2`/`h3`)。 +- `X-Mq-Origin-Protocol`: `h1` は HTTP/1.1 を強制します。`h2`・`h3`・ヘッダなしの場合は、サーバーが ALPN で h2 か HTTP/1.1 をネゴシエートします(HTTP/3 のオリジンクライアントはありません)。それ以外の値は `400`(`x-mq-error: bad-origin-protocol`)になります。 - `X-Mq-Accept-Encoding` はダウンロードの圧縮を要求します。 - `X-Mq-Forward-Cookie` は `Cookie` ヘッダを上流へ転送します(指定しなければ送られません)。 -- `X-Mq-Cache` はレスポンスをインメモリのオリジンキャッシュ(`--cache-max-bytes`)にオプトインします。 +- `X-Mq-Cache` は引き続き検証されます(不正な値は `400`、`x-mq-error: bad-cache-ttl`)が、正しい値を指定しても効果はありません。オリジンのレスポンスキャッシュは削除されました。 サーバーはまた、リクエストをまたいでオリジンコネクションをプールして再利用します。 @@ -49,4 +50,4 @@ curl -X POST http://127.0.0.1:8080/_mqproxy/fetch \ `--path` はここでも同様に機能します。ゲートウェイの MPQUIC コネクションは、各リクエストをバインドされた全パスにわたってアグリゲーションします。 -サーバー側のフラグ(`--no-gateway`、`--origin-ca`、`--request-metrics`、`--cache-max-bytes`、`--masquerade`)は [オプションリファレンス](/ja/reference/options#http-ゲートウェイモード) を参照してください。 +サーバー側のフラグ(`--no-gateway`、`--origin-ca`、`--request-metrics`、`--masquerade`)は [オプションリファレンス](/ja/reference/options#http-ゲートウェイモード) を参照してください。 diff --git a/website/ja/guide/tls-mitm.md b/website/ja/guide/tls-mitm.md index ff238ea4..eaf4a615 100644 --- a/website/ja/guide/tls-mitm.md +++ b/website/ja/guide/tls-mitm.md @@ -144,7 +144,7 @@ nft add rule inet mqproxy_block forward udp dport 443 reject mq.mitm conns= streams= mitm= opaque_not_tls= opaque_no_sni= opaque_bad_sni= opaque_no_h2= opaque_ignored= opaque_ca_scope= opaque_tls_incompat= opaque_timeout= opaque_too_large= opaque_eof= opaque_capacity= tls_fail= h2_fail= dead= leaf_hit= leaf_miss= reqs= rejects= ``` -`conns` と `streams` は現在開いている MITM コネクションと h2 ストリームの数で、それ以外は累積値です。`mitm` は終端したコネクション数、各 `opaque_*` は [上記](#不透明リレーになるもの) の理由ごとに不透明リレーしたコネクション数です。`tls_fail`、`h2_fail`、`dead` は、TLS エラー、h2 エラー、ピア消失で終了したコネクション数を数えます。`leaf_hit`/`leaf_miss` は偽造証明書キャッシュのヒットとミス、`reqs` は受信したすべての h2 リクエストの数、`rejects` はそのうちトンネルリクエストを開く前に拒否したものの数です(リクエストマッピングによる `x-mq-error` または 421 の応答、トンネルが使えないか開けなかった場合、不正なリクエストへの `RST_STREAM`、コネクションあたりのストリーム上限を超えたときの `REFUSED_STREAM`)。debug ログレベルでは、ルーティングの判断ごとに `mq_mitm: → mitm|opaque()` が出力されます。ヘッダやボディの値がログに出ることはありません。 +`conns` と `streams` は現在開いている MITM コネクションと h2 ストリームの数で、それ以外は累積値です。`mitm` は終端したコネクション数、各 `opaque_*` は [上記](#不透明リレーになるもの) の理由ごとに不透明リレーしたコネクション数です。`tls_fail`、`h2_fail`、`dead` は、TLS エラー、h2 エラー、ピア消失で終了したコネクション数を数えます。`leaf_hit`/`leaf_miss` は偽造証明書キャッシュのヒットとミス、`reqs` は受信したすべての h2 リクエストの数、`rejects` はそのうちトンネルリクエストを開く前に拒否したものの数です(リクエストマッピングによる `x-mq-error` または 421 の応答、トンネルが使えないか開けなかった場合、不正なリクエストへの `RST_STREAM`、コネクションあたりのストリーム上限を超えたときの `REFUSED_STREAM`)。debug ログレベルでは、ルーティングの判断ごとに `mq_mitm: → mitm|opaque()` が出力されます。`` は `opaque_*` カウンタ名から接頭辞を除いたものです(例: `opaque(no_h2)`)。ヘッダやボディの値がログに出ることはありません。 ## セキュリティ姿勢 diff --git a/website/ja/guide/udp-relay.md b/website/ja/guide/udp-relay.md index 9bc8a365..b657cbc6 100644 --- a/website/ja/guide/udp-relay.md +++ b/website/ja/guide/udp-relay.md @@ -34,7 +34,7 @@ UDP リレーは **同じ `--socks5` リスナー** 上で公開されます — --socks5 127.0.0.1:1080 ``` -`curl` は SOCKS5 UDP ASSOCIATE を話さないため、UDP 対応の SOCKS5 クライアントを使ってください。リポジトリには最小限のテストクライアント `udpsocks`(テストスイートでビルドされます)が同梱されており、手軽な確認に便利です。 +`curl` は SOCKS5 UDP ASSOCIATE を話さないため、UDP 対応の SOCKS5 クライアントを使ってください。リポジトリには最小限のテストクライアント `udpsocks`(`cargo build --release --locked -p mqproxy --examples` でビルドします)が同梱されており、手軽な確認に便利です。 ```bash # クライアントの SOCKS5 リスナー経由で UDP パケットをターゲットへリレー diff --git a/website/ja/reference/observability.md b/website/ja/reference/observability.md index 91fc7296..ece59e82 100644 --- a/website/ja/reference/observability.md +++ b/website/ja/reference/observability.md @@ -1,6 +1,6 @@ # オブザーバビリティ -どちらの側にも `--qlog ` を渡すと xquic qlog を出力します。パス単位のバイト数は、ストリーム内マルチパスが実際にフローを複数パスへ分割していることを確認できます — アグリゲーションが機能している鍵となる指標です。(xquic は `XQC_ENABLE_EVENT_LOG=ON` でビルドされている必要があり、`scripts/build-xquic.sh` がこれを行います。) +どちらの側にも `--qlog ` を渡すと xquic qlog を出力します。パス単位のバイト数は、ストリーム内マルチパスが実際にフローを複数パスへ分割していることを確認できます — アグリゲーションが機能している鍵となる指標です。 ## メトリクス @@ -10,10 +10,14 @@ ## テスト -ビルド後、同梱のテストスイートを実行します。 +リポジトリのルートでテストスイートを実行します。 ```bash -ctest --test-dir build --output-on-failure +cargo test --workspace --locked +cargo clippy --workspace --all-targets --locked -- -D warnings +cargo build --release --locked -p mqproxy --bins --examples +bash tests/test_cli_help.sh target/release/mqproxy +bash tests/integration/e2e_udp.sh # 他に e2e_{gateway,multipath,tproxy,mitm_h2,...}.sh ``` -ワイヤフレーミング、リレー/フロー状態機械、イングレスパース、ゲートウェイのリクエスト経路、TLS MITM 暗号コアをカバーし、マルチパスアグリゲーション・完全なゲートウェイチェーン・UDP リレー・透過キャプチャのエンドツーエンドスクリプトも含みます。root または `NET_ADMIN` を必要とするテスト (マルチパスと透過キャプチャのエンドツーエンド実行) は、非特権で実行すると自動的にスキップされます。 +cargo のテストはワイヤフレーミング、リレー/フロー状態機械、イングレスパース、ゲートウェイのリクエスト経路、TLS MITM をカバーします。`tests/integration/e2e_*.sh` のスクリプトは、マルチパスアグリゲーション・完全なゲートウェイチェーン・UDP リレー・透過キャプチャ・MITM をエンドツーエンドで実行します。root または `NET_ADMIN` を必要とするスクリプトは、非特権で実行すると自動的にスキップされます。 diff --git a/website/ja/reference/options.md b/website/ja/reference/options.md index fcff9fcb..057b5378 100644 --- a/website/ja/reference/options.md +++ b/website/ja/reference/options.md @@ -63,7 +63,7 @@ | `--no-gateway` | **このサーバーの HTTP ゲートウェイモードを無効化** (デフォルト有効)。TCP プロキシのコアは動作し続け、ゲートウェイのオリジンブリッジだけが OFF になるため、クライアントの `--gateway` イングレスは通信相手を失う。 | | `--origin-ca ` | オリジン TLS 検証用の追加 CA バンドル (プライベート CA/テスト)。検証自体は常に ON | | `--request-metrics` | ゲートウェイリクエストごとに 1 行の `mq.req` logfmt 行を出力 (method/status/target/ttfb/origin_protocol/cache/…)。オプトイン、デフォルト OFF。`--metrics-interval` とは独立。 | -| `--cache-max-bytes ` | `N` バイトに制限されたインメモリのオリジンレスポンスキャッシュ (`0` = OFF = デフォルト。例: `67108864` = 64 MiB)。リクエストごとに `X-Mq-Cache` でオプトイン。 | +| `--cache-max-bytes ` | 互換性のために受け付けるが、警告を出して無視する (オリジンのレスポンスキャッシュは削除済み)。 | | `--masquerade` | **未認証** のゲートウェイリクエストに素の `404` (X-Mq-* ヘッダなし) で応答し、プローブ/スキャナにはフィンガープリント可能な `403 auth-failed` ではなく一般的な HTTP/3 サーバーに見せる。認証済みリクエストには影響しない。ゲートウェイ専用、デフォルト OFF、インターネット公開サーバーに推奨。 | **クライアント** diff --git a/website/ja/reference/security-model.md b/website/ja/reference/security-model.md index 330de917..175f62a8 100644 --- a/website/ja/reference/security-model.md +++ b/website/ja/reference/security-model.md @@ -33,5 +33,6 @@ mqproxy の利用は自己責任で行ってください。その適合性・セ ## 謝辞 - [XQUIC](https://github.com/alibaba/xquic) (Alibaba) — QUIC/MPQUIC トランスポート。[mp0rta フォーク](https://github.com/mp0rta/xquic) 経由。 -- [BoringSSL](https://boringssl.googlesource.com/boringssl) — TLS バックエンド。 -- [nghttp2](https://nghttp2.org/) — C ビルドの TLS MITM イングレスの HTTP/2 フレーミング (Rust バイナリは `h2` クレートと rustls を使用)。 +- [BoringSSL](https://boringssl.googlesource.com/boringssl) — QUIC トランスポートの TLS バックエンド。 +- [h3wire](https://crates.io/crates/h3wire) — HTTP/3 フレーミングと QPACK。 +- [hyper](https://hyper.rs/)、[h2](https://crates.io/crates/h2)、[rustls](https://rustls.dev/) — オリジンへの HTTP 通信と、TLS MITM の TLS/HTTP/2 側。 diff --git a/website/reference/observability.md b/website/reference/observability.md index d56d6eca..652c9bf9 100644 --- a/website/reference/observability.md +++ b/website/reference/observability.md @@ -1,6 +1,6 @@ # Observability -Pass `--qlog ` to either side to emit xquic qlog. Per-path byte counts confirm that within-stream multipath is actually splitting a flow across paths — the key signal that aggregation is working. (xquic must be built with `XQC_ENABLE_EVENT_LOG=ON`, which `scripts/build-xquic.sh` does.) +Pass `--qlog ` to either side to emit xquic qlog. Per-path byte counts confirm that within-stream multipath is actually splitting a flow across paths — the key signal that aggregation is working. ## Metrics @@ -10,10 +10,14 @@ Pass `--qlog ` to either side to emit xquic qlog. Per-path byte counts conf ## Testing -After building, run the bundled test suite: +Run the test suite from the repository root: ```bash -ctest --test-dir build --output-on-failure +cargo test --workspace --locked +cargo clippy --workspace --all-targets --locked -- -D warnings +cargo build --release --locked -p mqproxy --bins --examples +bash tests/test_cli_help.sh target/release/mqproxy +bash tests/integration/e2e_udp.sh # also e2e_{gateway,multipath,tproxy,mitm_h2,...}.sh ``` -It covers wire framing, the relay/flow state machine, ingress parsing, the gateway request path, and the TLS MITM crypto core, alongside end-to-end scripts for multipath aggregation, the full gateway chain, UDP relay, and transparent capture. Tests that need root or `NET_ADMIN` (the multipath and transparent-capture end-to-end runs) skip automatically when run unprivileged. +The cargo tests cover wire framing, the relay/flow state machine, ingress parsing, the gateway request path, and TLS MITM. The `tests/integration/e2e_*.sh` scripts run end to end: multipath aggregation, the full gateway chain, UDP relay, transparent capture and MITM. Scripts that need root or `NET_ADMIN` skip themselves when run unprivileged. diff --git a/website/reference/options.md b/website/reference/options.md index 48831ee4..cb4b5c34 100644 --- a/website/reference/options.md +++ b/website/reference/options.md @@ -63,7 +63,7 @@ At least one client ingress flag (`--socks5`, `--http-connect`, `--gateway`, or | `--no-gateway` | **Disables HTTP Gateway mode for this server** (it is enabled by default). The TCP-proxy core keeps running; only the gateway origin bridge is turned off, so a client's `--gateway` ingress has nothing to talk to. | | `--origin-ca ` | Extra CA bundle for origin TLS verification (private CAs / tests); verification itself is always on | | `--request-metrics` | Emit one `mq.req` logfmt line per gateway request (method/status/target/ttfb/origin_protocol/cache/…). Opt-in; off by default. Independent of `--metrics-interval`. | -| `--cache-max-bytes ` | In-memory origin response cache bounded to `N` bytes (`0` = off = default; e.g. `67108864` = 64 MiB). Opt-in per request via `X-Mq-Cache`. | +| `--cache-max-bytes ` | Accepted for compatibility but ignored with a warning: the origin response cache was removed. | | `--masquerade` | Answer **unauthenticated** gateway requests with a bare `404` (no `X-Mq-*` headers), so probes/scanners see a generic HTTP/3 server instead of a fingerprintable `403 auth-failed`. Authenticated requests are unaffected. Gateway-only; off by default; recommended for internet-exposed servers. | **Client** diff --git a/website/reference/security-model.md b/website/reference/security-model.md index 63f83470..18184715 100644 --- a/website/reference/security-model.md +++ b/website/reference/security-model.md @@ -33,5 +33,6 @@ Use of mqproxy is at your own risk. Users are solely responsible for validating ## Acknowledgments - [XQUIC](https://github.com/alibaba/xquic) (Alibaba) — the QUIC/MPQUIC transport, via the [mp0rta fork](https://github.com/mp0rta/xquic). -- [BoringSSL](https://boringssl.googlesource.com/boringssl) — TLS backend. -- [nghttp2](https://nghttp2.org/) — HTTP/2 framing for the C build's TLS MITM ingress (the Rust binary uses the `h2` crate and rustls). +- [BoringSSL](https://boringssl.googlesource.com/boringssl) — TLS backend for the QUIC transport. +- [h3wire](https://crates.io/crates/h3wire) — HTTP/3 framing and QPACK. +- [hyper](https://hyper.rs/), [h2](https://crates.io/crates/h2) and [rustls](https://rustls.dev/) — origin HTTP, and the TLS/HTTP/2 side of the TLS MITM.