Skip to main content

dns_sd_native/
register.rs

1use std::num::NonZeroU32;
2use thiserror::Error;
3
4#[cfg(all(unix, not(target_os = "macos")))]
5pub use crate::linux::ServiceRegistration;
6#[cfg(target_os = "macos")]
7pub use crate::macos::ServiceRegistration;
8#[cfg(target_os = "windows")]
9pub use crate::windows::ServiceRegistration;
10
11/// Builder for DNS-SD service registrations.
12///
13/// See [RFC 6763](https://datatracker.ietf.org/doc/html/rfc6763) for details on the semantics of the various parameters.
14#[derive(Debug, Clone)]
15pub struct ServiceRegistrationBuilder {
16    pub(crate) service_type: String,
17    pub(crate) port: u16,
18    pub(crate) name: Option<String>,
19    pub(crate) host: Option<String>,
20    pub(crate) domain: Option<String>,
21    pub(crate) interface_index: Option<NonZeroU32>,
22    pub(crate) txt_record: Vec<(String, TxtRecordValue)>,
23}
24
25impl ServiceRegistrationBuilder {
26    /// Initializes a new service registration builder with the given service type and port.
27    ///
28    /// - `service_type`:
29    ///   The service type followed by the protocol, separated by a dot
30    ///   (e.g. `_ftp._tcp`). The service type must be an underscore, followed
31    ///   by 1-15 characters, which may be letters, digits, or hyphens.
32    ///   The transport protocol must be `_tcp` or `_udp`. New service types
33    ///   should be registered at <http://www.dns-sd.org/ServiceTypes.html>.
34    ///   
35    /// - `port`: The UDP/TCP port on which the service accepts connections.
36    ///
37    ///   Pass 0 for a "placeholder" service (i.e. a service that will not be discovered
38    ///   by browsing, but will cause a name conflict if another client tries to
39    ///   register that same name). Most clients will not use placeholder services.
40    pub fn new(service_type: impl AsRef<str>, port: u16) -> Self {
41        Self {
42            service_type: service_type.as_ref().to_string(),
43            port,
44            name: None,
45            host: None,
46            domain: None,
47            interface_index: None,
48            txt_record: Vec::new(),
49        }
50    }
51
52    /// Specifies the service name to be registered.
53    ///
54    /// Most applications will not specify a name, in which case the computer
55    /// name is used. If a name is specified, it must be 1-63 bytes of UTF-8 text.
56    pub fn name(&mut self, name: impl AsRef<str>) -> &mut Self {
57        self.name = Some(name.as_ref().to_string());
58        self
59    }
60
61    fn add_txt_record_key_value(&mut self, key: String, value: TxtRecordValue) {
62        self.txt_record.retain_mut(|(k, _v)| *k != key);
63        self.txt_record.push((key, value));
64    }
65
66    /// Adds a TXT record key to the service registration, with an empty value.
67    pub fn add_txt_record_key_empty(&mut self, key: impl AsRef<str>) -> &mut Self {
68        let key = key.as_ref().to_string();
69        self.add_txt_record_key_value(key, TxtRecordValue::KeyOnly);
70        self
71    }
72
73    /// Adds a TXT record key/value pair to the service registration.
74    ///
75    /// _Windows limitation:_ if `value` is an empty string, it will be treated as a key-only pair.
76    pub fn add_txt_record_key_string(
77        &mut self,
78        key: impl AsRef<str>,
79        value: impl AsRef<str>,
80    ) -> &mut Self {
81        let key = key.as_ref().to_string();
82        let value = value.as_ref().to_string();
83        self.add_txt_record_key_value(key, TxtRecordValue::String(value));
84        self
85    }
86
87    /// Adds a TXT record key/value pair to the service registration, with a binary value.
88    #[cfg(not(target_os = "windows"))] // Windows does not support binary TXT record values
89    pub fn add_txt_record_key_binary(
90        &mut self,
91        key: impl AsRef<str>,
92        value: impl AsRef<[u8]>,
93    ) -> &mut Self {
94        let key = key.as_ref().to_string();
95        let value = value.as_ref().to_vec();
96        self.add_txt_record_key_value(key, TxtRecordValue::Binary(value));
97        self
98    }
99
100    /// Specifies the interface on which to register the service
101    /// (the index for a given interface is determined via the `if_nametoindex()`
102    /// family of calls.)
103    ///
104    /// Most applications will not specify an interface, instead automatically
105    /// registering on all available interfaces.
106    pub fn interface_index(&mut self, index: NonZeroU32) -> &mut Self {
107        self.interface_index = Some(index);
108        self
109    }
110
111    /// Set the SRV target host name.
112    ///
113    /// Most applications will not specify a host, instead automatically using the machine's default
114    /// host name(s).
115    ///
116    /// Note that specifying a host does NOT create an address record for that host.
117    pub fn host(&mut self, host: impl AsRef<str>) -> &mut Self {
118        self.host = Some(host.as_ref().to_string());
119        self
120    }
121
122    /// Set the domain on which to advertise the service.
123    ///
124    /// Most applications will not specify a domain, instead automatically
125    /// registering in the default domain(s).
126    pub fn domain(&mut self, domain: impl AsRef<str>) -> &mut Self {
127        self.domain = Some(domain.as_ref().to_string());
128        self
129    }
130
131    /// Registers the service with the system, making it discoverable by remote clients.
132    pub async fn register(&self) -> Result<ServiceRegistration, ServiceRegistrationError> {
133        validate_txt_records(&self.txt_record)?;
134        ServiceRegistration::new(
135            &self.service_type,
136            self.port,
137            &self.name,
138            &self.host,
139            &self.domain,
140            self.interface_index,
141            &self.txt_record,
142        )
143        .await
144    }
145}
146
147/// Error type for service registration failures.
148#[derive(Error, Debug)]
149pub enum ServiceRegistrationError {
150    /// A TXT record key is invalid (empty, contains '=', or has non-ASCII characters).
151    #[error("invalid TXT record key {0:?}: {1}")]
152    InvalidTxtRecordKey(String, String),
153
154    /// A TXT record value is too large (key + value exceeds 255 bytes).
155    #[error("invalid TXT record value for key {0:?}: {1}")]
156    InvalidTxtRecordValue(String, String),
157
158    /// A string parameter contains an interior NUL byte.
159    #[error("parameter {0:?} contains interior nul byte at position {1}")]
160    ParameterContainsInteriorNulByte(String, usize),
161
162    /// The interface index is not valid.
163    #[error("interface index {0} is invalid")]
164    InvalidInterfaceIndex(u32),
165
166    /// The hostname could not be determined automatically.
167    #[error("hostname not set and could not be determined automatically: {0}")]
168    HostnameUnavailable(String),
169
170    /// DNS-SD not available on system (Linux only - either D-Bus or Avahi unavailable).
171    #[error("DNS-SD not available on system: {0}")]
172    DnsSdUnavailable(String),
173
174    /// The native DNS-SD API returned an error.
175    #[error("DNS-SD registration returned an error: {0}")]
176    RegistrationError(String),
177
178    /// The registration failed.
179    #[error("registration failed: {0}")]
180    RegistrationFailed(String),
181
182    /// A service name conflict was detected.
183    #[error("service name conflict")]
184    NameConflict,
185}
186
187#[derive(Debug, Clone)]
188pub(crate) enum TxtRecordValue {
189    KeyOnly,
190    String(String),
191    #[cfg(not(target_os = "windows"))] // Windows does not support binary TXT record values
192    Binary(Vec<u8>),
193}
194
195/// Validates that all TXT record key/value pairs conform to DNS-SD rules.
196pub(crate) fn validate_txt_records(
197    records: &[(String, TxtRecordValue)],
198) -> Result<(), ServiceRegistrationError> {
199    for (key, value) in records {
200        if key.is_empty() {
201            return Err(ServiceRegistrationError::InvalidTxtRecordKey(
202                key.clone(),
203                "key must not be empty".into(),
204            ));
205        }
206        if key.contains('=') {
207            return Err(ServiceRegistrationError::InvalidTxtRecordKey(
208                key.clone(),
209                "key must not contain '='".into(),
210            ));
211        }
212        // The characters of "Key" MUST be printable US-ASCII values
213        // (0x20-0x7E) [RFC 20], excluding '=' (0x3D).
214        if key.chars().any(|c| !(' '..='~').contains(&c)) {
215            return Err(ServiceRegistrationError::InvalidTxtRecordKey(
216                key.clone(),
217                "key must contain only printable US-ASCII characters (0x20-0x7E)".into(),
218            ));
219        }
220        let value_len = match value {
221            TxtRecordValue::KeyOnly => 0,
222            TxtRecordValue::String(s) => 1 + s.len(),
223            #[cfg(not(target_os = "windows"))]
224            TxtRecordValue::Binary(b) => 1 + b.len(),
225        };
226        if key.len() + value_len > 255 {
227            return Err(ServiceRegistrationError::InvalidTxtRecordValue(
228                key.clone(),
229                "key + value must not exceed 255 bytes".into(),
230            ));
231        }
232    }
233    Ok(())
234}