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.
This commit is contained in:
2026-10-29 10:52:50 +00:00
parent 29c85fa449
commit e0a6ccd2c1
2 changed files with 42 additions and 8 deletions
+37 -8
View File
@@ -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<UdpSocket> {
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<Ipv4Addr> = 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)?)
}