diff --git a/site/dashboard.html b/site/dashboard.html
index 5fa9777..2d9cc60 100644
--- a/site/dashboard.html
+++ b/site/dashboard.html
@@ -223,6 +223,10 @@ body {
.path-bar-fill.override { background: var(--emerald); }
.path-bar-fill.error { background: var(--rose); }
.path-bar-fill.blocked { background: var(--text-dim); }
+.path-bar-fill.udp { background: var(--text-dim); }
+.path-bar-fill.tcp { background: var(--violet); }
+.path-bar-fill.dot { background: var(--emerald); }
+.path-bar-fill.doh { background: var(--teal); }
.path-pct {
font-family: var(--font-mono);
font-size: 0.75rem;
@@ -288,6 +292,10 @@ body {
.path-tag.SERVFAIL { background: rgba(181, 68, 58, 0.12); color: var(--rose); }
.path-tag.BLOCKED { background: rgba(163, 152, 136, 0.15); color: var(--text-dim); }
.path-tag.COALESCED { background: rgba(138, 104, 158, 0.12); color: var(--violet-dim); }
+.path-tag.UDP { background: rgba(163, 152, 136, 0.15); color: var(--text-dim); }
+.path-tag.TCP { background: rgba(100, 116, 139, 0.12); color: var(--violet-dim); }
+.path-tag.DOT { background: rgba(82, 122, 82, 0.12); color: var(--emerald); }
+.path-tag.DOH { background: rgba(107, 124, 78, 0.12); color: var(--teal); }
.src-tag { font-size: 0.6rem; color: var(--text-dim); letter-spacing: 0.02em; }
/* Sidebar panels */
@@ -622,6 +630,16 @@ body {
+
+
+
@@ -643,6 +661,14 @@ body {
+
@@ -654,6 +680,7 @@ body {
Type |
Domain |
Path |
+ Transport |
Result |
Latency |
@@ -907,6 +934,27 @@ function renderMemory(mem, stats) {
`;
}
+function renderBarChart(containerId, defs, data, total) {
+ total = total || 1;
+ document.getElementById(containerId).innerHTML = defs.map(d => {
+ const count = data[d.key] || 0;
+ const pct = ((count / total) * 100).toFixed(1);
+ return `
+
+
${d.label}
+
+
${pct}%
+
`;
+ }).join('');
+}
+
+function encryptionPct(transport) {
+ const total = (transport.udp + transport.tcp + transport.dot + transport.doh) || 1;
+ return (((transport.dot + transport.doh) / total) * 100).toFixed(0);
+}
+
const PATH_DEFS = [
{ key: 'forwarded', label: 'Forward', cls: 'forward' },
{ key: 'recursive', label: 'Recursive', cls: 'recursive' },
@@ -918,20 +966,23 @@ const PATH_DEFS = [
];
function renderPaths(queries) {
- const total = queries.total || 1;
- const container = document.getElementById('pathBars');
- container.innerHTML = PATH_DEFS.map(p => {
- const count = queries[p.key] || 0;
- const pct = ((count / total) * 100).toFixed(1);
- return `
-
-
${p.label}
-
-
${pct}%
-
`;
- }).join('');
+ renderBarChart('pathBars', PATH_DEFS, queries, queries.total);
+}
+
+const TRANSPORT_DEFS = [
+ { key: 'udp', label: 'UDP', cls: 'udp' },
+ { key: 'tcp', label: 'TCP', cls: 'tcp' },
+ { key: 'dot', label: 'DoT', cls: 'dot' },
+ { key: 'doh', label: 'DoH', cls: 'doh' },
+];
+
+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 el = document.getElementById('transportEncrypted');
+ el.textContent = `${encPct}% encrypted`;
+ el.style.color = encPct >= 80 ? 'var(--emerald)' : encPct >= 50 ? 'var(--amber)' : 'var(--rose)';
}
function renderQueryLog(entries) {
@@ -942,6 +993,7 @@ function renderQueryLog(entries) {
function applyLogFilter() {
const domainFilter = document.getElementById('logFilterDomain').value.trim().toLowerCase();
const pathFilter = document.getElementById('logFilterPath').value;
+ const transportFilter = document.getElementById('logFilterTransport').value;
let filtered = lastLogEntries;
if (domainFilter) {
@@ -950,6 +1002,9 @@ function applyLogFilter() {
if (pathFilter) {
filtered = filtered.filter(e => e.path === pathFilter);
}
+ if (transportFilter) {
+ filtered = filtered.filter(e => e.transport === transportFilter);
+ }
const tbody = document.getElementById('queryLogBody');
document.getElementById('queryCount').textContent =
@@ -967,6 +1022,7 @@ function applyLogFilter() {
${e.query_type} |
${e.domain}${allowBtn} |
${e.path} |
+ ${e.transport} |
${e.dnssec === 'secure' ? '' : ''}${e.rescode} |
${e.latency_ms.toFixed(1)}ms |
`;
@@ -1141,11 +1197,13 @@ async function refresh() {
// QPS calculation
const now = Date.now();
+ const encPct = encryptionPct(stats.transport);
if (prevTotal !== null && prevTime !== null) {
const dt = (now - prevTime) / 1000;
const dq = q.total - prevTotal;
const qps = dt > 0 ? (dq / dt).toFixed(1) : '0.0';
- document.getElementById('qps').textContent = `~${qps}/s`;
+ const encTag = q.total > 0 ? ` ยท ${encPct}% enc` : '';
+ document.getElementById('qps').textContent = `~${qps}/s${encTag}`;
}
prevTotal = q.total;
prevTime = now;
@@ -1157,6 +1215,7 @@ async function refresh() {
// Panels
renderPaths(q);
+ renderTransport(stats.transport);
renderQueryLog(logs);
renderOverrides(overrides);
renderCache(cache);
diff --git a/src/api.rs b/src/api.rs
index 9aa3f60..fcc0bd9 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -152,6 +152,7 @@ struct QueryLogResponse {
domain: String,
query_type: String,
path: String,
+ transport: String,
rescode: String,
latency_ms: f64,
dnssec: String,
@@ -167,6 +168,7 @@ struct StatsResponse {
dnssec: bool,
srtt: bool,
queries: QueriesStats,
+ transport: TransportStats,
cache: CacheStats,
overrides: OverrideStats,
blocking: BlockingStatsResponse,
@@ -175,6 +177,14 @@ struct StatsResponse {
memory: MemoryStats,
}
+#[derive(Serialize)]
+struct TransportStats {
+ udp: u64,
+ tcp: u64,
+ dot: u64,
+ doh: u64,
+}
+
#[derive(Serialize)]
struct MobileStatsResponse {
enabled: bool,
@@ -483,6 +493,7 @@ async fn query_log(
domain: e.domain.clone(),
query_type: e.query_type.as_str().to_string(),
path: e.path.as_str().to_string(),
+ transport: e.transport.as_str().to_string(),
rescode: e.rescode.as_str().to_string(),
latency_ms: e.latency_us as f64 / 1000.0,
dnssec: e.dnssec.as_str().to_string(),
@@ -545,6 +556,12 @@ async fn stats(State(ctx): State>) -> Json {
blocked: snap.blocked,
errors: snap.errors,
},
+ transport: TransportStats {
+ udp: snap.transport_udp,
+ tcp: snap.transport_tcp,
+ dot: snap.transport_dot,
+ doh: snap.transport_doh,
+ },
cache: CacheStats {
entries: cache_len,
max_entries: cache_max,
diff --git a/src/ctx.rs b/src/ctx.rs
index e97a7ea..65b76d3 100644
--- a/src/ctx.rs
+++ b/src/ctx.rs
@@ -27,7 +27,7 @@ use crate::question::QueryType;
use crate::record::DnsRecord;
use crate::service_store::ServiceStore;
use crate::srtt::SrttCache;
-use crate::stats::{QueryPath, ServerStats};
+use crate::stats::{QueryPath, ServerStats, Transport};
use crate::system_dns::ForwardingRule;
pub struct ServerCtx {
@@ -87,6 +87,7 @@ pub async fn resolve_query(
raw_wire: &[u8],
src_addr: SocketAddr,
ctx: &Arc,
+ transport: Transport,
) -> crate::Result {
let start = Instant::now();
@@ -354,7 +355,7 @@ pub async fn resolve_query(
// Record stats and query log
{
let mut s = ctx.stats.lock().unwrap();
- let total = s.record(path);
+ let total = s.record(path, transport);
if total.is_multiple_of(1000) {
s.log_summary();
}
@@ -366,6 +367,7 @@ pub async fn resolve_query(
domain: qname,
query_type: qtype,
path,
+ transport,
rescode: response.header.rescode,
latency_us: elapsed.as_micros() as u64,
dnssec,
@@ -445,6 +447,7 @@ pub async fn handle_query(
raw_len: usize,
src_addr: SocketAddr,
ctx: &Arc,
+ transport: Transport,
) -> crate::Result<()> {
let query = match DnsPacket::from_buffer(&mut buffer) {
Ok(packet) => packet,
@@ -453,7 +456,7 @@ pub async fn handle_query(
return Ok(());
}
};
- match resolve_query(query, &buffer.buf[..raw_len], src_addr, ctx).await {
+ match resolve_query(query, &buffer.buf[..raw_len], src_addr, ctx, transport).await {
Ok(resp_buffer) => {
ctx.socket.send_to(resp_buffer.filled(), src_addr).await?;
}
diff --git a/src/doh.rs b/src/doh.rs
index bc4ba95..7325688 100644
--- a/src/doh.rs
+++ b/src/doh.rs
@@ -10,6 +10,7 @@ use crate::buffer::BytePacketBuffer;
use crate::ctx::{resolve_query, ServerCtx};
use crate::header::ResultCode;
use crate::packet::DnsPacket;
+use crate::stats::Transport;
const MAX_DNS_MSG: usize = 4096;
const DOH_CONTENT_TYPE: &str = "application/dns-message";
@@ -86,7 +87,7 @@ async fn resolve_doh(
let query_rd = query.header.recursion_desired;
let questions = query.questions.clone();
- match resolve_query(query, dns_bytes, src, ctx).await {
+ match resolve_query(query, dns_bytes, src, ctx, Transport::Doh).await {
Ok(resp_buffer) => {
let min_ttl = extract_min_ttl(resp_buffer.filled());
dns_response(resp_buffer.filled(), min_ttl)
diff --git a/src/dot.rs b/src/dot.rs
index d4eeb95..e883e0b 100644
--- a/src/dot.rs
+++ b/src/dot.rs
@@ -15,6 +15,7 @@ use crate::config::DotConfig;
use crate::ctx::{resolve_query, ServerCtx};
use crate::header::ResultCode;
use crate::packet::DnsPacket;
+use crate::stats::Transport;
const MAX_CONNECTIONS: usize = 512;
const IDLE_TIMEOUT: Duration = Duration::from_secs(30);
@@ -201,7 +202,15 @@ async fn handle_dot_connection(
}
};
- match resolve_query(query.clone(), &buffer.buf[..msg_len], remote_addr, ctx).await {
+ match resolve_query(
+ query.clone(),
+ &buffer.buf[..msg_len],
+ remote_addr,
+ ctx,
+ Transport::Dot,
+ )
+ .await
+ {
Ok(resp_buffer) => {
if write_framed(&mut stream, resp_buffer.filled())
.await
diff --git a/src/main.rs b/src/main.rs
index 1ec7791..bce7add 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,7 +15,7 @@ use numa::forward::{parse_upstream, Upstream, UpstreamPool};
use numa::override_store::OverrideStore;
use numa::query_log::QueryLog;
use numa::service_store::ServiceStore;
-use numa::stats::ServerStats;
+use numa::stats::{ServerStats, Transport};
use numa::system_dns::{
discover_system_dns, install_service, restart_service, service_status, uninstall_service,
};
@@ -610,7 +610,7 @@ async fn main() -> numa::Result<()> {
};
let ctx = Arc::clone(&ctx);
tokio::spawn(async move {
- if let Err(e) = handle_query(buffer, len, src_addr, &ctx).await {
+ if let Err(e) = handle_query(buffer, len, src_addr, &ctx, Transport::Udp).await {
error!("{} | HANDLER ERROR | {}", src_addr, e);
}
});
diff --git a/src/query_log.rs b/src/query_log.rs
index 1dc2d17..8ce4a6e 100644
--- a/src/query_log.rs
+++ b/src/query_log.rs
@@ -5,7 +5,7 @@ use std::time::SystemTime;
use crate::cache::DnssecStatus;
use crate::header::ResultCode;
use crate::question::QueryType;
-use crate::stats::QueryPath;
+use crate::stats::{QueryPath, Transport};
pub struct QueryLogEntry {
pub timestamp: SystemTime,
@@ -13,6 +13,7 @@ pub struct QueryLogEntry {
pub domain: String,
pub query_type: QueryType,
pub path: QueryPath,
+ pub transport: Transport,
pub rescode: ResultCode,
pub latency_us: u64,
pub dnssec: DnssecStatus,
@@ -107,6 +108,7 @@ mod tests {
domain: "example.com".into(),
query_type: QueryType::A,
path: QueryPath::Forwarded,
+ transport: Transport::Udp,
rescode: ResultCode::NOERROR,
latency_us: 500,
dnssec: DnssecStatus::Indeterminate,
diff --git a/src/stats.rs b/src/stats.rs
index c1a176f..feae945 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -97,9 +97,32 @@ pub struct ServerStats {
queries_local: u64,
queries_overridden: u64,
upstream_errors: u64,
+ transport_udp: u64,
+ transport_tcp: u64,
+ transport_dot: u64,
+ transport_doh: u64,
started_at: Instant,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Transport {
+ Udp,
+ Tcp,
+ Dot,
+ Doh,
+}
+
+impl Transport {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Transport::Udp => "UDP",
+ Transport::Tcp => "TCP",
+ Transport::Dot => "DOT",
+ Transport::Doh => "DOH",
+ }
+ }
+}
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryPath {
Local,
@@ -167,11 +190,15 @@ impl ServerStats {
queries_local: 0,
queries_overridden: 0,
upstream_errors: 0,
+ transport_udp: 0,
+ transport_tcp: 0,
+ transport_dot: 0,
+ transport_doh: 0,
started_at: Instant::now(),
}
}
- pub fn record(&mut self, path: QueryPath) -> u64 {
+ pub fn record(&mut self, path: QueryPath, transport: Transport) -> u64 {
self.queries_total += 1;
match path {
QueryPath::Local => self.queries_local += 1,
@@ -183,6 +210,12 @@ impl ServerStats {
QueryPath::Overridden => self.queries_overridden += 1,
QueryPath::UpstreamError => self.upstream_errors += 1,
}
+ match transport {
+ Transport::Udp => self.transport_udp += 1,
+ Transport::Tcp => self.transport_tcp += 1,
+ Transport::Dot => self.transport_dot += 1,
+ Transport::Doh => self.transport_doh += 1,
+ }
self.queries_total
}
@@ -206,6 +239,10 @@ impl ServerStats {
overridden: self.queries_overridden,
blocked: self.queries_blocked,
errors: self.upstream_errors,
+ transport_udp: self.transport_udp,
+ transport_tcp: self.transport_tcp,
+ transport_dot: self.transport_dot,
+ transport_doh: self.transport_doh,
}
}
@@ -242,4 +279,8 @@ pub struct StatsSnapshot {
pub overridden: u64,
pub blocked: u64,
pub errors: u64,
+ pub transport_udp: u64,
+ pub transport_tcp: u64,
+ pub transport_dot: u64,
+ pub transport_doh: u64,
}