nd_300/diagnostics/
ipv6.rs1use serde::Serialize;
2
3use super::shared_cache::SharedCache;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct Ipv6Info {
7 pub available: bool,
8 pub addresses: Vec<Ipv6Address>,
9 pub connectivity: Ipv6Connectivity,
10 pub dual_stack: bool,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct Ipv6Address {
15 pub interface: String,
16 pub address: String,
17 pub scope: String,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub enum Ipv6Connectivity {
22 Full,
23 LinkLocal,
24 None,
25}
26
27pub async fn collect_with_cache(cache: &SharedCache) -> Option<Ipv6Info> {
28 let addresses = get_ipv6_addresses_cached(cache).await;
29 let has_global = addresses.iter().any(|a| a.scope == "global");
30 let has_link_local = addresses.iter().any(|a| a.scope == "link-local");
31
32 let connectivity = if has_global {
33 test_ipv6_connectivity().await
34 } else if has_link_local {
35 Ipv6Connectivity::LinkLocal
36 } else {
37 Ipv6Connectivity::None
38 };
39
40 let dual_stack = has_global;
41
42 Some(Ipv6Info {
43 available: !addresses.is_empty(),
44 addresses,
45 connectivity,
46 dual_stack,
47 })
48}
49
50async fn get_ipv6_addresses_cached(cache: &SharedCache) -> Vec<Ipv6Address> {
51 #[cfg(windows)]
52 {
53 if let Some(ref ic) = cache.ipconfig {
55 return parse_ipv6_from_ipconfig(&ic.raw);
56 }
57 }
58 let _ = cache;
59 get_ipv6_addresses().await
60}
61
62#[cfg(windows)]
63fn parse_ipv6_from_ipconfig(text: &str) -> Vec<Ipv6Address> {
64 let mut addrs = Vec::new();
65 let mut current_iface = String::new();
66
67 for line in text.lines() {
68 if !line.starts_with(' ') && !line.starts_with('\t') && line.contains("adapter") {
69 current_iface = line.trim().trim_end_matches(':').to_string();
70 }
71
72 let trimmed = line.trim();
73 if trimmed.contains("IPv6 Address")
74 || trimmed.contains("Link-local IPv6")
75 || trimmed.contains("Temporary IPv6")
76 {
77 if let Some(addr) = trimmed
78 .split(':')
79 .skip(1)
80 .collect::<Vec<&str>>()
81 .join(":")
82 .trim()
83 .strip_suffix("(Preferred)")
84 {
85 let scope = if trimmed.contains("Link-local") {
86 "link-local"
87 } else {
88 "global"
89 };
90 addrs.push(Ipv6Address {
91 interface: current_iface.clone(),
92 address: addr.trim().to_string(),
93 scope: scope.to_string(),
94 });
95 } else {
96 let addr: String = trimmed.split(':').skip(1).collect::<Vec<&str>>().join(":");
97 let addr = addr.trim().trim_end_matches("(Preferred)").trim();
98 if !addr.is_empty() {
99 let scope = if trimmed.contains("Link-local") {
100 "link-local"
101 } else {
102 "global"
103 };
104 addrs.push(Ipv6Address {
105 interface: current_iface.clone(),
106 address: addr.to_string(),
107 scope: scope.to_string(),
108 });
109 }
110 }
111 }
112 }
113
114 addrs
115}
116
117pub async fn collect() -> Option<Ipv6Info> {
118 let addresses = get_ipv6_addresses().await;
119 let has_global = addresses.iter().any(|a| a.scope == "global");
120 let has_link_local = addresses.iter().any(|a| a.scope == "link-local");
121
122 let connectivity = if has_global {
124 test_ipv6_connectivity().await
125 } else if has_link_local {
126 Ipv6Connectivity::LinkLocal
127 } else {
128 Ipv6Connectivity::None
129 };
130
131 let dual_stack = has_global;
132
133 Some(Ipv6Info {
134 available: !addresses.is_empty(),
135 addresses,
136 connectivity,
137 dual_stack,
138 })
139}
140
141async fn get_ipv6_addresses() -> Vec<Ipv6Address> {
142 let mut addrs = Vec::new();
143
144 #[cfg(windows)]
145 {
146 let cmd = tokio::process::Command::new("ipconfig");
147 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
148 let text = String::from_utf8_lossy(&output.stdout);
149 let mut current_iface = String::new();
150
151 for line in text.lines() {
152 if !line.starts_with(' ') && !line.starts_with('\t') && line.contains("adapter") {
153 current_iface = line.trim().trim_end_matches(':').to_string();
154 }
155
156 let trimmed = line.trim();
157 if trimmed.contains("IPv6 Address")
158 || trimmed.contains("Link-local IPv6")
159 || trimmed.contains("Temporary IPv6")
160 {
161 if let Some(addr) = trimmed
162 .split(':')
163 .skip(1)
164 .collect::<Vec<&str>>()
165 .join(":")
166 .trim()
167 .strip_suffix("(Preferred)")
168 {
169 let scope = if trimmed.contains("Link-local") {
170 "link-local"
171 } else {
172 "global"
173 };
174 addrs.push(Ipv6Address {
175 interface: current_iface.clone(),
176 address: addr.trim().to_string(),
177 scope: scope.to_string(),
178 });
179 } else {
180 let addr: String =
181 trimmed.split(':').skip(1).collect::<Vec<&str>>().join(":");
182 let addr = addr.trim().trim_end_matches("(Preferred)").trim();
183 if !addr.is_empty() {
184 let scope = if trimmed.contains("Link-local") {
185 "link-local"
186 } else {
187 "global"
188 };
189 addrs.push(Ipv6Address {
190 interface: current_iface.clone(),
191 address: addr.to_string(),
192 scope: scope.to_string(),
193 });
194 }
195 }
196 }
197 }
198 }
199 }
200
201 #[cfg(unix)]
202 {
203 let mut cmd = tokio::process::Command::new("ip");
204 cmd.args(["-6", "addr", "show"]);
205 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
206 let text = String::from_utf8_lossy(&output.stdout);
207 let mut current_iface = String::new();
208
209 for line in text.lines() {
210 let trimmed = line.trim();
211 if !line.starts_with(' ') {
212 current_iface = trimmed.split(':').nth(1).unwrap_or("").trim().to_string();
213 } else if trimmed.starts_with("inet6") {
214 let parts: Vec<&str> = trimmed.split_whitespace().collect();
215 if parts.len() >= 4 {
216 let addr = parts[1].split('/').next().unwrap_or(parts[1]);
217 let scope = parts.get(3).unwrap_or(&"unknown").to_string();
218 addrs.push(Ipv6Address {
219 interface: current_iface.clone(),
220 address: addr.to_string(),
221 scope,
222 });
223 }
224 }
225 }
226 }
227
228 if addrs.is_empty() {
230 let cmd = tokio::process::Command::new("ifconfig");
231 if let Some(output) = super::util::run_with_timeout(cmd, super::util::QUICK).await {
232 let text = String::from_utf8_lossy(&output.stdout);
233 let mut current_iface = String::new();
234
235 for line in text.lines() {
236 if !line.starts_with('\t') && !line.starts_with(' ') {
237 current_iface = line.split(':').next().unwrap_or("").to_string();
238 } else if line.contains("inet6") {
239 let parts: Vec<&str> = line.split_whitespace().collect();
240 if let Some(addr) = parts.get(1) {
241 let scope = if addr.starts_with("fe80") {
242 "link-local"
243 } else if *addr == "::1" {
244 "loopback"
245 } else {
246 "global"
247 };
248 addrs.push(Ipv6Address {
249 interface: current_iface.clone(),
250 address: addr.to_string(),
251 scope: scope.to_string(),
252 });
253 }
254 }
255 }
256 }
257 }
258 }
259
260 addrs
261}
262
263async fn test_ipv6_connectivity() -> Ipv6Connectivity {
264 match tokio::time::timeout(
266 std::time::Duration::from_secs(5),
267 tokio::net::TcpStream::connect("[2001:4860:4860::8888]:443"),
268 )
269 .await
270 {
271 Ok(Ok(_)) => Ipv6Connectivity::Full,
272 _ => {
273 match tokio::time::timeout(
275 std::time::Duration::from_secs(3),
276 tokio::net::TcpStream::connect("[2606:4700:4700::1111]:443"),
277 )
278 .await
279 {
280 Ok(Ok(_)) => Ipv6Connectivity::Full,
281 _ => Ipv6Connectivity::None,
282 }
283 }
284 }
285}