nd_300/diagnostics/
routing_table.rs1use 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 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}