Skip to main content

ipp_printer_app/
mdns.rs

1//! mDNS / DNS-SD advertising for IPP printers (`_ipp._tcp.local.`).
2//!
3//! Gated by the default-on `mdns` feature. [`Advertiser::register_all`]
4//! publishes one service instance per printer in the registry, with the TXT
5//! records CUPS / cups-browsed need for IPP-Everywhere auto-discovery
6//! (RFC 8011 + Bonjour for IPP + PWG 5100.14).
7
8use std::collections::HashMap;
9
10use mdns_sd::{ServiceDaemon, ServiceInfo};
11
12use crate::printer::PrinterRegistry;
13
14const IPP_SERVICE: &str = "_ipp._tcp.local.";
15
16/// Holds the [`ServiceDaemon`] and the list of registered fullnames so we can
17/// unregister cleanly on drop.
18pub struct Advertiser {
19    daemon: ServiceDaemon,
20    fullnames: Vec<String>,
21}
22
23impl Advertiser {
24    /// Start a daemon and register every printer in the registry.
25    pub fn register_all(registry: &PrinterRegistry, port: u16) -> mdns_sd::Result<Self> {
26        let daemon = ServiceDaemon::new()?;
27        let host = hostname();
28        let mut fullnames = Vec::new();
29        for rec in registry.read().iter() {
30            let info = service_info(&host, port, &rec.config.name, &rec.config.make_and_model)?;
31            let fullname = info.get_fullname().to_string();
32            daemon.register(info)?;
33            log::info!("mdns: registered {fullname}");
34            fullnames.push(fullname);
35        }
36        Ok(Self { daemon, fullnames })
37    }
38}
39
40impl Drop for Advertiser {
41    fn drop(&mut self) {
42        for fullname in &self.fullnames {
43            let _ = self.daemon.unregister(fullname);
44        }
45        let _ = self.daemon.shutdown();
46    }
47}
48
49fn hostname() -> String {
50    let h = std::process::Command::new("hostname")
51        .output()
52        .ok()
53        .and_then(|o| String::from_utf8(o.stdout).ok())
54        .map(|s| s.trim().to_string())
55        .filter(|s| !s.is_empty())
56        .unwrap_or_else(|| "localhost".to_string());
57    // mdns-sd normalises trailing ".local." — pass bare hostname.
58    h
59}
60
61fn service_info(
62    host: &str,
63    port: u16,
64    name: &str,
65    make_and_model: &str,
66) -> mdns_sd::Result<ServiceInfo> {
67    let mut txt: HashMap<String, String> = HashMap::new();
68    txt.insert("rp".into(), format!("ipp/print/{name}"));
69    txt.insert("ty".into(), make_and_model.to_string());
70    txt.insert("note".into(), make_and_model.to_string());
71    txt.insert("product".into(), format!("({make_and_model})"));
72    // Document formats CUPS asks for during driverless setup.
73    txt.insert(
74        "pdl".into(),
75        "image/pwg-raster,application/vnd.cups-raster,application/octet-stream".into(),
76    );
77    // IPP Everywhere advertises URF=…; CUPS reads this for the everywhere driver.
78    txt.insert("URF".into(), "W8,SRGB24,CP1,RS203".into());
79    txt.insert("Color".into(), "F".into());
80    txt.insert("Duplex".into(), "F".into());
81    txt.insert("adminurl".into(), format!("http://{host}.local:{port}/"));
82    txt.insert("priority".into(), "0".into());
83    txt.insert("qtotal".into(), "1".into());
84    // TXT version per PWG 5100.14.
85    txt.insert("txtvers".into(), "1".into());
86
87    let info = ServiceInfo::new(
88        IPP_SERVICE,
89        name,
90        &format!("{host}.local."),
91        "", // IPs filled by enable_addr_auto
92        port,
93        txt,
94    )?
95    .enable_addr_auto();
96    Ok(info)
97}