1use std::net::{Ipv4Addr, SocketAddrV4};
21
22use socket2::{Domain, Protocol, Socket, Type};
23
24const MDNS_PORT: u16 = 5353;
25const MDNS_GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct BindFailure {
30 pub step: &'static str,
31 pub os_error: Option<i32>,
33 pub message: String,
34}
35
36fn fail(step: &'static str, e: std::io::Error) -> BindFailure {
37 BindFailure { step, os_error: e.raw_os_error(), message: e.to_string() }
38}
39
40pub fn probe_bind(ip: Ipv4Addr) -> Result<(), BindFailure> {
45 let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).map_err(|e| fail("create", e))?;
46 sock.set_reuse_address(true).map_err(|e| fail("reuse-address", e))?;
47 #[cfg(unix)]
48 sock.set_reuse_port(true).map_err(|e| fail("reuse-port", e))?;
49 let any = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, MDNS_PORT);
50 sock.bind(&any.into()).map_err(|e| fail("bind", e))?;
51 sock.join_multicast_v4(&MDNS_GROUP, &ip).map_err(|e| fail("join-group", e))?;
52 sock.set_multicast_if_v4(&ip).map_err(|e| fail("set-multicast-if", e))?;
53 let group = SocketAddrV4::new(MDNS_GROUP, MDNS_PORT);
54 sock.send_to(&[0u8; 12], &group.into()).map_err(|e| fail("send", e))?;
55 Ok(())
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct PortHolder {
61 pub pid: u32,
62 pub process: String,
63 pub address: String,
65}
66
67#[cfg(windows)]
70pub fn udp_5353_holders() -> Result<Vec<PortHolder>, String> {
71 windows_impl::holders()
72}
73
74#[cfg(not(windows))]
75pub fn udp_5353_holders() -> Result<Vec<PortHolder>, String> {
76 Err("holder lookup is only implemented on Windows".to_string())
77}
78
79#[cfg(windows)]
80mod windows_impl {
81 use std::net::{Ipv4Addr, Ipv6Addr};
82
83 use windows_sys::Win32::NetworkManagement::IpHelper::{
84 GetExtendedUdpTable, MIB_UDP6ROW_OWNER_PID, MIB_UDPROW_OWNER_PID, UDP_TABLE_OWNER_PID,
85 };
86 use windows_sys::Win32::Networking::WinSock::{AF_INET, AF_INET6};
87
88 use super::{PortHolder, MDNS_PORT};
89
90 const ERROR_INSUFFICIENT_BUFFER: u32 = 122;
91
92 fn table(family: u32) -> Result<Vec<u8>, String> {
94 let mut buf: Vec<u8> = Vec::new();
95 let mut size: u32 = 0;
96 for _ in 0..5 {
99 let rc = unsafe {
102 GetExtendedUdpTable(buf.as_mut_ptr().cast(), &mut size, 0, family, UDP_TABLE_OWNER_PID, 0)
103 };
104 match rc {
105 0 => {
106 buf.truncate(size as usize);
107 return Ok(buf);
108 }
109 ERROR_INSUFFICIENT_BUFFER => buf.resize(size as usize, 0),
110 other => return Err(format!("GetExtendedUdpTable failed with {other}")),
111 }
112 }
113 Err("the UDP table kept growing".to_string())
114 }
115
116 fn rows_v4(buf: &[u8]) -> Vec<(String, u16, u32)> {
118 let count = buf.get(..4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).unwrap_or(0) as usize;
119 let row = std::mem::size_of::<MIB_UDPROW_OWNER_PID>();
120 (0..count)
121 .filter_map(|i| {
122 let at = 4 + i * row;
123 if at + row > buf.len() {
124 return None;
125 }
126 let r = unsafe { std::ptr::read_unaligned(buf.as_ptr().add(at).cast::<MIB_UDPROW_OWNER_PID>()) };
128 let addr = Ipv4Addr::from(r.dwLocalAddr.to_ne_bytes()).to_string();
129 Some((addr, u16::from_be((r.dwLocalPort & 0xFFFF) as u16), r.dwOwningPid))
131 })
132 .collect()
133 }
134
135 fn rows_v6(buf: &[u8]) -> Vec<(String, u16, u32)> {
136 let count = buf.get(..4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).unwrap_or(0) as usize;
137 let row = std::mem::size_of::<MIB_UDP6ROW_OWNER_PID>();
138 (0..count)
139 .filter_map(|i| {
140 let at = 4 + i * row;
141 if at + row > buf.len() {
142 return None;
143 }
144 let r = unsafe { std::ptr::read_unaligned(buf.as_ptr().add(at).cast::<MIB_UDP6ROW_OWNER_PID>()) };
146 let addr = Ipv6Addr::from(r.ucLocalAddr).to_string();
147 Some((addr, u16::from_be((r.dwLocalPort & 0xFFFF) as u16), r.dwOwningPid))
148 })
149 .collect()
150 }
151
152 pub fn holders() -> Result<Vec<PortHolder>, String> {
153 let mut found: Vec<(String, u32)> = Vec::new();
154 for (family, v6) in [(AF_INET as u32, false), (AF_INET6 as u32, true)] {
155 let buf = table(family)?;
156 let rows = if v6 { rows_v6(&buf) } else { rows_v4(&buf) };
157 found.extend(rows.into_iter().filter(|(_, port, _)| *port == MDNS_PORT).map(|(a, _, pid)| (a, pid)));
158 }
159 let pids: Vec<sysinfo::Pid> = found.iter().map(|(_, pid)| sysinfo::Pid::from_u32(*pid)).collect();
160 let mut sys = sysinfo::System::new();
161 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&pids), false);
162 Ok(found
163 .into_iter()
164 .map(|(address, pid)| PortHolder {
165 pid,
166 process: sys
167 .process(sysinfo::Pid::from_u32(pid))
168 .map(|p| p.name().to_string_lossy().to_string())
169 .unwrap_or_else(|| "(exited or not visible)".to_string()),
170 address,
171 })
172 .collect())
173 }
174}
175
176const MAX_HOLDERS_NAMED: usize = 8;
178
179fn group_holders(holders: &[PortHolder], own_pid: u32) -> String {
183 let mut groups: Vec<((u32, &str, &str), usize)> = Vec::new();
184 for h in holders {
185 let key = (h.pid, h.process.as_str(), h.address.as_str());
186 match groups.iter_mut().find(|(k, _)| *k == key) {
187 Some((_, n)) => *n += 1,
188 None => groups.push((key, 1)),
189 }
190 }
191 groups.sort_by(|a, b| a.0.cmp(&b.0));
192 let shown: Vec<String> = groups
193 .iter()
194 .take(MAX_HOLDERS_NAMED)
195 .map(|((pid, process, address), n)| {
196 let me = if *pid == own_pid { ", this process" } else { "" };
197 let times = if *n > 1 { format!(" x{n}") } else { String::new() };
198 format!("{process} (pid {pid}{me}) on {address}{times}")
199 })
200 .collect();
201 let more = groups.len().saturating_sub(MAX_HOLDERS_NAMED);
202 let tail = if more > 0 { format!(", and {more} more" ) } else { String::new() };
203 format!("{}{tail}", shown.join(", "))
204}
205
206pub fn format_diagnosis(
208 probes: &[(Ipv4Addr, Result<(), BindFailure>)],
209 holders: &Result<Vec<PortHolder>, String>,
210 own_pid: u32,
211) -> String {
212 let probes_text = if probes.is_empty() {
213 "no interface to probe".to_string()
214 } else {
215 probes
216 .iter()
217 .map(|(ip, r)| match r {
218 Ok(()) => format!("{ip}: set-up works (so this was not a bind failure)"),
219 Err(f) => match f.os_error {
220 Some(code) => format!("{ip}: FAILED at {} (os error {code}: {})", f.step, f.message),
221 None => format!("{ip}: FAILED at {} ({})", f.step, f.message),
222 },
223 })
224 .collect::<Vec<_>>()
225 .join("; ")
226 };
227 let holders_text = match holders {
228 Err(why) => format!("holder lookup unavailable ({why})"),
229 Ok(h) if h.is_empty() => "nobody holds UDP 5353".to_string(),
230 Ok(h) => format!("UDP 5353 is held by {}", group_holders(h, own_pid)),
231 };
232 format!("{probes_text}. {holders_text}")
233}
234
235pub fn diagnose(missing: &[Ipv4Addr]) -> String {
238 let probes: Vec<(Ipv4Addr, Result<(), BindFailure>)> = missing.iter().map(|ip| (*ip, probe_bind(*ip))).collect();
239 format_diagnosis(&probes, &udp_5353_holders(), std::process::id())
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn ip(s: &str) -> Ipv4Addr {
247 s.parse().unwrap()
248 }
249
250 fn holder(pid: u32, process: &str, address: &str) -> PortHolder {
251 PortHolder { pid, process: process.to_string(), address: address.to_string() }
252 }
253
254 #[test]
257 fn the_area54_case_reads_as_one_line_naming_the_holders() {
258 let probes = vec![(
259 ip("192.168.1.26"),
260 Err(BindFailure { step: "bind", os_error: Some(10048), message: "Only one usage of each socket address".into() }),
261 )];
262 let holders = Ok(vec![
263 holder(23800, "chrome.exe", "::"),
264 holder(5648, "chrome.exe", "0.0.0.0"),
265 holder(2964, "svchost.exe", "0.0.0.0"),
266 ]);
267 let text = format_diagnosis(&probes, &holders, 10708);
268 assert!(text.contains("192.168.1.26: FAILED at bind (os error 10048"), "{text}");
269 assert!(text.contains("chrome.exe (pid 23800) on ::"), "{text}");
270 assert!(text.contains("svchost.exe (pid 2964) on 0.0.0.0"), "{text}");
271 assert!(!text.contains("this process"), "{text}");
272 }
273
274 #[test]
275 fn a_working_set_up_says_so_because_it_rules_out_the_bind() {
276 let text = format_diagnosis(&[(ip("192.168.1.26"), Ok(()))], &Ok(vec![holder(10708, "agentmux-srv.exe", "0.0.0.0")]), 10708);
277 assert!(text.contains("set-up works (so this was not a bind failure)"), "{text}");
278 assert!(text.contains("agentmux-srv.exe (pid 10708, this process) on 0.0.0.0"), "{text}");
279 }
280
281 #[test]
284 fn a_program_holding_many_sockets_is_one_entry_with_a_count_and_the_list_is_capped() {
285 let mut holders = Vec::new();
286 for _ in 0..9 {
287 holders.push(holder(29204, "chrome.exe", "0.0.0.0"));
288 }
289 holders.push(holder(11936, "node.exe", "0.0.0.0"));
290 holders.push(holder(11936, "node.exe", "0.0.0.0"));
291 let text = format_diagnosis(&[], &Ok(holders), 1);
292 assert!(text.contains("chrome.exe (pid 29204) on 0.0.0.0 x9"), "{text}");
293 assert!(text.contains("node.exe (pid 11936) on 0.0.0.0 x2"), "{text}");
294 assert_eq!(text.matches("chrome.exe").count(), 1, "{text}");
295
296 let many: Vec<PortHolder> = (1..=12).map(|pid| holder(pid, "svc.exe", "0.0.0.0")).collect();
297 let capped = format_diagnosis(&[], &Ok(many), 1);
298 assert!(capped.contains(", and 4 more"), "{capped}");
299 assert_eq!(capped.matches("svc.exe").count(), MAX_HOLDERS_NAMED, "{capped}");
300 }
301
302 #[test]
303 fn every_other_shape_still_reads_sensibly() {
304 assert!(format_diagnosis(&[], &Ok(vec![]), 1).contains("no interface to probe"));
305 assert!(format_diagnosis(&[], &Ok(vec![]), 1).contains("nobody holds UDP 5353"));
306 let unavailable = format_diagnosis(&[], &Err("only on Windows".into()), 1);
307 assert!(unavailable.contains("holder lookup unavailable (only on Windows)"), "{unavailable}");
308 let no_code = format_diagnosis(
309 &[(ip("10.0.0.5"), Err(BindFailure { step: "join-group", os_error: None, message: "weird".into() }))],
310 &Ok(vec![]),
311 1,
312 );
313 assert!(no_code.contains("FAILED at join-group (weird)"), "{no_code}");
314 }
315
316 #[test]
320 fn an_exclusive_holder_makes_the_probe_fail_at_the_bind_step() {
321 let holder = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
322 #[cfg(windows)]
324 {
325 use std::os::windows::io::AsRawSocket;
326 const SOL_SOCKET: i32 = 0xffff;
327 const SO_EXCLUSIVEADDRUSE: i32 = !4;
328 let on: i32 = 1;
329 let rc = unsafe {
331 windows_sys::Win32::Networking::WinSock::setsockopt(
332 holder.as_raw_socket() as usize,
333 SOL_SOCKET,
334 SO_EXCLUSIVEADDRUSE,
335 (&on as *const i32).cast(),
336 4,
337 )
338 };
339 assert_eq!(rc, 0, "set SO_EXCLUSIVEADDRUSE");
340 }
341 let any = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, MDNS_PORT);
342 if holder.bind(&any.into()).is_err() {
343 return;
347 }
348 let failure = probe_bind(Ipv4Addr::LOCALHOST).expect_err("an exclusive holder must make the bind fail");
349 assert_eq!(failure.step, "bind", "{failure:?}");
350 assert!(failure.os_error.is_some(), "the OS's own error code is kept: {failure:?}");
351 drop(holder);
352 }
353
354 #[test]
358 #[ignore = "reads this machine's sockets; run by hand"]
359 fn prints_the_diagnosis_for_this_machine() {
360 let ips: Vec<Ipv4Addr> = if_addrs::get_if_addrs()
361 .unwrap_or_default()
362 .into_iter()
363 .filter_map(|i| match i.ip() {
364 std::net::IpAddr::V4(v4) if !v4.is_loopback() && !v4.is_link_local() => Some(v4),
365 _ => None,
366 })
367 .collect();
368 println!("DIAGNOSIS: {}", diagnose(&ips));
369 }
370
371 #[cfg(windows)]
374 #[test]
375 fn the_holder_lookup_finds_this_process_on_the_port() {
376 let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
377 sock.set_reuse_address(true).unwrap();
378 let any = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, MDNS_PORT);
379 if sock.bind(&any.into()).is_err() {
380 return; }
382 let holders = udp_5353_holders().expect("the lookup works");
383 let me = holders.iter().find(|h| h.pid == std::process::id());
384 let me = me.unwrap_or_else(|| panic!("this process (pid {}) must be listed: {holders:?}", std::process::id()));
385 assert_eq!(me.address, "0.0.0.0");
386 assert!(!me.process.is_empty() && me.process != "(exited or not visible)", "{me:?}");
387 drop(sock);
388 }
389}