feat: transport protocol tracking (UDP/TCP/DoT/DoH) with dashboard visualization
Thread Transport enum through resolve pipeline, record per-query transport in stats and query log. Dashboard gets bar chart panel with encryption %, transport column in query log, and filter dropdown.
This commit is contained in:
17
src/api.rs
17
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<Arc<ServerCtx>>) -> Json<StatsResponse> {
|
||||
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,
|
||||
|
||||
@@ -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<ServerCtx>,
|
||||
transport: Transport,
|
||||
) -> crate::Result<BytePacketBuffer> {
|
||||
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<ServerCtx>,
|
||||
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?;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
src/dot.rs
11
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<S>(
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
43
src/stats.rs
43
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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user