diff --git a/Cargo.toml b/Cargo.toml index 290946d..d425b83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,3 +87,8 @@ flate2 = "1.1.10" # src/uplink/download.rs). Features are the bare minimum: TLS via rustls, no # openssl-sys; no json/gzip/cookies/etc, since the body is an opaque binary. reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] } +# The local IPv4 addresses, so the SSDP listener can join the multicast group +# on every interface rather than the one the OS happens to pick (see +# `bind_ssdp`). Windows PCs routinely have VPN/Hyper-V/WSL adapters. +# MIT OR BSD-3-Clause - checked against docs/LICENSES.md before adding. +if-addrs = "0.15" diff --git a/src/discovery/mod.rs b/src/discovery/mod.rs index 15d35cf..fec10e5 100644 --- a/src/discovery/mod.rs +++ b/src/discovery/mod.rs @@ -80,7 +80,7 @@ use std::net::{Ipv4Addr, SocketAddr, UdpSocket as StdUdpSocket}; use std::time::Duration; use tokio::net::UdpSocket; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; /// The SSDP multicast group, which is the standard one - Bambu's discovery is /// ordinary SSDP with vendor headers, not a protocol of its own. @@ -523,18 +523,47 @@ pub async fn listen_ssdp(port: u16, window: Duration, raw: bool) -> anyhow::Resu Ok(found) } -/// Binds the SSDP port and joins the group on every interface. +/// Binds the SSDP port and joins the group on every IPv4 interface. /// -/// `Ipv4Addr::UNSPECIFIED` as the interface asks the kernel to pick, which is -/// right for a gateway appliance: it may well have two interfaces and only -/// the operator knows which one faces the printers. +/// Passing `UNSPECIFIED` as the interface asks the OS to pick one, and that +/// is the wrong default on a machine with several: a Windows PC with Wi-Fi +/// plus a VPN, Hyper-V or WSL adapter joins on whichever it prefers, and +/// then hears nothing from printers on the other while looking perfectly +/// healthy. Only the operator knows which one faces the printers, so join on +/// all of them and say which were joined. Falls back to the OS's choice if +/// the interfaces cannot be listed. fn bind_ssdp(port: u16) -> anyhow::Result { let socket = StdUdpSocket::bind((Ipv4Addr::UNSPECIFIED, port)).map_err(|err| { anyhow::anyhow!("could not bind UDP :{port} for discovery: {err} (is another process using it?)") })?; - socket - .join_multicast_v4(&SSDP_GROUP, &Ipv4Addr::UNSPECIFIED) - .map_err(|err| anyhow::anyhow!("could not join {SSDP_GROUP} on :{port}: {err}"))?; + + let mut interfaces: Vec = if_addrs::get_if_addrs() + .unwrap_or_default() + .into_iter() + .filter(|interface| !interface.is_loopback()) + .filter_map(|interface| match interface.ip() { + std::net::IpAddr::V4(v4) => Some(v4), + std::net::IpAddr::V6(_) => None, + }) + .collect(); + if interfaces.is_empty() { + interfaces.push(Ipv4Addr::UNSPECIFIED); + } + + let mut joined = Vec::new(); + for interface in &interfaces { + match socket.join_multicast_v4(&SSDP_GROUP, interface) { + Ok(()) => joined.push(*interface), + // One adapter that refuses (down, no multicast) is not a reason to + // stop listening on the others. + Err(err) => debug!(%interface, ?err, "could not join the SSDP group on this interface"), + } + } + if joined.is_empty() { + anyhow::bail!("could not join {SSDP_GROUP} on :{port} on any of {interfaces:?}"); + } + info!(port, interfaces = ?joined, "listening for SSDP"); + socket.set_nonblocking(true)?; Ok(UdpSocket::from_std(socket)?) }