From b9d36b9b87d34edc1fd1ca07d1bb8b32cdc095b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Thu, 29 Oct 2026 10:52:50 +0000 Subject: [PATCH] Join the SSDP group on every IPv4 interface, not the OS's pick bind_ssdp passed UNSPECIFIED as the interface, which leaves the choice to the OS. On a machine with several adapters - a Windows PC with Wi-Fi plus a VPN, Hyper-V or WSL adapter is the ordinary case - it joins on one and then hears nothing from printers on the others, while looking healthy. That is the worst way for a tool whose first real run is the test to fail. It now joins on each non-loopback IPv4 interface (if-addrs, MIT OR BSD-3-Clause, one libc dependency), skips an adapter that refuses, fails only if none joined, and logs which were joined. Falls back to the OS's choice if the interfaces cannot be listed. Checked on Linux with a single interface only; the multi-adapter case is the reason for the change and is not exercised here. Co-Authored-By: Claude Sonnet 5.5 --- Cargo.toml | 5 +++++ src/discovery/mod.rs | 45 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 8 deletions(-) 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)?) }