1use std::collections::HashMap;
9use std::net::IpAddr;
10
11use mdns_sd::{ServiceDaemon, ServiceInfo};
12use parking_lot::Mutex;
13
14use crate::printer::{PrinterRecord, PrinterRegistry};
15use crate::status::AdvertiserControl;
16
17const IPP_SERVICE: &str = "_ipp._tcp.local.";
18
19const VIRTUAL_IFACE_PREFIXES: &[&str] =
31 &["veth", "docker", "br-", "virbr", "vnet", "vmnet", "vboxnet"];
32
33pub struct Advertiser {
37 daemon: ServiceDaemon,
38 port: u16,
39 host: String,
40 addrs: Vec<IpAddr>,
41 registered: Mutex<HashMap<String, String>>,
43}
44
45impl Advertiser {
46 pub fn register_all(registry: &PrinterRegistry, port: u16) -> mdns_sd::Result<Self> {
48 let me = Self {
49 daemon: ServiceDaemon::new()?,
50 port,
51 host: hostname(),
52 addrs: advertise_addrs(),
53 registered: Mutex::new(HashMap::new()),
54 };
55 for rec in registry.read().iter() {
56 me.register_one(rec)?;
57 }
58 Ok(me)
59 }
60
61 fn register_one(&self, rec: &PrinterRecord) -> mdns_sd::Result<()> {
64 let info = service_info(
65 &self.host,
66 &self.addrs,
67 self.port,
68 rec.config.display_label(),
69 &rec.config.name,
70 &rec.config.make_and_model,
71 &rec.uuid,
72 )?;
73 let fullname = info.get_fullname().to_string();
74 self.daemon.register(info)?;
75 log::info!("mdns: registered {fullname}");
76 self.registered
77 .lock()
78 .insert(rec.config.name.clone(), fullname);
79 Ok(())
80 }
81}
82
83impl AdvertiserControl for Advertiser {
84 fn publish(&self, rec: &PrinterRecord) {
85 if !self.registered.lock().contains_key(&rec.config.name) {
86 if let Err(e) = self.register_one(rec) {
87 log::warn!("mdns: republish of {} failed: {e}", rec.config.name);
88 }
89 }
90 }
91
92 fn withdraw(&self, name: &str) {
93 if let Some(fullname) = self.registered.lock().remove(name) {
94 log::info!("mdns: withdrawing {fullname} (device offline)");
95 let _ = self.daemon.unregister(&fullname);
96 }
97 }
98
99 fn is_advertised(&self, name: &str) -> bool {
100 self.registered.lock().contains_key(name)
101 }
102}
103
104impl Drop for Advertiser {
105 fn drop(&mut self) {
106 for fullname in self.registered.lock().values() {
107 let _ = self.daemon.unregister(fullname);
108 }
109 let _ = self.daemon.shutdown();
110 }
111}
112
113fn is_virtual_iface(name: &str) -> bool {
116 VIRTUAL_IFACE_PREFIXES.iter().any(|p| name.starts_with(p))
117}
118
119fn advertise_addrs() -> Vec<IpAddr> {
126 let ifaces = match if_addrs::get_if_addrs() {
127 Ok(i) => i,
128 Err(e) => {
129 log::warn!("mdns: interface enumeration failed ({e}); advertising on all interfaces");
130 return Vec::new();
131 }
132 };
133 let mut addrs = Vec::new();
134 for iface in ifaces {
135 if iface.is_loopback() || iface.is_link_local() || !iface.is_oper_up() {
136 continue;
137 }
138 if is_virtual_iface(&iface.name) {
139 log::debug!(
140 "mdns: skipping virtual interface {} ({})",
141 iface.name,
142 iface.ip()
143 );
144 continue;
145 }
146 log::debug!("mdns: advertising on {} ({})", iface.name, iface.ip());
147 addrs.push(iface.ip());
148 }
149 addrs
150}
151
152fn hostname() -> String {
153 let h = std::process::Command::new("hostname")
154 .output()
155 .ok()
156 .and_then(|o| String::from_utf8(o.stdout).ok())
157 .map(|s| s.trim().to_string())
158 .filter(|s| !s.is_empty())
159 .unwrap_or_else(|| "localhost".to_string());
160 h
162}
163
164fn service_info(
165 host: &str,
166 addrs: &[IpAddr],
167 port: u16,
168 instance_name: &str,
169 logical_name: &str,
170 make_and_model: &str,
171 uuid: &str,
172) -> mdns_sd::Result<ServiceInfo> {
173 let mut txt: HashMap<String, String> = HashMap::new();
174 txt.insert("rp".into(), format!("ipp/print/{logical_name}"));
177 let bare_uuid = uuid.strip_prefix("urn:uuid:").unwrap_or(uuid);
182 if !bare_uuid.is_empty() {
183 txt.insert("UUID".into(), bare_uuid.to_string());
184 }
185 txt.insert("ty".into(), make_and_model.to_string());
186 txt.insert("note".into(), make_and_model.to_string());
187 txt.insert("product".into(), format!("({make_and_model})"));
188 txt.insert(
190 "pdl".into(),
191 "image/pwg-raster,application/vnd.cups-raster,application/octet-stream".into(),
192 );
193 txt.insert("URF".into(), "W8,SRGB24,CP1,RS203".into());
195 txt.insert("Color".into(), "F".into());
196 txt.insert("Duplex".into(), "F".into());
197 txt.insert("adminurl".into(), format!("http://{host}.local:{port}/"));
198 txt.insert("priority".into(), "0".into());
199 txt.insert("qtotal".into(), "1".into());
200 txt.insert("txtvers".into(), "1".into());
202
203 let info = if addrs.is_empty() {
208 ServiceInfo::new(
209 IPP_SERVICE,
210 instance_name,
211 &format!("{host}.local."),
212 "", port,
214 txt,
215 )?
216 .enable_addr_auto()
217 } else {
218 ServiceInfo::new(
219 IPP_SERVICE,
220 instance_name,
221 &format!("{host}.local."),
222 addrs,
223 port,
224 txt,
225 )?
226 };
227 Ok(info)
228}
229
230#[cfg(test)]
231mod tests {
232 use super::is_virtual_iface;
233
234 #[test]
235 fn flags_container_and_vm_interfaces() {
236 for name in [
237 "veth1a2b3c", "docker0", "br-9f3c1d20a", "virbr0", "vnet3", "vmnet8", "vboxnet0", ] {
245 assert!(is_virtual_iface(name), "{name} should be filtered out");
246 }
247 }
248
249 #[test]
250 fn keeps_real_interfaces() {
251 for name in ["eth0", "enp3s0", "wlan0", "wlp2s0", "br0", "tun0", "lo"] {
254 assert!(!is_virtual_iface(name), "{name} should be kept");
255 }
256 }
257}