Skip to main content

nd_300/diagnostics/
routing_table.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct RouteEntry {
5    pub destination: String,
6    pub gateway: String,
7    pub mask: String,
8    pub interface: String,
9    pub metric: Option<u32>,
10    pub flags: Option<String>,
11}
12
13pub async fn collect() -> Option<Vec<RouteEntry>> {
14    #[cfg(windows)]
15    {
16        collect_windows().await
17    }
18
19    #[cfg(target_os = "macos")]
20    {
21        collect_macos().await
22    }
23
24    #[cfg(target_os = "linux")]
25    {
26        collect_linux().await
27    }
28}
29
30#[cfg(windows)]
31async fn collect_windows() -> Option<Vec<RouteEntry>> {
32    let mut cmd = tokio::process::Command::new("route");
33    cmd.args(["print", "-4"]);
34    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
35
36    let text = String::from_utf8_lossy(&output.stdout);
37    let mut entries = Vec::new();
38    let mut in_routes = false;
39
40    for line in text.lines() {
41        let line = line.trim();
42        if line.starts_with("Network Destination") {
43            in_routes = true;
44            continue;
45        }
46        if line.starts_with("=") || line.is_empty() {
47            if in_routes && !entries.is_empty() {
48                break;
49            }
50            continue;
51        }
52
53        if in_routes {
54            let parts: Vec<&str> = line.split_whitespace().collect();
55            if parts.len() >= 4 {
56                entries.push(RouteEntry {
57                    destination: parts[0].to_string(),
58                    mask: parts[1].to_string(),
59                    gateway: parts[2].to_string(),
60                    interface: parts[3].to_string(),
61                    metric: parts.get(4).and_then(|s| s.parse().ok()),
62                    flags: None,
63                });
64            }
65        }
66    }
67
68    Some(entries)
69}
70
71#[cfg(target_os = "macos")]
72async fn collect_macos() -> Option<Vec<RouteEntry>> {
73    let mut cmd = tokio::process::Command::new("netstat");
74    cmd.args(["-rn", "-f", "inet"]);
75    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
76
77    let text = String::from_utf8_lossy(&output.stdout);
78    Some(parse_macos_routes(&text))
79}
80
81#[cfg(any(target_os = "macos", test))]
82fn parse_macos_routes(text: &str) -> Vec<RouteEntry> {
83    let mut entries = Vec::new();
84    for line in text.lines() {
85        if line.starts_with("Destination")
86            || line.starts_with("Routing")
87            || line.starts_with("Internet")
88        {
89            continue;
90        }
91        let parts: Vec<&str> = line.split_whitespace().collect();
92        // Darwin's columns are Destination Gateway Flags Netif [Expire].
93        // `Expire` is optional and often `!`; taking the final field therefore
94        // produced bogus interface names in technician output.
95        if parts.len() >= 4 && !parts[0].ends_with(':') {
96            entries.push(RouteEntry {
97                destination: parts[0].to_string(),
98                gateway: parts[1].to_string(),
99                mask: String::new(),
100                flags: Some(parts[2].to_string()),
101                interface: parts[3].to_string(),
102                metric: None,
103            });
104        }
105    }
106
107    entries
108}
109
110#[cfg(target_os = "linux")]
111async fn collect_linux() -> Option<Vec<RouteEntry>> {
112    let mut cmd = tokio::process::Command::new("ip");
113    cmd.args(["route", "show"]);
114    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
115
116    let text = String::from_utf8_lossy(&output.stdout);
117    let mut entries = Vec::new();
118
119    for line in text.lines() {
120        let parts: Vec<&str> = line.split_whitespace().collect();
121        if parts.is_empty() {
122            continue;
123        }
124
125        let dest = parts[0].to_string();
126        let mut gateway = String::new();
127        let mut iface = String::new();
128        let mut metric = None;
129
130        let mut i = 1;
131        while i < parts.len() {
132            match parts[i] {
133                "via" if i + 1 < parts.len() => {
134                    gateway = parts[i + 1].to_string();
135                    i += 1;
136                }
137                "dev" if i + 1 < parts.len() => {
138                    iface = parts[i + 1].to_string();
139                    i += 1;
140                }
141                "metric" if i + 1 < parts.len() => {
142                    metric = parts[i + 1].parse().ok();
143                    i += 1;
144                }
145                _ => {}
146            }
147            i += 1;
148        }
149
150        entries.push(RouteEntry {
151            destination: dest,
152            gateway,
153            mask: String::new(),
154            interface: iface,
155            metric,
156            flags: None,
157        });
158    }
159
160    Some(entries)
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn macos_netif_is_not_expire_column() {
169        let fixture = "Routing tables\n\nInternet:\nDestination Gateway Flags Netif Expire\ndefault 10.1.0.1 UGScg en0\n10.1.0.1 d4:6a:91:ba:18:5c UHLWIir en0 1200\n10.1.0.46 link#11 UHLWI en0 !\n";
170        let routes = parse_macos_routes(fixture);
171        assert_eq!(routes.len(), 3);
172        assert!(routes.iter().all(|route| route.interface == "en0"));
173    }
174}