refactor(odoh): deduplicate post-audit findings

- Hoist ODOH_CONTENT_TYPE to a single pub(crate) constant in odoh.rs;
  relay.rs imports it instead of declaring its own.
- Generalize dashboard encryptionPct(data, encryptedKeys, allKeys)
  so both Inbound Wire and Outbound Wire panels share the same math
  instead of drifting independently.
- Extract RelayState::new() and build_app() helpers in relay.rs so
  the test spawn_relay() and production run() wire the same router
  + body-limit layer. Prevents future middleware from landing in one
  path but not the other.

All 344 lib tests pass; no behavior change.
This commit is contained in:
Razvan Dimescu
2026-04-20 16:03:34 +03:00
parent be60f6ccbc
commit eb5ea3b645
3 changed files with 30 additions and 34 deletions

View File

@@ -971,9 +971,11 @@ function renderBarChart(containerId, defs, data, total) {
}).join(''); }).join('');
} }
function encryptionPct(transport) { function encryptionPct(data, encryptedKeys, allKeys) {
const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1; const total = allKeys.reduce((s, k) => s + (data[k] || 0), 0);
return (((transport.dot + transport.doh) / total) * 100).toFixed(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 = [ const PATH_DEFS = [
@@ -1001,7 +1003,7 @@ const TRANSPORT_DEFS = [
function renderTransport(transport) { function renderTransport(transport) {
const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1; const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1;
renderBarChart('transportBars', TRANSPORT_DEFS, transport, total); 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'); const el = document.getElementById('transportEncrypted');
el.textContent = `${encPct}% encrypted inbound`; el.textContent = `${encPct}% encrypted inbound`;
el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)'; el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)';
@@ -1017,8 +1019,7 @@ const UPSTREAM_WIRE_DEFS = [
function renderUpstreamWire(ut) { function renderUpstreamWire(ut) {
const total = (ut.udp + ut.doh + ut.dot + ut.odoh) || 0; const total = (ut.udp + ut.doh + ut.dot + ut.odoh) || 0;
renderBarChart('upstreamWireBars', UPSTREAM_WIRE_DEFS, ut, total || 1); renderBarChart('upstreamWireBars', UPSTREAM_WIRE_DEFS, ut, total || 1);
const encrypted = ut.doh + ut.dot + ut.odoh; const encPct = encryptionPct(ut, ['doh', 'dot', 'odoh'], ['udp', 'doh', 'dot', 'odoh']);
const encPct = total > 0 ? Math.round((encrypted / total) * 100) : 0;
const el = document.getElementById('upstreamWireEncrypted'); const el = document.getElementById('upstreamWireEncrypted');
el.textContent = total > 0 ? `${encPct}% encrypted outbound` : ''; el.textContent = total > 0 ? `${encPct}% encrypted outbound` : '';
el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)'; el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)';

View File

@@ -25,7 +25,7 @@ use tokio::time::timeout;
use crate::Result; use crate::Result;
/// MIME type used for both directions of the ODoH exchange (RFC 9230 §4). /// 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 /// 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 /// non-success. Protects against a hostile relay streaming a huge body on

View File

@@ -20,10 +20,9 @@ use serde::Deserialize;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use crate::forward::build_https_client_with_pool; use crate::forward::build_https_client_with_pool;
use crate::odoh::ODOH_CONTENT_TYPE;
use crate::Result; 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 /// Cap on the opaque body we accept from a client. ODoH envelopes are
/// ~100300 bytes in practice; anything larger is malformed or hostile. /// ~100300 bytes in practice; anything larger is malformed or hostile.
const MAX_BODY_BYTES: usize = 4 * 1024; const MAX_BODY_BYTES: usize = 4 * 1024;
@@ -55,23 +54,30 @@ struct RelayState {
rejected_bad_request: AtomicU64, rejected_bad_request: AtomicU64,
} }
pub async fn run(addr: SocketAddr) -> Result<()> { impl RelayState {
let state = Arc::new(RelayState { fn new() -> Arc<Self> {
Arc::new(RelayState {
client: build_https_client_with_pool(RELAY_POOL_PER_HOST), client: build_https_client_with_pool(RELAY_POOL_PER_HOST),
total_requests: AtomicU64::new(0), total_requests: AtomicU64::new(0),
forwarded_ok: AtomicU64::new(0), forwarded_ok: AtomicU64::new(0),
forwarded_err: AtomicU64::new(0), forwarded_err: AtomicU64::new(0),
rejected_bad_request: 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<RelayState>) -> Router {
Router::new()
.route("/relay", post(handle_relay)) .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)) .layer(DefaultBodyLimit::max(MAX_BODY_BYTES))
.route("/health", get(handle_health)) .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?; let listener = TcpListener::bind(addr).await?;
info!("ODoH relay listening on {}", addr); info!("ODoH relay listening on {}", addr);
axum::serve(listener, app).await?; axum::serve(listener, app).await?;
@@ -199,19 +205,8 @@ mod tests {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
let state = Arc::new(RelayState { let state = RelayState::new();
client: build_https_client_with_pool(RELAY_POOL_PER_HOST), let app = build_app(state.clone());
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());
tokio::spawn(async move { tokio::spawn(async move {
let _ = axum::serve(listener, app).await; let _ = axum::serve(listener, app).await;