diff --git a/site/dashboard.html b/site/dashboard.html index 710692b..7b20e17 100644 --- a/site/dashboard.html +++ b/site/dashboard.html @@ -971,9 +971,11 @@ function renderBarChart(containerId, defs, data, total) { }).join(''); } -function encryptionPct(transport) { - const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1; - return (((transport.dot + transport.doh) / total) * 100).toFixed(0); +function encryptionPct(data, encryptedKeys, allKeys) { + const total = allKeys.reduce((s, k) => s + (data[k] || 0), 0); + if (total === 0) return 0; + const encrypted = encryptedKeys.reduce((s, k) => s + (data[k] || 0), 0); + return Math.round((encrypted / total) * 100); } const PATH_DEFS = [ @@ -1001,7 +1003,7 @@ const TRANSPORT_DEFS = [ function renderTransport(transport) { const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1; renderBarChart('transportBars', TRANSPORT_DEFS, transport, total); - const encPct = encryptionPct(transport); + const encPct = encryptionPct(transport, ['dot', 'doh'], ['udp', 'tcp', 'dot', 'doh']); const el = document.getElementById('transportEncrypted'); el.textContent = `${encPct}% encrypted inbound`; el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)'; @@ -1017,8 +1019,7 @@ const UPSTREAM_WIRE_DEFS = [ function renderUpstreamWire(ut) { const total = (ut.udp + ut.doh + ut.dot + ut.odoh) || 0; renderBarChart('upstreamWireBars', UPSTREAM_WIRE_DEFS, ut, total || 1); - const encrypted = ut.doh + ut.dot + ut.odoh; - const encPct = total > 0 ? Math.round((encrypted / total) * 100) : 0; + const encPct = encryptionPct(ut, ['doh', 'dot', 'odoh'], ['udp', 'doh', 'dot', 'odoh']); const el = document.getElementById('upstreamWireEncrypted'); el.textContent = total > 0 ? `${encPct}% encrypted outbound` : ''; el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)'; diff --git a/src/odoh.rs b/src/odoh.rs index 2cfa9c5..0901c94 100644 --- a/src/odoh.rs +++ b/src/odoh.rs @@ -25,7 +25,7 @@ use tokio::time::timeout; use crate::Result; /// MIME type used for both directions of the ODoH exchange (RFC 9230 §4). -const ODOH_CONTENT_TYPE: &str = "application/oblivious-dns-message"; +pub(crate) const ODOH_CONTENT_TYPE: &str = "application/oblivious-dns-message"; /// Cap on the response body we read into memory when the relay returns /// non-success. Protects against a hostile relay streaming a huge body on diff --git a/src/relay.rs b/src/relay.rs index 8d6ab40..122796e 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -20,10 +20,9 @@ use serde::Deserialize; use tokio::net::TcpListener; use crate::forward::build_https_client_with_pool; +use crate::odoh::ODOH_CONTENT_TYPE; use crate::Result; -const ODOH_CONTENT_TYPE: &str = "application/oblivious-dns-message"; - /// Cap on the opaque body we accept from a client. ODoH envelopes are /// ~100–300 bytes in practice; anything larger is malformed or hostile. const MAX_BODY_BYTES: usize = 4 * 1024; @@ -55,23 +54,30 @@ struct RelayState { rejected_bad_request: AtomicU64, } -pub async fn run(addr: SocketAddr) -> Result<()> { - let state = Arc::new(RelayState { - client: build_https_client_with_pool(RELAY_POOL_PER_HOST), - total_requests: AtomicU64::new(0), - forwarded_ok: AtomicU64::new(0), - forwarded_err: AtomicU64::new(0), - rejected_bad_request: AtomicU64::new(0), - }); +impl RelayState { + fn new() -> Arc { + Arc::new(RelayState { + client: build_https_client_with_pool(RELAY_POOL_PER_HOST), + total_requests: AtomicU64::new(0), + forwarded_ok: AtomicU64::new(0), + forwarded_err: AtomicU64::new(0), + rejected_bad_request: AtomicU64::new(0), + }) + } +} - let app = Router::new() +/// `DefaultBodyLimit` overrides axum's 2 MiB default so hostile clients +/// can't force the relay to buffer multi-MB bodies before our own cap. +fn build_app(state: Arc) -> Router { + Router::new() .route("/relay", post(handle_relay)) - // Overrides axum's default (2 MiB) so hostile clients can't force - // the relay to buffer multi-MB bodies before our own cap check. .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)) .route("/health", get(handle_health)) - .with_state(state); + .with_state(state) +} +pub async fn run(addr: SocketAddr) -> Result<()> { + let app = build_app(RelayState::new()); let listener = TcpListener::bind(addr).await?; info!("ODoH relay listening on {}", addr); axum::serve(listener, app).await?; @@ -199,19 +205,8 @@ mod tests { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - let state = Arc::new(RelayState { - client: build_https_client_with_pool(RELAY_POOL_PER_HOST), - total_requests: AtomicU64::new(0), - forwarded_ok: AtomicU64::new(0), - forwarded_err: AtomicU64::new(0), - rejected_bad_request: AtomicU64::new(0), - }); - - let app = Router::new() - .route("/relay", post(handle_relay)) - .layer(DefaultBodyLimit::max(MAX_BODY_BYTES)) - .route("/health", get(handle_health)) - .with_state(state.clone()); + let state = RelayState::new(); + let app = build_app(state.clone()); tokio::spawn(async move { let _ = axum::serve(listener, app).await;