diff --git a/site/dashboard.html b/site/dashboard.html
index fa2d965..710692b 100644
--- a/site/dashboard.html
+++ b/site/dashboard.html
@@ -228,6 +228,7 @@ body {
.path-bar-fill.tcp { background: var(--violet); }
.path-bar-fill.dot { background: var(--emerald); }
.path-bar-fill.doh { background: var(--teal); }
+.path-bar-fill.odoh { background: var(--violet-dim); }
.path-pct {
font-family: var(--font-mono);
font-size: 0.75rem;
@@ -637,16 +638,26 @@ body {
-
+
+
+
+
@@ -992,7 +1003,24 @@ function renderTransport(transport) {
renderBarChart('transportBars', TRANSPORT_DEFS, transport, total);
const encPct = encryptionPct(transport);
const el = document.getElementById('transportEncrypted');
- el.textContent = `${encPct}% encrypted`;
+ el.textContent = `${encPct}% encrypted inbound`;
+ el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)';
+}
+
+const UPSTREAM_WIRE_DEFS = [
+ { key: 'udp', label: 'UDP', cls: 'udp' },
+ { key: 'doh', label: 'DoH', cls: 'doh' },
+ { key: 'dot', label: 'DoT', cls: 'dot' },
+ { key: 'odoh', label: 'ODoH', cls: 'odoh' },
+];
+
+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 el = document.getElementById('upstreamWireEncrypted');
+ el.textContent = total > 0 ? `${encPct}% encrypted outbound` : '';
el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)';
}
@@ -1234,6 +1262,7 @@ async function refresh() {
// Panels
renderPaths(q);
renderTransport(stats.transport);
+ renderUpstreamWire(stats.upstream_transport || { udp: 0, doh: 0, dot: 0, odoh: 0 });
renderQueryLog(logs);
renderOverrides(overrides);
renderCache(cache);
diff --git a/src/main.rs b/src/main.rs
index e077a2f..8f9fecf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -66,7 +66,17 @@ fn main() -> numa::Result<()> {
.as_deref()
.and_then(|s| s.parse().ok())
.unwrap_or(8443);
- let addr: std::net::SocketAddr = ([127, 0, 0, 1], port).into();
+ let bind: std::net::IpAddr = std::env::args()
+ .nth(3)
+ .as_deref()
+ .map(|s| {
+ s.parse().unwrap_or_else(|e| {
+ eprintln!("invalid bind address '{}': {}", s, e);
+ std::process::exit(1);
+ })
+ })
+ .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
+ let addr = std::net::SocketAddr::new(bind, port);
eprintln!(
"\x1b[1;38;2;192;98;58mNuma\x1b[0m — ODoH relay on {}\n",
addr
@@ -107,7 +117,8 @@ fn main() -> numa::Result<()> {
eprintln!(" service status Check if the service is running");
eprintln!(" lan on Enable LAN service discovery (mDNS)");
eprintln!(" lan off Disable LAN service discovery");
- eprintln!(" relay [PORT] Run as an ODoH relay (RFC 9230, default port 8443)");
+ eprintln!(" relay [PORT] [BIND]");
+ eprintln!(" Run as an ODoH relay (RFC 9230, default 127.0.0.1:8443)");
eprintln!(" setup-phone Generate a QR code to install Numa DoT on a phone");
eprintln!(" help Show this help");
eprintln!();