diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 7a23f89..beff1c6 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -966,13 +966,6 @@ impl DnsIncoming { // decode RDATA based on the record type. let rec: Option = match ty { - TYPE_A => Some(Box::new(DnsAddress::new( - &name, - ty, - class, - ttl, - self.read_ipv4().into(), - ))), TYPE_CNAME | TYPE_PTR => Some(Box::new(DnsPointer::new( &name, ty, @@ -1004,6 +997,13 @@ impl DnsIncoming { self.read_char_string(), self.read_char_string(), ))), + TYPE_A => Some(Box::new(DnsAddress::new( + &name, + ty, + class, + ttl, + self.read_ipv4().into(), + ))), TYPE_AAAA => Some(Box::new(DnsAddress::new( &name, ty, diff --git a/src/lib.rs b/src/lib.rs index a345f59..3483e6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ mod service_info; pub use error::{Error, Result}; pub use service_daemon::{ - DaemonEvent, Metrics, ServiceDaemon, ServiceEvent, UnregisterStatus, + DaemonEvent, IfKind, Metrics, ServiceDaemon, ServiceEvent, UnregisterStatus, SERVICE_NAME_LEN_MAX_DEFAULT, }; pub use service_info::{AsIpAddrs, IntoTxtProperties, ServiceInfo, TxtProperties, TxtProperty}; diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 18b1e60..e421908 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -293,6 +293,32 @@ impl ServiceDaemon { self.send_cmd(Command::SetOption(DaemonOption::ServiceNameLenMax(len_max))) } + /// Include interfaces that match `if_kind` for this service daemon. + /// + /// For example: + /// ```ignore + /// daemon.enable_interface("en0")?; + /// ``` + pub fn enable_interface(&self, if_kind: impl IntoIfKindVec) -> Result<()> { + let if_kind_vec = if_kind.into_vec(); + self.send_cmd(Command::SetOption(DaemonOption::EnableInterface( + if_kind_vec.kinds, + ))) + } + + /// Ignore/exclude interfaces that match `if_kind` for this daemon. + /// + /// For example: + /// ```ignore + /// daemon.disable_interface(IfKind::IPv6)?; + /// ``` + pub fn disable_interface(&self, if_kind: impl IntoIfKindVec) -> Result<()> { + let if_kind_vec = if_kind.into_vec(); + self.send_cmd(Command::SetOption(DaemonOption::DisableInterface( + if_kind_vec.kinds, + ))) + } + /// The main event loop of the daemon thread /// /// In each round, it will: @@ -548,7 +574,7 @@ impl ServiceDaemon { Command::UnregisterResend(packet, ip) => { if let Some(intf_sock) = zc.intf_socks.get(&ip) { - debug!("Send a packet length of {}", packet.len()); + debug!("UnregisterResend from {}", &ip); broadcast_on_intf(&packet[..], intf_sock); zc.increase_counter(Counter::UnregisterResend, 1); } @@ -599,7 +625,7 @@ impl ServiceDaemon { } } -/// Creates a new UDP socket that uses `intf_ip` to send and recv multicast. +/// Creates a new UDP socket that uses `intf` to send and recv multicast. fn new_socket_bind(intf: &Interface) -> Result { // Use the same socket for receiving and sending multicast packets. // Such socket has to bind to INADDR_ANY or IN6ADDR_ANY. @@ -636,11 +662,10 @@ fn new_socket_bind(intf: &Interface) -> Result { sock.set_multicast_if_v6(intf.index.unwrap_or(0)) .map_err(|e| e_fmt!("set multicast_if on addr {}: {}", ip, e))?; - // Test if we can send packets successfully. - let multicast_addr = SocketAddrV6::new(GROUP_ADDR_V6, MDNS_PORT, 0, 0).into(); - let test_packet = DnsOutgoing::new(0).to_packet_data(); - sock.send_to(&test_packet, &multicast_addr) - .map_err(|e| e_fmt!("send multicast packet on addr {}: {}", ip, e))?; + // We are not sending multicast packets to test this socket as there might + // be many IPv6 interfaces on a host and could cause such send error: + // "No buffer space available (os error 55)". + Ok(sock) } } @@ -688,6 +713,97 @@ struct IntfSock { sock: Socket, } +/// Specify kinds of interfaces. It is used to enable or to disable interfaces in the daemon. +/// +/// Note that for ergonomic reasons, `From<&str>` and `From` are implemented. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum IfKind { + /// All interfaces. + All, + + /// All IPv4 interfaces. + IPv4, + + /// All IPv6 interfaces. + IPv6, + + /// By the interface name, for example "en0" + Name(String), + + /// By an IPv4 or IPv6 address. + Addr(IpAddr), +} + +impl IfKind { + /// Checks if `intf` matches with this interface kind. + fn matches(&self, intf: &Interface) -> bool { + match self { + IfKind::All => true, + IfKind::IPv4 => intf.ip().is_ipv4(), + IfKind::IPv6 => intf.ip().is_ipv6(), + IfKind::Name(ifname) => ifname == &intf.name, + IfKind::Addr(addr) => addr == &intf.ip(), + } + } +} + +/// The first use case of specifying an interface was to +/// use an interface name. Hence adding this for ergonomic reasons. +impl From<&str> for IfKind { + fn from(val: &str) -> IfKind { + IfKind::Name(val.to_string()) + } +} + +impl From<&String> for IfKind { + fn from(val: &String) -> IfKind { + IfKind::Name(val.to_string()) + } +} + +/// Still for ergonomic reasons. +impl From for IfKind { + fn from(val: IpAddr) -> IfKind { + IfKind::Addr(val) + } +} + +/// A list of `IfKind` that can be used to match interfaces. +pub struct IfKindVec { + kinds: Vec, +} + +/// A trait that converts a type into a Vec of `IfKind`. +pub trait IntoIfKindVec { + fn into_vec(self) -> IfKindVec; +} + +impl> IntoIfKindVec for T { + fn into_vec(self) -> IfKindVec { + let if_kind: IfKind = self.into(); + IfKindVec { + kinds: vec![if_kind], + } + } +} + +impl> IntoIfKindVec for Vec { + fn into_vec(self) -> IfKindVec { + let kinds: Vec = self.into_iter().map(|x| x.into()).collect(); + IfKindVec { kinds } + } +} + +/// Selection of interfaces. +struct IfSelection { + /// The interfaces to be selected. + if_kind: IfKind, + + /// Whether the `if_kind` should be enabled or not. + selected: bool, +} + /// A struct holding the state. It was inspired by `zeroconf` package in Python. struct Zeroconf { /// Local interfaces with sockets to recv/send on these interfaces. @@ -721,6 +837,9 @@ struct Zeroconf { /// Options service_name_len_max: u8, + /// All interface selections called to the daemon. + if_selections: Vec, + /// Socket for signaling. signal_sock: UdpSocket, @@ -753,6 +872,7 @@ impl Zeroconf { let service_name_len_max = SERVICE_NAME_LEN_MAX_DEFAULT; let timers = vec![]; + let if_selections = vec![]; Ok(Self { intf_socks, @@ -766,6 +886,7 @@ impl Zeroconf { poller, monitors, service_name_len_max, + if_selections, signal_sock, timers, }) @@ -774,7 +895,31 @@ impl Zeroconf { fn process_set_option(&mut self, daemon_opt: DaemonOption) { match daemon_opt { DaemonOption::ServiceNameLenMax(length) => self.service_name_len_max = length, + DaemonOption::EnableInterface(if_kind) => self.enable_interface(if_kind), + DaemonOption::DisableInterface(if_kind) => self.disable_interface(if_kind), + } + } + + fn enable_interface(&mut self, kinds: Vec) { + for if_kind in kinds { + self.if_selections.push(IfSelection { + if_kind, + selected: true, + }); + } + + self.process_if_selections(); + } + + fn disable_interface(&mut self, kinds: Vec) { + for if_kind in kinds { + self.if_selections.push(IfSelection { + if_kind, + selected: false, + }); } + + self.process_if_selections(); } fn notify_monitors(&mut self, event: DaemonEvent) { @@ -826,6 +971,45 @@ impl Zeroconf { key } + /// Apply all selections to the available interfaces. + fn process_if_selections(&mut self) { + // By default, we enable all interfaces. + let interfaces = my_ip_interfaces(); + let intf_count = interfaces.len(); + let mut intf_selections = vec![true; intf_count]; + + // apply if_selections + for selection in self.if_selections.iter() { + // Mark the interfaces for this selection. + for i in 0..intf_count { + if selection.if_kind.matches(&interfaces[i]) { + intf_selections[i] = selection.selected; + } + } + } + + // Update `intf_socks` based on the selections. + for (idx, intf) in interfaces.into_iter().enumerate() { + let ip_addr = intf.ip(); + + if intf_selections[idx] { + // Add the interface + if self.intf_socks.get(&ip_addr).is_none() { + self.add_new_interface(intf); + } + } else { + // Remove the interface + if let Some(if_sock) = self.intf_socks.remove(&ip_addr) { + if let Err(e) = self.poller.delete(&if_sock.sock) { + error!("process_if_selections: poller.delete {:?}: {}", &ip_addr, e); + } + // Remove from poll_ids + self.poll_ids.retain(|_, v| v != &ip_addr); + } + } + } + } + /// Check for IP changes and update intf_socks as needed. fn check_ip_changes(&mut self) { // Get the current interfaces. @@ -862,37 +1046,36 @@ impl Zeroconf { // Add newly found interfaces. for intf in my_ifaddrs { - // Skip existing interfaces. - if self.intf_socks.get(&intf.ip()).is_some() { - continue; + if self.intf_socks.get(&intf.ip()).is_none() { + self.add_new_interface(intf); } + } + } - // Bind the new interface. - let new_ip = intf.ip(); - let sock = match new_socket_bind(&intf) { - Ok(s) => { - debug!("check_ip_changes: bind {}", &intf.ip()); - s - } - Err(e) => { - debug!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); - continue; - } - }; - - // Add the new interface into the poller. - let key = self.add_poll(new_ip); - if let Err(e) = self.poller.add(&sock, polling::Event::readable(key)) { - error!("check_ip_changes: poller add ip {}: {}", new_ip, e); + fn add_new_interface(&mut self, intf: Interface) { + // Bind the new interface. + let new_ip = intf.ip(); + let sock = match new_socket_bind(&intf) { + Ok(s) => s, + Err(e) => { + error!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); + return; } + }; - self.intf_socks.insert(new_ip, IntfSock { intf, sock }); + // Add the new interface into the poller. + let key = self.add_poll(new_ip); + if let Err(e) = self.poller.add(&sock, polling::Event::readable(key)) { + error!("check_ip_changes: poller add ip {}: {}", new_ip, e); + return; + } - self.add_addr_in_my_services(new_ip); + self.intf_socks.insert(new_ip, IntfSock { intf, sock }); - // Notify the monitors. - self.notify_monitors(DaemonEvent::IpAdd(new_ip)); - } + self.add_addr_in_my_services(new_ip); + + // Notify the monitors. + self.notify_monitors(DaemonEvent::IpAdd(new_ip)); } /// Registers a service. @@ -1205,8 +1388,7 @@ impl Zeroconf { if let Some(records) = self.cache.ptr.get(ty_domain) { for record in records.iter() { if let Some(ptr) = record.any().downcast_ref::() { - let info = self.create_service_info_from_cache(ty_domain, &ptr.alias); - let info = match info { + let info = match self.create_service_info_from_cache(ty_domain, &ptr.alias) { Ok(ok) => ok, Err(err) => { error!("Error while creating service info from cache: {}", err); @@ -1267,7 +1449,7 @@ impl Zeroconf { } } - // resolve A records + // resolve A and AAAA records if let Some(records) = self.cache.addr.get(info.get_hostname()) { for answer in records.iter() { if let Some(dns_a) = answer.any().downcast_ref::() { @@ -1662,6 +1844,8 @@ impl fmt::Display for Command { #[derive(Debug)] enum DaemonOption { ServiceNameLenMax(u8), + EnableInterface(Vec), + DisableInterface(Vec), } struct DnsCache { diff --git a/src/service_info.rs b/src/service_info.rs index 19ffef4..a47ef8e 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -17,7 +17,7 @@ const DNS_OTHER_TTL: u32 = 4500; // 75 minutes for non-host records (PTR, TXT et /// Complete info about a Service Instance. /// /// We can construct some PTR, one SRV and one TXT record from this info, -/// as well as A (IPv4 Address) records. +/// as well as A (IPv4 Address) and AAAA (IPv6 Address) records. #[derive(Debug, Clone)] pub struct ServiceInfo { ty_domain: String, // . @@ -42,6 +42,11 @@ impl ServiceInfo { /// /// `my_name` is the instance name, without the service type suffix. /// + /// `host_name` is the "host" in the context of DNS. It is used as the "name" + /// in the address records (i.e. TYPE_A and TYPE_AAAA records). It means that + /// for the same hostname in the same local network, the service resolves in + /// the same addresses. Be sure to check it if you see unexpected addresses resolved. + /// /// `properties` can be `None` or key/value string pairs, in a type that /// implements [`IntoTxtProperties`] trait. It supports: /// - `HashMap` diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index 1545bb6..5f019a5 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -1,6 +1,7 @@ use if_addrs::{IfAddr, Interface}; use mdns_sd::{ - DaemonEvent, IntoTxtProperties, ServiceDaemon, ServiceEvent, ServiceInfo, UnregisterStatus, + DaemonEvent, IfKind, IntoTxtProperties, ServiceDaemon, ServiceEvent, ServiceInfo, + UnregisterStatus, }; use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -233,7 +234,7 @@ fn service_without_properties_with_alter_net_v4() { let first_ip = if_addrs[0].ip(); let alter_ip = ipv4_alter_net(&if_addrs); let host_ip = vec![first_ip, alter_ip]; - let host_name = "serv-no-prop."; + let host_name = "serv-no-prop-v4."; let port = 5201; let my_service = ServiceInfo::new( ty_domain, @@ -304,7 +305,7 @@ fn service_without_properties_with_alter_net_v6() { let first_ip = if_addrs[0].ip(); let alter_ip = ipv6_alter_net(&if_addrs); let host_ip = vec![first_ip, alter_ip]; - let host_name = "serv-no-prop."; + let host_name = "serv-no-prop-v6."; let port = 5201; let my_service = ServiceInfo::new( ty_domain, @@ -441,6 +442,169 @@ fn test_into_txt_properties() { assert_eq!(txt_props.get_property_val_str("key2").unwrap(), "val2"); } +/// Test enabling an interface using its name, for example "en0". +#[test] +fn service_with_named_interface_only() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // First, disable all interfaces. + d.disable_interface(IfKind::All).unwrap(); + + // Register a service with a name len > 15. + let my_ty_domain = "_named_intf_only._udp.local."; + let host_name = "my_host."; + let host_ipv4 = ""; + let port = 5202; + let my_service = ServiceInfo::new( + my_ty_domain, + "my_instance", + host_name, + &host_ipv4, + port, + None, + ) + .expect("invalid service info") + .enable_addr_auto(); + + d.register(my_service).unwrap(); + + // Browse for a service and verify all addresses are IPv4. + let browse_chan = d.browse(my_ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + let addrs = info.get_addresses(); + resolved = true; + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + addrs + ); + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(_) => { + break; + } + } + } + + assert!(resolved == false); + + // Second, find an interface. + let if_addrs: Vec = my_ip_interfaces() + .into_iter() + .filter(|iface| iface.addr.ip().is_ipv4()) + .collect(); + let if_name = if_addrs[0].name.clone(); + + // Enable the named interface. + println!("Enable interface with name {}", &if_name); + d.enable_interface(&if_name).unwrap(); + + // Browse again. + let browse_chan = d.browse(my_ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + let addrs = info.get_addresses(); + resolved = true; + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + addrs + ); + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(_) => { + break; + } + } + } + + assert!(resolved); + + d.shutdown().unwrap(); +} + +#[test] +fn service_with_ipv4_only() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Disable IPv6, so the daemon is IPv4 only now. + d.disable_interface(IfKind::IPv6).unwrap(); + + // Register a service with a name len > 15. + let service_ipv4_only = "_test_ipv4_only._udp.local."; + let host_name = "my_host_ipv4_only."; + let host_ipv4 = ""; + let port = 5201; + let my_service = ServiceInfo::new( + service_ipv4_only, + "my_instance", + host_name, + &host_ipv4, + port, + None, + ) + .expect("invalid service info") + .enable_addr_auto(); + let result = d.register(my_service); + assert!(result.is_ok()); + + // Browse for a service and verify all addresses are IPv4. + let browse_chan = d.browse(service_ipv4_only).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + let addrs = info.get_addresses(); + resolved = true; + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + addrs + ); + assert!(info.get_addresses().len() > 0); + for addr in info.get_addresses().iter() { + assert!(addr.is_ipv4()); + } + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(_) => { + break; + } + } + } + + assert!(resolved); + d.shutdown().unwrap(); +} + #[test] fn service_with_invalid_addr_v4() { // Create a daemon