Skip to content

Commit

Permalink
Allow non-standard max length for a service name (#96)
Browse files Browse the repository at this point in the history
A new method `ServiceDaemon.set_service_name_len_max()` is added to support setting a custom max length for the service name.

A public constant SERVICE_NAME_LEN_MAX_DEFAULT is defined for the default / standard value.

Service name length error reporting (a trade-off): it will no longer return Error directly from register() method for service name length errors. Instead, it will be reported in error log and DaemonEvent::Error received by any monitors. The register itself will not succeed in such error cases. See the test case changes for an example.
  • Loading branch information
keepsimple1 authored Mar 6, 2023
1 parent a4e5c57 commit a07f550
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt;

/// A basic error type from this library.
#[derive(Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
/// Like a classic EAGAIN. The receiver should retry.
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ mod service_daemon;
mod service_info;

pub use error::{Error, Result};
pub use service_daemon::{DaemonEvent, Metrics, ServiceDaemon, ServiceEvent, UnregisterStatus};
pub use service_daemon::{
DaemonEvent, Metrics, ServiceDaemon, ServiceEvent, UnregisterStatus,
SERVICE_NAME_LEN_MAX_DEFAULT,
};
pub use service_info::{AsIpv4Addrs, IntoTxtProperties, ServiceInfo, TxtProperties, TxtProperty};

/// A handler to receive messages from [ServiceDaemon]. Re-export from `flume` crate.
Expand Down
84 changes: 78 additions & 6 deletions src/service_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ macro_rules! e_fmt {
};
}

/// The default max length of the service name without domain, not including the
/// leading underscore (`_`). It is set to 15 per
/// [RFC 6763 section 7.2](https://www.rfc-editor.org/rfc/rfc6763#section-7.2).
pub const SERVICE_NAME_LEN_MAX_DEFAULT: u8 = 15;

const MDNS_PORT: u16 = 5353;
const GROUP_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);

Expand Down Expand Up @@ -245,6 +250,30 @@ impl ServiceDaemon {
Ok(resp_r)
}

/// Change the max length allowed for a service name.
///
/// As RFC 6763 defines a length max for a service name, a user should not call
/// this method unless they have to. See [`SERVICE_NAME_LEN_MAX_DEFAULT`].
///
/// `len_max` is capped at an internal limit, which is currently 30.
pub fn set_service_name_len_max(&self, len_max: u8) -> Result<()> {
const SERVICE_NAME_LEN_MAX_LIMIT: u8 = 30; // Double the default length max.

if len_max > SERVICE_NAME_LEN_MAX_LIMIT {
return Err(Error::Msg(format!(
"service name length max {} is too large",
len_max
)));
}

self.sender
.try_send(Command::SetOption(DaemonOption::ServiceNameLenMax(len_max)))
.map_err(|e| match e {
TrySendError::Full(_) => Error::Again,
e => e_fmt!("flume::channel::send failed: {}", e),
})
}

/// The main event loop of the daemon thread
///
/// In each round, it will:
Expand Down Expand Up @@ -480,6 +509,10 @@ impl ServiceDaemon {
zc.monitors.push(resp_s);
}

Command::SetOption(daemon_opt) => {
zc.process_set_option(daemon_opt);
}

_ => {
error!("unexpected command: {:?}", &command);
}
Expand Down Expand Up @@ -577,6 +610,9 @@ struct Zeroconf {

/// Channels to notify events.
monitors: Vec<Sender<DaemonEvent>>,

/// Options
service_name_len_max: u8,
}

impl Zeroconf {
Expand All @@ -601,6 +637,7 @@ impl Zeroconf {

let broadcast_addr = SocketAddrV4::new(GROUP_ADDR, MDNS_PORT).into();
let monitors = Vec::new();
let service_name_len_max = SERVICE_NAME_LEN_MAX_DEFAULT;

Ok(Self {
intf_socks,
Expand All @@ -613,9 +650,16 @@ impl Zeroconf {
counters: HashMap::new(),
poller,
monitors,
service_name_len_max,
})
}

fn process_set_option(&mut self, daemon_opt: DaemonOption) {
match daemon_opt {
DaemonOption::ServiceNameLenMax(length) => self.service_name_len_max = length,
}
}

fn notify_monitors(&mut self, event: DaemonEvent) {
// Only retain the monitors that are still connected.
self.monitors.retain(|sender| {
Expand Down Expand Up @@ -722,6 +766,13 @@ impl Zeroconf {
///
/// Zeroconf will then respond to requests for information about this service.
fn register_service(&mut self, info: ServiceInfo) {
// Check the service name length.
if let Err(e) = check_service_name_length(info.get_type(), self.service_name_len_max) {
error!("check_service_name_length: {}", &e);
self.notify_monitors(DaemonEvent::Error(e));
return;
}

let outgoing_addrs = self.send_unsolicited_response(&info);
if !outgoing_addrs.is_empty() {
self.notify_monitors(DaemonEvent::Announce(
Expand Down Expand Up @@ -758,6 +809,7 @@ impl Zeroconf {
}

/// Send an unsolicited response for owned service via `intf_sock`.
/// Returns true if sent out successfully.
fn broadcast_service_on_intf(&self, info: &ServiceInfo, intf_sock: &IntfSock) -> bool {
let service_fullname = info.get_fullname();
debug!("broadcast service {}", service_fullname);
Expand Down Expand Up @@ -1448,6 +1500,9 @@ pub enum DaemonEvent {
/// Daemon unsolicitly announced a service from an interface.
Announce(String, String),

/// Daemon encountered an error.
Error(Error),

/// Daemon detected a new IPv4 address from the host.
Ipv4Add(Ipv4Addr),

Expand Down Expand Up @@ -1482,9 +1537,16 @@ enum Command {
/// Monitor noticable events in the daemon.
Monitor(Sender<DaemonEvent>),

SetOption(DaemonOption),

Exit,
}

#[derive(Debug)]
enum DaemonOption {
ServiceNameLenMax(u8),
}

struct DnsCache {
/// <record_name, list_of_records_of_the_same_name>
map: HashMap<String, Vec<DnsRecordBox>>,
Expand Down Expand Up @@ -1598,10 +1660,25 @@ impl DnsCache {
}
}

/// The length of Service Domain name supported in this lib.
const DOMAIN_LEN: usize = "._tcp.local.".len();

/// Validate the length of "service_name" in a "_<service_name>.<domain_name>." string.
fn check_service_name_length(ty_domain: &str, limit: u8) -> Result<()> {
let service_name_len = ty_domain.len() - DOMAIN_LEN - 1; // exclude the leading `_`
if service_name_len > limit as usize {
return Err(e_fmt!("Service name length must be <= {} bytes", limit));
}
Ok(())
}

/// Validate the service name in a fully qualified name.
///
/// A Full Name = <Instance>.<Service>.<Domain>
/// The only `<Domain>` supported are "._tcp.local." and "._udp.local.".
///
/// Note: this function does not check for the length of the service name.
/// Instead `register_service` method will check the length.
fn check_service_name(fullname: &str) -> Result<()> {
if !(fullname.ends_with("._tcp.local.") || fullname.ends_with("._udp.local.")) {
return Err(e_fmt!(
Expand All @@ -1610,8 +1687,7 @@ fn check_service_name(fullname: &str) -> Result<()> {
));
}

let domain_len = "._tcp.local.".len();
let remaining: Vec<&str> = fullname[..fullname.len() - domain_len].split('.').collect();
let remaining: Vec<&str> = fullname[..fullname.len() - DOMAIN_LEN].split('.').collect();
let name = remaining.last().ok_or_else(|| e_fmt!("No service name"))?;

if &name[0..1] != "_" {
Expand All @@ -1620,10 +1696,6 @@ fn check_service_name(fullname: &str) -> Result<()> {

let name = &name[1..];

if name.len() > 15 {
return Err(e_fmt!("Service name (\"{}\") must be <= 15 bytes", name));
}

if name.contains("--") {
return Err(e_fmt!("Service name must not contain '--'"));
}
Expand Down
35 changes: 28 additions & 7 deletions tests/mdns_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use if_addrs::{IfAddr, Ifv4Addr};
use mdns_sd::{
Error, IntoTxtProperties, ServiceDaemon, ServiceEvent, ServiceInfo, UnregisterStatus,
DaemonEvent, Error, IntoTxtProperties, ServiceDaemon, ServiceEvent, ServiceInfo,
UnregisterStatus,
};
use std::collections::HashMap;
use std::net::Ipv4Addr;
Expand Down Expand Up @@ -464,9 +465,10 @@ fn subtype() {
fn service_name_check() {
// Create a daemon for the server.
let server_daemon = ServiceDaemon::new().expect("Failed to create server daemon");
let monitor = server_daemon.monitor().unwrap();
// Register a service with a name len > 15.
let service_name_too_long = "_service-name-too-long._udp.local.";
let host_ipv4 = "127.0.0.1";
let host_ipv4 = "";
let host_name = "my_host.";
let port = 5200;
let my_service = ServiceInfo::new(
Expand All @@ -477,13 +479,32 @@ fn service_name_check() {
port,
None,
)
.expect("valid service info");
let result = server_daemon.register(my_service);
assert!(result.is_err());
if let Err(e) = result {
println!("register error: {}", &e);
.expect("valid service info")
.enable_addr_auto();
let result = server_daemon.register(my_service.clone());
assert!(result.is_ok());

// Verify that the daemon reported error.
let event = monitor.recv_timeout(Duration::from_millis(500)).unwrap();
assert!(matches!(event, DaemonEvent::Error(_)));
match event {
DaemonEvent::Error(e) => println!("Daemon error: {}", e),
_ => {}
}

// Verify that we can increase the service name length max.
server_daemon.set_service_name_len_max(30).unwrap();
let result = server_daemon.register(my_service);
assert!(result.is_ok());

// Verify that the service was published successfully.
let event = monitor.recv_timeout(Duration::from_millis(500)).unwrap();
assert!(matches!(event, DaemonEvent::Announce(_, _)));

// Check for the internal upper limit of service name length max.
let r = server_daemon.set_service_name_len_max(31);
assert!(r.is_err());

server_daemon.shutdown().unwrap();
}

Expand Down

0 comments on commit a07f550

Please sign in to comment.