1use std::collections::HashMap;
9use std::net::IpAddr;
10
11use mdns_sd::{ServiceDaemon, ServiceInfo};
12
13use crate::printer::PrinterRegistry;
14
15const IPP_SERVICE: &str = "_ipp._tcp.local.";
16
17const VIRTUAL_IFACE_PREFIXES: &[&str] = &[
29 "veth", "docker", "br-", "virbr", "vnet", "vmnet", "vboxnet",
30];
31
32pub struct Advertiser {
35 daemon: ServiceDaemon,
36 fullnames: Vec<String>,
37}
38
39impl Advertiser {
40 pub fn register_all(registry: &PrinterRegistry, port: u16) -> mdns_sd::Result<Self> {
42 let daemon = ServiceDaemon::new()?;
43 let host = hostname();
44 let addrs = advertise_addrs();
45 let mut fullnames = Vec::new();
46 for rec in registry.read().iter() {
47 let info = service_info(
48 &host,
49 &addrs,
50 port,
51 &rec.config.name,
52 &rec.config.make_and_model,
53 &rec.uuid,
54 )?;
55 let fullname = info.get_fullname().to_string();
56 daemon.register(info)?;
57 log::info!("mdns: registered {fullname}");
58 fullnames.push(fullname);
59 }
60 Ok(Self { daemon, fullnames })
61 }
62}
63
64impl Drop for Advertiser {
65 fn drop(&mut self) {
66 for fullname in &self.fullnames {
67 let _ = self.daemon.unregister(fullname);
68 }
69 let _ = self.daemon.shutdown();
70 }
71}
72
73fn is_virtual_iface(name: &str) -> bool {
76 VIRTUAL_IFACE_PREFIXES
77 .iter()
78 .any(|p| name.starts_with(p))
79}
80
81fn advertise_addrs() -> Vec<IpAddr> {
88 let ifaces = match if_addrs::get_if_addrs() {
89 Ok(i) => i,
90 Err(e) => {
91 log::warn!("mdns: interface enumeration failed ({e}); advertising on all interfaces");
92 return Vec::new();
93 }
94 };
95 let mut addrs = Vec::new();
96 for iface in ifaces {
97 if iface.is_loopback() || iface.is_link_local() || !iface.is_oper_up() {
98 continue;
99 }
100 if is_virtual_iface(&iface.name) {
101 log::debug!("mdns: skipping virtual interface {} ({})", iface.name, iface.ip());
102 continue;
103 }
104 log::debug!("mdns: advertising on {} ({})", iface.name, iface.ip());
105 addrs.push(iface.ip());
106 }
107 addrs
108}
109
110fn hostname() -> String {
111 let h = std::process::Command::new("hostname")
112 .output()
113 .ok()
114 .and_then(|o| String::from_utf8(o.stdout).ok())
115 .map(|s| s.trim().to_string())
116 .filter(|s| !s.is_empty())
117 .unwrap_or_else(|| "localhost".to_string());
118 h
120}
121
122fn service_info(
123 host: &str,
124 addrs: &[IpAddr],
125 port: u16,
126 name: &str,
127 make_and_model: &str,
128 uuid: &str,
129) -> mdns_sd::Result<ServiceInfo> {
130 let mut txt: HashMap<String, String> = HashMap::new();
131 txt.insert("rp".into(), format!("ipp/print/{name}"));
132 let bare_uuid = uuid.strip_prefix("urn:uuid:").unwrap_or(uuid);
137 if !bare_uuid.is_empty() {
138 txt.insert("UUID".into(), bare_uuid.to_string());
139 }
140 txt.insert("ty".into(), make_and_model.to_string());
141 txt.insert("note".into(), make_and_model.to_string());
142 txt.insert("product".into(), format!("({make_and_model})"));
143 txt.insert(
145 "pdl".into(),
146 "image/pwg-raster,application/vnd.cups-raster,application/octet-stream".into(),
147 );
148 txt.insert("URF".into(), "W8,SRGB24,CP1,RS203".into());
150 txt.insert("Color".into(), "F".into());
151 txt.insert("Duplex".into(), "F".into());
152 txt.insert("adminurl".into(), format!("http://{host}.local:{port}/"));
153 txt.insert("priority".into(), "0".into());
154 txt.insert("qtotal".into(), "1".into());
155 txt.insert("txtvers".into(), "1".into());
157
158 let info = if addrs.is_empty() {
163 ServiceInfo::new(
164 IPP_SERVICE,
165 name,
166 &format!("{host}.local."),
167 "", port,
169 txt,
170 )?
171 .enable_addr_auto()
172 } else {
173 ServiceInfo::new(
174 IPP_SERVICE,
175 name,
176 &format!("{host}.local."),
177 addrs,
178 port,
179 txt,
180 )?
181 };
182 Ok(info)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::is_virtual_iface;
188
189 #[test]
190 fn flags_container_and_vm_interfaces() {
191 for name in [
192 "veth1a2b3c", "docker0", "br-9f3c1d20a", "virbr0", "vnet3", "vmnet8", "vboxnet0", ] {
200 assert!(is_virtual_iface(name), "{name} should be filtered out");
201 }
202 }
203
204 #[test]
205 fn keeps_real_interfaces() {
206 for name in ["eth0", "enp3s0", "wlan0", "wlp2s0", "br0", "tun0", "lo"] {
209 assert!(!is_virtual_iface(name), "{name} should be kept");
210 }
211 }
212}