Skip to main content

nd_300/diagnostics/
dns_cache.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct DnsCacheEntry {
5    pub name: String,
6    pub record_type: String,
7    pub data: String,
8    pub ttl: Option<u32>,
9}
10
11pub async fn collect() -> Option<Vec<DnsCacheEntry>> {
12    #[cfg(windows)]
13    {
14        collect_windows().await
15    }
16
17    #[cfg(target_os = "macos")]
18    {
19        // macOS doesn't have an easy way to dump DNS cache
20        None
21    }
22
23    #[cfg(target_os = "linux")]
24    {
25        collect_linux().await
26    }
27}
28
29#[cfg(windows)]
30async fn collect_windows() -> Option<Vec<DnsCacheEntry>> {
31    let mut cmd = tokio::process::Command::new("ipconfig");
32    cmd.args(["/displaydns"]);
33    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
34
35    let text = String::from_utf8_lossy(&output.stdout);
36    let mut entries = Vec::new();
37    let mut current_name = String::new();
38    let mut current_type = String::new();
39    let mut current_ttl = None;
40
41    for line in text.lines() {
42        let line = line.trim();
43
44        if line.contains("Record Name") {
45            current_name = line
46                .split(':')
47                .nth(1)
48                .map(|s| s.trim().to_string())
49                .unwrap_or_default();
50        } else if line.contains("Record Type") {
51            let type_num: u32 = line
52                .split(':')
53                .nth(1)
54                .and_then(|s| s.trim().parse().ok())
55                .unwrap_or(0);
56            current_type = match type_num {
57                1 => "A",
58                5 => "CNAME",
59                28 => "AAAA",
60                12 => "PTR",
61                15 => "MX",
62                _ => "OTHER",
63            }
64            .to_string();
65        } else if line.contains("Time To Live") {
66            current_ttl = line.split(':').nth(1).and_then(|s| s.trim().parse().ok());
67        } else if line.contains("A (Host) Record")
68            || line.contains("CNAME Record")
69            || line.contains("AAAA Record")
70        {
71            let data = line
72                .split_once(':')
73                .map(|x| x.1)
74                .unwrap_or("")
75                .trim()
76                .to_string();
77            if !current_name.is_empty() {
78                entries.push(DnsCacheEntry {
79                    name: current_name.clone(),
80                    record_type: current_type.clone(),
81                    data,
82                    ttl: current_ttl,
83                });
84            }
85        }
86    }
87
88    // Limit to prevent overwhelming output
89    entries.truncate(50);
90
91    if entries.is_empty() {
92        None
93    } else {
94        Some(entries)
95    }
96}
97
98#[cfg(target_os = "linux")]
99async fn collect_linux() -> Option<Vec<DnsCacheEntry>> {
100    // Try resolvectl
101    let mut cmd = tokio::process::Command::new("resolvectl");
102    cmd.args(["statistics"]);
103    if let Some(output) = super::util::run_with_timeout(cmd, super::util::SLOW).await {
104        let text = String::from_utf8_lossy(&output.stdout);
105        if !text.is_empty() {
106            // resolvectl doesn't dump individual entries easily
107            // Just return stats-based info
108            let mut entries = Vec::new();
109            for line in text.lines() {
110                if line.contains("Current Cache Size") {
111                    entries.push(DnsCacheEntry {
112                        name: "Cache Statistics".to_string(),
113                        record_type: "INFO".to_string(),
114                        data: line.trim().to_string(),
115                        ttl: None,
116                    });
117                }
118            }
119            if !entries.is_empty() {
120                return Some(entries);
121            }
122        }
123    }
124
125    None
126}