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;