diff --git a/Cargo.lock b/Cargo.lock index 15eadd6..145fd9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2414,8 +2414,6 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" [[package]] name = "mdns-sd" version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a19dd805348943831582c4d9e6921c66de689127d6b27665a4e155c2117799" dependencies = [ "fastrand", "flume", @@ -2625,7 +2623,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 2.0.2", "proc-macro2", "quote", "syn 2.0.119", @@ -3098,15 +3096,6 @@ dependencies = [ "toml_edit 0.20.2", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4866,18 +4855,6 @@ dependencies = [ "winnow 0.5.40", ] -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap 2.14.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - [[package]] name = "toml_parser" version = "1.1.3+spec-1.1.0" @@ -5906,9 +5883,6 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] [[package]] name = "winreg" diff --git a/Cargo.toml b/Cargo.toml index cab1920..ed74046 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/lanspread-tauri-deno-ts/src-tauri", "crates/lanspread-utils", ] +exclude = ["vendor/mdns-sd"] [workspace.dependencies] base64 = "0.23" @@ -23,20 +24,12 @@ futures = "0.3" gethostname = "1" if-addrs = "0.15" log = "0.4" -mdns-sd = "0.21" +mdns-sd = { path = "vendor/mdns-sd" } mimalloc = { version = "0.1", features = ["secure"] } rayon = "1" -rcgen = { - version = "=0.14.10", - default-features = false, - features = ["aws_lc_rs"] -} +rcgen = { version = "=0.14.10", default-features = false, features = ["aws_lc_rs"] } rustix = "1" -rustls = { - version = "=0.23.43", - default-features = false, - features = ["aws-lc-rs", "logging", "std"] -} +rustls = { version = "=0.23.43", default-features = false, features = ["aws-lc-rs", "logging", "std"] } s2n-quic = { version = "=1.88.0", default-features = false, diff --git a/crates/lanspread-mdns/src/lib.rs b/crates/lanspread-mdns/src/lib.rs index b603bf9..d956505 100644 --- a/crates/lanspread-mdns/src/lib.rs +++ b/crates/lanspread-mdns/src/lib.rs @@ -149,6 +149,9 @@ pub struct MdnsBrowser { #[derive(Debug, Clone)] pub struct MdnsService { pub addr: SocketAddr, + /// IP address observed on the response datagram. This is independent of + /// the attacker-controlled A/AAAA target in `addr`. + pub source_ip: std::net::IpAddr, pub fullname: String, pub hostname: String, pub properties: HashMap, @@ -260,6 +263,11 @@ impl MdnsBrowser { ) -> Option { log::trace!("mdns ServiceResolved event: {info:?}"); + let Some(source_ip) = info.get_observed_source() else { + log::debug!("Ignoring mDNS resolution without an observed response source"); + return None; + }; + if info.ty_domain != self.service_type { log::trace!( "Got mDNS with uninteresting service type: {} (expected: {})", @@ -283,6 +291,7 @@ impl MdnsBrowser { let properties = info.get_properties().clone().into_property_map_str(); return Some(MdnsService { addr, + source_ip, fullname: info.get_fullname().to_string(), hostname: info.get_hostname().to_string(), properties, diff --git a/justfile b/justfile index a93ba24..dc64477 100644 --- a/justfile +++ b/justfile @@ -55,9 +55,10 @@ bundle: catalog-check-production fmt: cargo +nightly fmt - tombi format --offline - fd -tf -e md -E security-report -E 'SECURITY_AUDIT_*.md' -x prettier --write --prose-wrap always --print-width 80 - rumdl check --flavor commonmark --exclude 'security-report/**,SECURITY_AUDIT_*.md' --fix + cargo +nightly fmt --manifest-path vendor/mdns-sd/Cargo.toml + fd -tf -e toml -E vendor -x tombi format --offline + fd -tf -e md -E security-report -E vendor -E 'SECURITY_AUDIT_*.md' -x prettier --write --prose-wrap always --print-width 80 + rumdl check --flavor commonmark --exclude 'security-report/**,vendor/**,SECURITY_AUDIT_*.md' --fix just --fmt _fix: @@ -72,6 +73,10 @@ clippy: test: {{ TAURI_FIXTURE_ENV }} TAURI_CONFIG='{{ TAURI_DEV_CONFIG }}' cargo test --workspace --all-targets --all-features +# The patched dependency is intentionally outside the Cargo workspace. +mdns-vendor-test: + cargo test --manifest-path vendor/mdns-sd/Cargo.toml --all-targets --all-features + # Regenerate the committed acceptance catalogs from their fixture packages. fixture-catalogs: cargo run -p lanspread-compat --bin lanspread-fixture-catalog -- \ diff --git a/vendor/mdns-sd/.gitignore b/vendor/mdns-sd/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/vendor/mdns-sd/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/vendor/mdns-sd/CHANGELOG.md b/vendor/mdns-sd/CHANGELOG.md new file mode 100644 index 0000000..5d4e19c --- /dev/null +++ b/vendor/mdns-sd/CHANGELOG.md @@ -0,0 +1,949 @@ +# Version 0.21.1 (2026-08-30) + +This is a bugfix release. + +## Bug fixes / improvements + +- Send goodbye (TTL=0) packets under conflict-resolved names. When probing renames a + record due to a name conflict (RFC 6762 section 9), now the goodbye carries the updated + names peers actually observed. (#495, commit `fe525a6`) +- Harden DNS name parsing (`read_name`) and name compression handling: skip only the + malformed record instead of the whole packet. (#492, commit `5bf6b7c`) + +## All changes + +* `5bf6b7c 2026-08-30` refactoring: read_name and name compression (#492) (keepsimple1) +* `fe525a6 2026-08-26` fix: send goodbye packets under conflict-resolved names (#495) (dhavli) + +Thanks and welcome our new contributor @dhavli ! + +# Version 0.21.0 (2026-08-10) + +## Breaking changes + +- The max outgoing packet size is now 1452 bytes (Ethernet MTU), down from 8972 bytes, + per [RFC 6762 section 17](https://datatracker.ietf.org/doc/html/rfc6762#section-17). + A record too big for one packet is still sent alone, in a packet of up to the RFC's + 9000-byte ceiling, so no record is dropped. Users that need to change the limit + can call the new `ServiceDaemon::set_max_packet_size()`. + +## New features + +- Add `ServiceDaemon::set_max_packet_size()` to change the max byte size of outgoing + packets on the interfaces matching a given `IfKind`, and export the new default as + `MAX_PKT_DEFAULT`. (#487, commit `82711a8`) +- Re-export the receiver-side error types `RecvError`, `RecvTimeoutError` and + `TryRecvError` from `flume`, so that a caller can name them without adding `flume` + as a direct dependency. (#488, commit `20c0eac`) + +## Bug fixes / improvements + +- On the receive side, the IPv4 and IPv6 max packet sizes are now tracked separately + (8972 and 8952 bytes) so that each stays within the RFC's 9000-byte limit including + its own IP header. (#487, commit `82711a8`) + +## All changes + +* `6bf2bdb 2026-08-09` refactor: extract the code that does cache flush (#490) (keepsimple1) +* `20c0eac 2026-08-08` feat: re-export Receiver error types (#488) (#489) (keepsimple1) +* `82711a8 2026-08-07` feat: limit generated packet size per RFC 6762 section 17 (#487) (keepsimple1) + +# Version 0.20.3 (2026-07-26) + +This is a small bugfix release. + +## Bug fixes / improvements + +- Fix a panic when encoding a name that contains a label longer than 63 bytes. The packet write path now reports an error and skips the offending question or record, instead of asserting. (#484, commit `08362e3`) + +## All changes + +* `a574e93 2026-07-24` docs: add RFC 6762 compliance rows for v0.20.2 features (keepsimple1) +* `08362e3 2026-07-24` fix: Panic in write_utf8 (assert s.len() < 64) (#484) (keepsimple1) + +# Version 0.20.2 (2026-07-16) + +This is a feature and bugfix release focused on improved RFC 6762 compliance. + +## New features + +- Implement RFC 6762 section 6 multicast rate limiting: responses to the same record are rate-limited to no more than once per second on a given interface. (#476, commit `76f42bd`) +- Delay responses to PTR queries per RFC 6762 section 6, spreading responses over a random 10-50ms (instead of 20-120ms) window to reduce collisions. (#479, commit `a37e959`) +- Add a random jitter 10-50ms (instead of 20-120ms) before sending the initial query per RFC 6762 section 5.2. (#480, commit `7c55c75`) + +## Bug fixes / improvements + +- Space announcement retransmissions wider than the rate-limit window so that repeated announcements are not dropped. (#477, commit `6f39a46`) +- Avoid leaking empty cache entries for records that are not for us. (#481, commit `5cfa13a`) + +## All changes + +* `5cfa13a 2026-07-15` fix: avoid leaking empty cache entries for records not for us (#481) (keepsimple1) +* `7c55c75 2026-07-14` feat: add a jitter for initial query per RFC 6762 section 5.2 (#480) (keepsimple1) +* `a37e959 2026-07-12` feat: delay PTR query responses per RFC 6762 section 6 (#479) (keepsimple1) +* `6f39a46 2026-07-08` fix: space announcement retransmissions wider than the rate-limit window (#477) (keepsimple1) +* `76f42bd 2026-07-07` feat: Implement RFC 6762 section 6 multicast rate limiting (#476) (keepsimple1) + +# Version 0.20.1 (2026-06-28) + +This is a small feature and maintenance release. + +## New features + +- Add `IfKind::Predicate` and a new `IfPredicate` type, allowing interfaces to be selected with a custom predicate function (e.g. matching by interface name pattern). (#474, commit `fd72146`) + +## Other changes + +- chore(deps): update dependencies. (#454, commit `06bdad1`) + +## All changes + +* `fd72146 2026-06-22` Add `IfPredicate` for more flexible interface filtering (#474) (MAlba124) +* `06bdad1 2026-06-16` chore(deps): update (#454) (CosminPerRam) + +Thanks and welcome our new contributor @MAlba124 ! + +# Version 0.20.0 (2026-05-24) + +This release contains a small breaking change in the optional `serde` feature, hence the minor version bump. + +## Breaking changes + +- Remove `#[serde(untagged)]` from `ScopedIp`. The serialized form of `ScopedIp` now includes its enum variant tag (`V4` / `V6`), which improves compatibility with binary serde formats (e.g. MessagePack). Only affects users of the optional `serde` feature. (#472, commit `5fa4b92`) + +## Other changes + +- tests: remove explicit random port assignments. +- chore: remove an unused dev-dependency. + +## All changes + +* `fee8ab7 2026-05-24` remove unused dev-dependency (keepsimple1) +* `e745a2f 2026-05-23` tests: remove explicit port random assignments (#455) (CosminPerRam) +* `5fa4b92 2026-05-22` Remove untagged attribute from ScopedIp (#472) (Rascal) + +# Version 0.19.2 (2026-05-17) + +This is a bugfix and small-feature release. + +## New features + +- Support RFC 6762 legacy unicast responses: when a query arrives from a source port other than `5353`, the daemon now replies directly to the querier's address/port instead of multicasting. This enables interoperability with one-shot / legacy mDNS queriers. (#469, commit `933bdbe`) +- Added a new `Error::DaemonShutdown` variant, returned by `ServiceDaemon` methods after the daemon thread has exited (previously surfaced as a generic `Error::Msg`). The `Error` enum is `#[non_exhaustive]`, so this is additive. (commit `a457193`) + +## Bug fixes / improvements + +- Optimize outgoing DNS serialization to reduce allocations on the send path. (#467, commit `fb26d7f`) +- Replace a stray `println!` in `DnsHostInfo::write` with `debug!`, so the library no longer writes to stdout. (#465, commit `96008a4`) +- Expanded `# Errors` doc sections on the major `ServiceDaemon` APIs (`new`, `new_with_port`, `browse`, `stop_browse`, `resolve_hostname`, `stop_resolve_hostname`, `register`, `unregister`), documenting when `Error::Again` vs. `Error::DaemonShutdown` is returned. (#464, commit `a457193`) + +## All changes + +* `933bdbe 2026-05-15` Add support for RFC 6762 legacy unicast responses (#469) (Luqmaan) +* `fb26d7f 2026-04-30` Optimize dns outgoing serialization (#467) (Alexander) +* `a457193 2026-04-28` docs: add doc comments for error handling on major APIs (#464) (keepsimple1) +* `96008a4 2026-04-28` Replace println with debug log (#465) (Alexander) + +Thanks and welcome our new contributors @luqs1 and @anti-social ! + +# Version 0.19.1 (2026-04-19) + +This is a bugfix release. + +## Bug fixes + +- When responding to a query, pick a source IP that matches the querier's subnet, so responses are reachable on multi-homed hosts. (#460, commit `d210372`) +- Validate TXT property length in the `ServiceInfo` constructor, catching oversized properties at registration time instead of at send time. (#458, commit `cc81eec`) + +## All changes + +* `d210372 2026-04-18` fix: use a source IP matching the querier's subnet when responding (#460) (keepsimple1) +* `cc81eec 2026-04-12` fix: check TXT property length in ServiceInfo constructor (#458) (keepsimple1) + +# Version 0.19.0 (2026-04-04) + +## Breaking changes + +- `ScopedIpV4` now carries `interface_ids` tracking which network interfaces discovered the address. The derived `Eq`/`Hash` now includes `interface_ids`, so two `ScopedIpV4` values with the same IP but different interface lists are no longer equal. (commits `43bd8f3`, `0661bf1`, `247447b`) + +## New features + +- New optional `serde` feature: adds `Serialize`/`Deserialize` on `InterfaceId`, `ScopedIpV4`, `ScopedIpV6`, `ScopedIp`, `TxtProperties`, `TxtProperty`, and `ResolvedService`. (commit `c2c2f75`) +- New public APIs: `ScopedIpV4::new()`, `ScopedIpV4::interface_ids()`, `InterfaceId::get_addrs()`. + +## Bug fixes + +- Avoid known-answer suppression when querying on a new interface, so address records are discovered promptly. (commit `468c5ee`) +- Track modified instances when removing records from an interface, so `ServiceResolved` events reflect updated addresses. (commit `7daa1d4`) + +## All changes + +* `3903f09 2026-04-04` refactoring: simplify handle_query (#452) (keepsimple1) +* `b6ddc18 2026-04-04` refactoring: move add_answer_with_additionals into struct DnsOutgoing (#451) (keepsimple1) +* `468c5ee 2026-04-03` fix: avoid known-answer suppression when querying on a new interface (#450) (keepsimple1) +* `7daa1d4 2026-04-01` fix: track modified_instances when removing records from an interface (#448) (keepsimple1) +* `247447b 2026-03-26` fix: ScopedIp considered Eq when interface_ids change (#446) (keepsimple1) +* `0661bf1 2026-03-24` refactoring: ScopedIpV4 to use multiple InterfaceIds (#444) (keepsimple1) +* `c2c2f75 2026-03-15` Serde Deserialize+Serialize implementation (#440) (Rascal) +* `43bd8f3 2026-03-13` add interface_id in ScopedIpV4 (#439) (keepsimple1) + +Thanks and welcome our new contributor @Rascal ! + +# Verison 0.18.2 (2026-03-10) + +- A bugfix: refresh of address records didn't work when hostname is not lowercase. + +## All changes + +* `ec1e733 2026-03-11` fix: Refresh of A and AAAA records (#441) (hrzlgnm) + +# Version 0.18.1 (2026-02-28) + +- A bugfix for `disable_interface` with an IPv4 address: clarified that the semantics is to disable IPv4 on the interface identified by the IPv4 address. +- Added new variants for `IfKind` enum: `IndexIPV4(u32)` and `IndexIPV6(u32)`. + +## All changes + +* `7e04122 2026-02-28` fix(test): use all IPv4 interfaces in test_disable_interface_cache (#435) (keepsimple1) + +# Version 0.18.0 (2026-02-15) + +A few new features, documentation enhancements and breaking changes. + +## Breaking changes +- Removed one default feature: `reuseport`. It is handled transparently now. (see commit `58bc8c5`) +- New feature: support `.` and `\` in instance names (see commit `3481b94`) +- New internal fix: proper cleanup on daemon shutdown (see commit `8d24304`) +- New internal fix: exclude point-to-point interfaces by default (e.g. tunnel interface) (see commit `85b6cd9`) + +## All changes +* `b694333 2026-02-12` Added documentation about service name length (FelixSelter) +* `85b6cd9 2026-02-10` fix for macOS: exclude IFF_POINTTOPOINT interfaces and exclude Apple P2P interfaces by default (#425) (keepsimple1) +* `58bc8c5 2026-02-09` fix: invert reuseport feature (#430) (keepsimple1) +* `8d24304 2026-02-07` feat: add proper cleanup on daemon shutdown (#421) (Thibaut M.) +* `69418d6 2026-02-05` chore: update some comments (#429) (keepsimple1) +* `3481b94 2026-02-06` feat: implement RFC 6763 Section 4.3 escaping for instance names (#420) (Thibaut M.) +* `58c15b4 2026-01-27` fix: only add a scope in Display to unicast link local v6 addresses (#424) (hrzlgnm) +* `3f34136 2026-01-24` Return errors from send_dns_outgoing (#419) (keepsimple1) +* `0e323f6 2026-01-21` ci: update github action checkout (#422) (Thibaut M.) + +Thanks and welcome new contributors: @thibaut-pascal, @FelixSelter + +# Version 0.17.2 (2026-01-16) + +## New features (non-breaking) + +A new 'feature' literally: a default feature `reuseport` to control if SO_REUSEPORT should be used. Useful for old Linux kernels (before 3.9). + +## All changes + +* `466d373 2026-01-15` feat: add `reuseport` feature (#414) (Hanssen) +* `1770d01 2025-12-31` exmaples: add --verify option in the query example (#412) (keepsimple1) + +Thanks and welcome our new contributor @Hanssen0 ! + +# Version 0.17.1 (2025-12-05) + +## New features (non-breaking) + +`ServiceDaemon::new_with_port(port: u16)` allows using a custom port for mainly development / testing purposes. + +## All changes + +* `9088f8b 2025-12-04` feat: use Self on impl return of itself for RRType (#409) (CosminPerRam) +* `91b3e66 2025-12-02` Add a custom port option (#408) (Kaido Kert) +* `3e7a582 2025-11-30` handle empty return from send_dns_outgoing (#407) (keepsimple1) + +Thanks and welcome our new contributor @kaidokert ! + +# Version 0.17.0 (2025-11-06) + +## Breaking changes + +Loopback interfaces are enabled by default now. The main reason is that some user reported a failure of publishing services locally. I think this change probably only impacts very few. + +## New features + +A couple of new APIs are added for `ServiceInfo`: `set_interfaces` and `set_link_local_only` based on some real world use cases. + +## All changes + +* `6752eaf 2025-11-04` feat: service registration with granular iface/ip conditions (#398) (twizansk) +* `7293e7c 2025-11-02` Enable loopback interfaces by default (#397) (keepsimple1) +* `505bd71 2025-11-02` Fix clippy for tests and remove unnecessary tests (#403) (keepsimple1) + +# Version 0.16.0 (2025-10-29) + +A bugfix release. But we also bumped up rustc MSRV to 1.71.0, hence bumping our own minor version. + +## All changes + +* `6c01cf6 2025-10-27` Handle IPv6 disabled in kernel (#396) (keepsimple1) +* `b15c4c3 2025-10-23` log the interface name when joining multicast group (#394) (keepsimple1) +* `382521c 2025-10-15` refactoring only: make resolve_updated_instances easier to understand (#393) (keepsimple1) + +# Version 0.15.1 (2025-09-06) + +New feature: cache only browsing. Check out the new methods `browse_cache` and `accept_unsolicited`. + +## All changes + +* `3f6d6e9 2025-09-04` feat: support cache only browsing (#388) (twizansk) + +Thanks and welcome our new contributor @twizansk ! + +# Version 0.15.0 (2025-08-31) + +## Breaking changes + +- `ServiceEvent::ServiceData` is merged back with `ServiceResolved` (i.e. replacing it). The end result is: we have a single `ServiceEvent::ServiceResolved(ResolvedService)` going forward. + +Hence, a service is respresented by `ResolvedService` on the client side, and by `ServiceInfo` on the server side. + +And `user_service_data()` is no longer needed and removed. + +Sorry about the confusions but I think this helps for the long term. I think / hope the required code changes are minimal for most users. + +## All changes + +* `221e0be 2025-08-29` feat: impl AsIpAddrs for Box (#387) (Jean-Gab) +* `f88fae1 2025-08-27` merge ServiceData with ServiceResolved (#386) (keepsimple1) + +Thanks our new contributor @Jean-Gab, welcome! + +# Version 0.14.1 (2025-8-19) + +This is a bugfix release with only a doc comments change. + +* `ec4fb9a 2025-08-18` doc: add a missing line in doc code example (#384) (keepsimple1) + +# Version 0.14.0 (2025-8-10) + +## Breaking changes + +- `ServiceEvent::ServiceData` to support IPv6 with scope_id. It will deprecate `ServiceEvent::ServiceResolved`. + +Users must call `ServiceDaemon.use_service_data` to use `ServiceData` instead of `ServiceResolved` event. + +- `HostnameResolutionEvent::AddressesFound` uses the new `ScopedIp` instead of `IpAddr`. +- `ServiceEvent` is `non_exhaustive` now. + +## Other hightlights + +- Internal: define `MyUdpSocket` that uses PKTINFO. It also prepares for supporting unicast. +- Internal: define `MyIntf` to better handle multiple addresses on an interface. +- A few bugfixes. + +* `123ecba 2025-08-08` rename HostIp to ScopedIp (#382) (keepsimple1) +* `0c9395f 2025-08-07` rename ServiceDetailed to ServiceData (#381) (keepsimple1) +* `52cc67c 2025-08-06` refactoring: define our own interface struct to allow multiple addresses (#380) (keepsimple1) +* `1b194de 2025-08-01` feat: Remove unneeded multicast send tracking (#377) (hrzlgnm) +* `8cb1a59 2025-07-30` refactoring only: move run into ZeroConf (#376) (keepsimple1) +* `2eddad3 2025-07-30` ServiceEvent: add non_exhaustive and fix clippy (#375) (keepsimple1) +* `0d6ba35 2025-07-28` bugfix: remove duplicated address record for IPv6 on a wrong interface (#373) (keepsimple1) +* `45b3cdd 2025-07-16` fix repetitive ServiceRemoved and Resolve again after interface up (#371) (keepsimple1) +* `eb90591 2025-07-13` Exclude interfaces that are operational down (#369) (keepsimple1) +* `1103ab2 2025-07-08` feat: new API to use ResolvedService instead of ServiceInfo for resolved service event (#362) (keepsimple1) + +# Version 0.13.11 (2025-7-7) + +This is a small bugfix release before we add potential breaking changes. + +* `8b221d2 2025-06-22` derive `Clone` for `ServiceEvent` and `HostnameResolutionEvent` (#366) (Shadowcat650) + +Welcome our new contributor @Shadowcat650 ! + +# Version 0.13.10 (2025-6-21) + +This is a bugfix release. + +* `14841ac 2025-06-21` add comments for setting TTL (#364) (keepsimple1) +* `3db5e1b 2025-06-17` bugfix: set multicast TTL to 255 (#361) (Sameer Puri) +* `22a2688 2025-05-20` doc: add a section for Conflict resolution (#359) (keepsimple1) +* `60888b7 2025-05-13` Refactoring only: extract parts of probing_handler into functions (#357) (keepsimple1) +* `db2f484 2025-05-11` bugfix: not to resolve records that expires soon (#353) (keepsimple1) + +Welcome our new contributor @sameer ! + +# Version 0.13.9 (2025-04-22) + +This is a bugfix release. + +* `a734dd2 2025-05-02` bugfix: refresh TXT when needed (#354) (keepsimple1) +* `51c03c3 2025-05-01` bugfix: TXT records should use OTHER_TTL same as PTR (#355) (keepsimple1) +* `07750cb 2025-04-25` bugfix: only remove a service instance if all its SRV are gone (#350) (keepsimple1) + +# Version 0.13.8 (2025-04-22) + +This is a bugfix release that also prepares for adding InterfaceId in resolved service info. + +* `2d49195 2025-04-22` bugfix: should keep A records for hostname queriers (#348) (keepsimple1) +* `fe08df1 2025-04-22` bump up version to 0.13.8 (#347) (keepsimple1) +* `f6c7e80 2025-04-22` feat: extend `DnsAddress` with an `InterfaceId` (#342) (hrzlgnm) +* `513372d 2025-04-20` remove address filter for multicast loopback (#346) (keepsimple1) + +# Version 0.13.7 (2025-04-15) + +This is a bugfix release that further reduces the memory footprint of DNS cache. + +* `a6a0961 2025-04-15` optimization: remove cache entries when stop_browse (#344) (keepsimple1) + +# Version 0.13.6 (2025-04-07) + +This is a bugfix release that reduces / limits the memory footprint of the cached records and timers. + +* `5fa1b31 2025-04-07` optimization: remove expired timers and skip DNS datagrams that are not for us (#338) (keepsimple1) +* `3e3aec9 2025-03-29` refactoring: make tiebreaking more modular (#336) (keepsimple1) + +# Version 0.13.5 (2025-03-25) + +This is a patch fix release as the previous release (0.13.4) was broken in service resolution. + +* `e65757f 2025-03-26` bugfix: make `get_addr` method use lowercase hostname (#332) (Justus K) + +Thanks @Stupremee for the fix and welcome! + +# Version 0.13.4 (2025-03-24) + +## Highlights + +- New API for host IP check: `ServiceDaemon::set_ip_check_interval` and `ServiceDaemon::get_ip_check_interval`. +- Bugfixes. + +## All changes + +* `e64c0d4 2025-03-24` bugfix: hostname resolution should be case insensitive (#330) (keepsimple1) +* `5e4df27 2025-03-19` bugfix: when now equals next_ip_check (#328) (keepsimple1) +* `9538120 2025-03-12` examples: support logfile in the register example (#326) (keepsimple1) +* `8422690 2025-03-13` chore(deps): update fastrand and socket2 (#324) (CosminPerRam) +* `dd2d868 2025-03-07` feat: add boxed in DnsRecordExt to reduce Box::new usage (#323) (CosminPerRam) +* `3ea6cf1 2025-03-05` Detect IP changes: reduce the interval (#313) (keepsimple1) +* `7feabc9 2025-03-04` feat: handle DnsIncoming IPv4, 6 and String read parsing errors (#309) (CosminPerRam) + +# Version 0.13.3 (2025-03-01) + +## Highlights + +* `TxtProperties`: Support `into_property_map_str`. +* For querier: a new struct `ResolvedService` that can be created from `ServiceInfo`. +* Support a service to publish using loopback interfaces via `enable_interface`. +* Bugfixes. + +## All changes + +* `3e6842f 2025-02-28` enable_interface: support loopback interface (#317) (keepsimple1) +* `7e77a5e 2025-02-26` fix: remove related addresses in the cache when disabling an interface (#316) (keepsimple1) +* `735fb22 2025-02-22` refactoring: move handle_poller_events into Zeroconf impl (#315) (keepsimple1) +* `e886787 2025-02-20` ci: add doc check and fail if there are any warnings (#310) (CosminPerRam) +* `9d92545 2025-02-18` refactoring: make e_fmt! available within the crate (#311) (keepsimple1) +* `4b671d4 2025-02-14` ResolvedService: a new plain struct for attributes of a service (#302) (keepsimple1) +* `349de66 2025-02-13` fix: include loopback addresses in filtering criteria (#306) (Minetake) +* `026a745 2025-02-09` refactoring: property length check (#305) (keepsimple1) +* `818f37c 2025-02-10` TXT record: docs to remind users of the maximum length of the attribute. (#304) (Lazy Panda) +* `fba8025 2025-02-09` TxtProperties: new method to get a HashMap of properties (#303) (keepsimple1) + +# Version 0.13.2 (2025-02-02) + +This is a bugfix release. + +## All changes + +* 4288190 check any match for address records in conflict handler (#294) (keepsimple1) +* b51f67d unit test: fix a timing issue (#292) (keepsimple1) +* 7afed98 bugfix: check data len for NSEC record (#291) (keepsimple1) + +# Version 0.13.1 (2024-12-16) + +This is a bugfix release. Fixed a bug where upper case service names failed to publish. + +## All changes + +* 71647a1 test: cover upper case in service name (#288) (keepsimple1) +* 6ff9b52 fix: service keys must be lowercase (#286) (Jesper L. Nielsen) + +# Version 0.13.0 (2024-12-15) + +There are no breaking changes in API. Bump the minor version due to the change of rustc version to Rust 1.70.0. + +## Highlights + +* Use `mio` instead of `polling` to poll sockets. +* New API `set_multicast_loop_v4` and `set_multicast_loop_v6` of `ServiceDaemon`. +* All logging are updated to be `debug` or `trace` levels only. + +## All changes + +* 489ef5a test: fix a flaky test (#283) (keepsimple1) +* 1ddae63 feat: new API to set multicast loop for ServiceDaemon (#281) (keepsimple1) +* fcd31f3 dependency: use mio to replace polling (#280) (keepsimple1) +* 99483b7 reduce logging levels (#277) (keepsimple1) + +# Version 0.12.0 (2024-11-24) + +There are no breaking changes in API. Bump the minor version due to new features and the change of rustc version. + +## Highlights + +* Support name probing and conflict resolution [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-8) +* Support service liveness checking via `verify` API. [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-10.4) +* rustc version changed to 1.65.0 +* performance improvements and doc updates. + +## All changes + +* 7f6c5e9 perf: avoid cloning in filtering ptr (#272) (CosminPerRam) +* e185d6f refactoring: define an enum for DNS resource record types (#274) (keepsimple1) +* d117f4f refactoring: move exec_command into Zeroconf (#273) (keepsimple1) +* 39acd80 feat: replace remaining Box with type (#271) (CosminPerRam) +* b50fe8c perf: optimize u8_slice_to_hex by replacing Vec with String (#270) (CosminPerRam) +* db545b1 doc: some spelling fixes (#269) (CosminPerRam) +* 7328f45 doc: add a table of RFC compliance details (#268) (keepsimple1) +* 1ade666 feat: verify to support Cache Flush on Failure Indication (#267) (keepsimple1) +* 8b63fd7 feat: support name probing and conflict resolution (#265) (keepsimple1) +* 429ecde dev-test: enhance test case for ipv4 only auto addr (#263) (keepsimple1) +* f902cf2 register service: apply interface selection for auto IP addr (#262) (keepsimple1) +* 0381e30 dns_cache: address record should only flush on the same network (#261) (keepsimple1) + +# Version 0.11.5 (2024-09-28) + +This is a bugfix release. + +## All changes + +* 2829d8e tests: fix remove addr test (#258) (keepsimple1) +* 4f58e2f dns_parser: check against potential name compression loop (#257) (keepsimple1) + +# Version 0.11.4 (2024-09-10) + +Bugfixes. Added checks for corrupted RR data to prevent unnecessary panics. Thanks for new +contributor @rise0chen ! + +Sorry that this release has a few merged small commits as I didn't know how to properly +merge in a PR that targets a fetaure branch used in another PR, instead of `main` branch. + +## All changes + +* e54485e add --verbose in CI test run (#254) (keepsimple1) +* f0c4c27 remove fastrand dependency from dev-test (#252) (keepsimple1) +* dff1596 Merge pull request #250 from keepsimple1/rdata-check (keepsimple1) +* 659e684 fix cargo clippy warning (keepsimple1) +* 90a2f12 Merge pull request #251 from rise0chen/rdata-check (keepsimple1) +* 6d51f55 Merge branch 'rdata-check' into rdata-check (keepsimple1) +* 1b2cf40 add a check for rr data len (keepsimple1) +* a5de799 feat: test random data (rise0chen) +* 40698a3 add test case and simplify DnsTxt::new (keepsimple1) +* fc489bd refactoring error log (keepsimple1) +* a3fad8e add a check for rr data len (keepsimple1) + +# Version 0.11.3 (2024-08-23) + +A release of bugfixes and refactorings. + +## All changes + +* 3292110 DnsTxt debug print: make its text field human-readable (#247) (keepsimple1) (2024-08-21) +* 5567c1f Send SearchStarted events with addrs as `ip (intf-name)` (#245) (hrzlgnm) (2024-08-22) +* 9a91a53 cache flush: add the missing timer for updated expires (#244) (keepsimple1) (2024-08-19) +* c1d7efa Change intf_socks to a map of (Interface, Socket) (#242) (keepsimple1) (2024-08-18) +* 404100d refactor out a common method for DnsRecordExt (#241) (keepsimple1) (2024-08-18) +* f055c78 Refresh A and AAAA records of active `.browse` queriers (#240) (hrzlgnm) (2024-08-17) +* 0453030 Avoid redundant query, announcement and unregistration overhaul (#239) (hrzlgnm) (2024-08-16) + +# Version 0.11.2 (2024-08-06) + +Mostly a bugfix and refactoring release, with limited support added for: +- Known Answer Suppression (RFC 6762 section 7.1 and 7.2): + - single packet for querier and responder, + - multi-packet for querier. + +## All changes + +* 92eae74 add support for Known Answer Suppression part 2: multi-packet: querier side (#232) (keepsimple1) +* ada3486 fix test integration_success: respond count or known answer suppression count (#237) (keepsimple1) +* 8106d07 Skip link local addresses while checking for redundant announcements or query packets (#235) (hrzlgnm) +* b1a173a check data length in read_u16 (#234) (keepsimple1) +* d1c9157 Add sanity check for service type domain suffix in browse (#231) (keepsimple1) +* 736bec6 enable DEBUG logging for a failed test in CI (#229) (keepsimple1) +* fd00210 add logs in test to debug CI failure (#228) (keepsimple1) +* 5e0f1d3 add support for Known Answer Suppression part 1 (#227) (keepsimple1) +* 5ae18a6 refactoring: remove Send for DnsRecordBox (#226) (keepsimple1) +* d7d4867 fix integration_success test (#223) (keepsimple1) +* 6f34f1c move DnsCache into its own module (#221) (keepsimple1) +* bcdc2f9 add welcome to our new contributor (#220) (keepsimple1) + +# Version 0.11.1 (2024-05-13) + +## Highlights + +- Start to honor cache flush bit. +- Improved cache refresh logic. +- Code refactorings. +- And a few bugfixes. + +## All changes + +* 098f2df move unit tests into integration test (#218) +* 80291ba refresh PTR records (#217) +* 5eb74b5 refactoring: extract details from exec_command into own functions (#215) +* 551ed4d Bugfix: AddressesRemoved missing actual addrs (#210) +* 3c924f4 Bugfix: cache flush properly (#211) +* ccdae2d Bugfix: logging feature cannot be disabled (#212) +* 626f9fa refresh SRV records and send out ServiceRemoved for expired SRV (#180) +* 06e2cf7 feat: merge match same arms (#209) +* bf5cea3 perf: in adding answers, use static dispatch instead of dynamic dispatch (#207) +* 19d2161 feat: extract match addr to type as a function (#205) +* 5bdcdd6 feat: remove clone derive from counter (#208) +* e7fc0e0 feat: replace box dns with declared type (#206) +* 5732665 feat: apply nursery lints (#202) +* 16cb5cd feat: honor cache flush (#201) + +Welcome our new contributor: @lyager ! Thanks! + +# Version 0.11.0 (2024-04-21) + +## Breaking changes + +* Now `ServiceDaemon::register()` requires `hostname` to end with ".local." + +## New features + +* Support resolving hostnames directly: `ServiceDaemon::resolve_hostname()` + +## All changes + +* example code: refactor the query output prints and the register hostname (#189) +* support multiple questions in send_query_vec (#194) +* CI: fix a test waiting for IPv6 addr (#195) +* Add support for resolving non-service hostnames (#192) +* zeroconf: use min heap for timers (#196) +* Fix flaky test (#198) +* enable logging for examples and add doc for logging (#199) + +Welcome our new contributor: @oysteintveit-nordicsemi ! Thanks! + +# Version 0.10.5 (2024-03-24) + +## Notes + +* Port 0 is now considered valid in ServiceInfo (#181) + +## Changes + +* reduce SearchStopped notification send error to warn (#178) +* refactoring: extract handle_poller_events() (#177) +* Do not consider port 0 as a missing info (#181) +* query TYPE_A and TYPE_AAAA via Command::Resolve (#185) +* bump socket2 version (#174) +* add NSEC record to debug resolve issue (#183) + +Welcome our new contributors: @hrzlgnm and @irvingoujAtDevolution ! Thanks! + +# Version 0.10.4 (2024-02-10) + +This is a bug fix release. + +## Changes + +* Add sanity checks in DNS message decoding (#169) +* fine-tune MAX_MSG_ABSOLUTE (#170) + +# Version 0.10.3 (2024-01-14) + +This is a bug fix release. + +## Changes + +* netmask -> subnet (#164) + +Welcome our new contributor @amfaber ! Thanks! + +# Version 0.10.2 (2023-12-28) + +This is a bug fix release. + +## Changes + +* use human-readable address in error log of send_packet (#155) +* query for unresolved instances only when needed (#157) +* Fix panic due to range out of bounds in txt record parsing (#159) +* Sanity check for empty service type name (#160) +* Added comment for updating service info by re-registering. + +Welcome our new contributor @Raphiiko ! Thanks! + +Happy new year 2024! + +# Version 0.10.1 (2023-12-2) + +This is a bug fix release. + +## Changes + +* update flume to 0.11 (#152) +* bugfix: signal event key is possible to overlap with socket poll ids (#153) + +# Version 0.10.0 (2023-11-28) + +## Breaking changes + +* `ServiceDaemon::shutdown()` return type changed from `Result<()>` to `Result>` (#149) + +## Other changes + +* Related to the breaking change, a client can receive `DaemonStatus` to be sure the daemon is shutdown. +* A new enum `DaemonStatus` and a new API `ServiceDaemon::status()` are introduced. +* Updated CI in GitHub Actions: replace `actions-rs` with `dtolnay/rust-toolchain`. + +# Version 0.9.3 + +This is a bugfix release. + +* apply interface selections when IP addresses change (#142) +* Remove un-necessary panic (#144) +* Always include subtype info if exists (#146) + +p.s. Happy Halloween! + +# Version 0.9.2 + +The release includes a bugfix, thanks to @Mornix ! + +* fix PTR expiration from preventing later service resolution (#140) +* updated doc comments for `DnsCache::add_or_update`. + +# Version 0.9.1 + +There are no breaking changes. + +* support interface selection (#137) + +Added two new methods for `ServiceDaemon`: `enable_interface` and `disable_interface`, and some refactoring. + +# Version 0.9.0 + +* Ssupports IPv6 (#130) (Thanks to @izissise) +* ServiceInfo: support get_addresses_v4 (#132) +* bugfix: set address type correctly (#134) + +This is a breaking change, including: + +- Trait `AsIpv4Addrs` changes to `AsIpAddrs` to support both IPv4 and IPv6. +- `ServiceInfo::new()` uses the new `AsIpAddrs` trait. +- `ServiceInfo::get_addresses()` returns both IPv4 and IPv6 addresses, while a new convenience method `get_addresses_v4` returns IPv4 only. + +But in general, because the trait hides away details, the user code is likely keeping working without code changes. + +Improvements: + +* avoid redundant annoucement or query packets (#135) + +# Version 0.8.1 + +* Remove env_logger in dev-dependencies and lower MSRV to 1.60.0. (#128) + +# Version 0.8.0 + +No breaking changes in API. This release brings two potential user-visible changes: + +* use UDP socket to signal the daemon for commands. (#125) + +This change reduces CPU utilization of the daemon thread as well as its latency to +the user commands. Internally it uses local UDP sockets to signal the daemon. + +* Added the link-local feature to if_addrs in Cargo.toml to enable link-local interfaces in Windows. (#126) + +This change makes link-local interfaces visible to users in Windows where they didn't show up previously. + +# Version 0.7.5 + +* Revert the changes in v0.7.4 and support link-local addrs alongside routable addrs. (#122) + +# Version 0.7.4 (deprecated) + +* Not to use link-local addrs if routable addrs exist (#117) + +# Version 0.7.3 + +## Highlights + +- Internal refactoring: always use DnsCache to resolve Servive Instances. When processing incoming packets, +we used to update the cache one record at a time and also build separate service info structs to resolve. Now we finish the cache updates first, and then resolve instances from the cache. + +- Added env_logger for the examples code and enhanced the examples as well. + +## What's Changed + +* Support updating instances after they are resolved by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/104 +* add optional "unregister" in example code by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/107 +* Returns an error with logging for read_name invalid offset by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/109 +* register example should keep running by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/110 +* Refactoring DnsCache and how to resolve Service Instance by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/108 +* add sanity check in reading a record data RDATA by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/111 +* Enable logging for the examples by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/112 +* register example: a simpler input for the service type by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/113 + + +# Version 0.7.2 + +Highlights: + +- Implemented `Display` trait for `TxtProperty`: print using +`key=value` format, where `value` is same as `.get_property_val_str()`. + +- Implemented `Debug` trait for `TxtProperty`: print using +a struct format, where `value` prints as a string if it is UTF-8, or +prints as hex if it is not UTF-8. + +# Version 0.7.1 + +Highlights: + +- A bug fix: remove duplicated keys in TXT records received. + +# Version 0.7.0 + +Breaking Changes: + +- Allow non-standard max length for a service name. The check for +the length of a service name is moved to the daemon. If a service +name is too long, there will be an error log and an error event sent +to the monitors. + +- `ServiceInfo.get_property_val()` returns `Option>` +instead of `Option<&str>`. Now a new `ServiceInfo.get_property_val_str()` +returns `Option<&str>`. + +In other words, migrate to `get_property_val_str()` if you don't +want to worry about non-UTF8 values. + +Highlights: + +- Allow non-standard max length for a service name: A new method +`ServiceDaemon.set_service_name_len_max()` is added to support that. +Only use it when you really need to. +- Support non-UTF-8 value for TXT properties. +- Support `no value` for a TXT property, i.e. boolean keys. +- Added checks for ASCII keys in a TXT property. + +# Version 0.6.1 + +Highlights: + +- Fixs a bug: missing TXT records in received responses. + +# Version 0.6.0 + +Breaking Changes: + +- `ServiceInfo::new()` takes `IntoTxtProperties` trait instead of a +`HashMap` of properties. It is also backward-compatiable: the trait +is implemented for `HashMap` and `Option`. +- `ServiceInfo::get_properties()` returns `&TxtProperties` instead of +a `HashMap` of properties. It is also mostly backward-compatiable: +support `iter()`, `get()` methods. + +Highlights: + +- TXT properties' names are now case insensitive. And the original user input +order is kept. +- A new method `ServiceInfo::enable_addr_auto()`: automatically fill in IP +addresses for published services. +- Detect IP changes. +- A new `ServiceDaemon::monitor()` method to return a `Receiver` handle to +monitor the daemon events, such as IP changes. + +# Version 0.5.10 + +- skip interfaces that failed to bind (#79) (re-apply fix in v0.5.6) + +# Version 0.5.9 + +- Ignore duplicate keys (#74) +- update error msg for send_packet (#69) + +# Version 0.5.8 + +- call check_service_name before sending the cmd to the daemon. (#60) +- Changed dependency on 'log' crate to be optional (#64) +- configure mDNS daemon thread a name (#66) +- log an error if socket read returns 0 and reset the socket (#67) + +# Version 0.5.7 + +- Allow service names with trailing '.' (#56) +- query unresolved instances (#58) + +# Verison 0.5.6 + +- handle join_multicast_v4 error gracefully (#53) + +# Version 0.5.5 + +- track IPv4 interfaces with sockets to support multiple LANs (#48) + +# Version 0.5.4 + +- Fix a bug in resolving multiple IPs for a host. +- Code reorg: separate modules out of lib.rs. +- Listening socket joins multicast on all interfaces. + +# Version 0.5.3 + +- Support subtypes. +- Bind every valid IPv4 interface for outgoing sockets. +- Include Windows and macOS in GitHub Actions. + +# Version 0.5.2 + +- Add support for Windows platform. + +# Version 0.5.1 + +- Fix missing info in the license files. +- Add docs.rs badge. +- Make Error implement std::error::Error. + +# Version 0.5.0 + +- Allow multiple formats for host_ipv4 to create ServiceInfo. +- A breaking change: change `ServiceInfo::new()` to return a `Result<>`. +- Update `nix` dependency to version 0.24.1. + +# Version 0.4.3 + +- Fix a bug in stop-browse + +# Version 0.4.2 + +- New feature: support meta-query `_services._dns-sd._udp` per RFC 6763. + +# Version 0.4.1 + +- Update docs. + +# Version 0.4.0 + +- Replace `crossbeam-channel` with `flume`. + +# Version 0.3.0 + +- Add "get_metrics" in API. +- Fixed a bug in cache refresh. +- Fixed a bug in retransmission. + +# Version 0.2.2 + +- Add the first example code. Thanks @lu-zero! (#5) + +# Version 0.2.1 + +- mDNS daemon respond socket to be blocking for simpler send. + +# Version 0.2.0 + +- Public API internally to use the unblocking try_send() to replace send(). +- Add `Again` in Error type to support retry. + +# Version 0.1.0 + +- Initial version diff --git a/vendor/mdns-sd/Cargo.toml b/vendor/mdns-sd/Cargo.toml new file mode 100644 index 0000000..3d4db70 --- /dev/null +++ b/vendor/mdns-sd/Cargo.toml @@ -0,0 +1,116 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +rust-version = "1.71.0" +name = "mdns-sd" +version = "0.21.1" +authors = ["keepsimple "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "mDNS Service Discovery library with no async runtime dependency" +documentation = "https://docs.rs/mdns-sd" +readme = "README.md" +keywords = [ + "mdns", + "service-discovery", + "zeroconf", + "dns-sd", +] +categories = ["network-programming"] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/keepsimple1/mdns-sd" + +[features] +async = ["flume/async"] +default = [ + "async", + "logging", +] +logging = ["log"] +serde = ["dep:serde"] + +[lib] +name = "mdns_sd" +path = "src/lib.rs" + +[[example]] +name = "query" +path = "examples/query.rs" + +[[example]] +name = "register" +path = "examples/register.rs" + +[[test]] +name = "addr_parse" +path = "tests/addr_parse.rs" + +[[test]] +name = "mdns_test" +path = "tests/mdns_test.rs" + +[[test]] +name = "shutdown_test" +path = "tests/shutdown_test.rs" + +[dependencies.fastrand] +version = "2.4" + +[dependencies.flume] +version = "0.12" +default-features = false + +[dependencies.if-addrs] +version = "0.15" +features = ["link-local"] + +[dependencies.log] +version = "0.4" +optional = true + +[dependencies.mio] +version = "1.2" +features = [ + "os-poll", + "net", +] + +[dependencies.serde] +version = "1.0.228" +features = ["derive"] +optional = true + +[dependencies.socket-pktinfo] +version = "0.4.0" + +[dependencies.socket2] +version = "0.6" +features = ["all"] + +[dev-dependencies.env_logger] +version = "= 0.11.6" +features = ["humantime"] +default-features = false + +[dev-dependencies.humantime] +version = "2.3" + +[dev-dependencies.serde_json] +version = "1.0.150" + +[dev-dependencies.test-log] +version = "0.2.21" diff --git a/vendor/mdns-sd/Cargo.toml.orig b/vendor/mdns-sd/Cargo.toml.orig new file mode 100644 index 0000000..1df5669 --- /dev/null +++ b/vendor/mdns-sd/Cargo.toml.orig @@ -0,0 +1,35 @@ +[package] +name = "mdns-sd" +version = "0.21.1" +authors = ["keepsimple "] +edition = "2018" +rust-version = "1.71.0" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/keepsimple1/mdns-sd" +documentation = "https://docs.rs/mdns-sd" +keywords = ["mdns", "service-discovery", "zeroconf", "dns-sd"] +categories = ["network-programming"] +description = "mDNS Service Discovery library with no async runtime dependency" + +[features] +async = ["flume/async"] +logging = ["log"] +serde = ["dep:serde"] +default = ["async", "logging"] + +[dependencies] +fastrand = "2.4" +flume = { version = "0.12", default-features = false } # channel between threads +if-addrs = { version = "0.15", features = ["link-local"] } # get local IP addresses +log = { version = "0.4", optional = true } # logging +mio = { version = "1.2", features = ["os-poll", "net"] } # select/poll sockets +socket2 = { version = "0.6", features = ["all"] } # socket APIs +socket-pktinfo = "0.4.0" +# support for serde's deserialize/serialize traits +serde = { version = "1.0.228", features = ["derive"], optional = true } + +[dev-dependencies] +env_logger = { version = "= 0.11.6", default-features = false, features = ["humantime"] } +humantime = "2.3" +serde_json = "1.0.150" +test-log = "0.2.21" diff --git a/vendor/mdns-sd/LANSPREAD_PATCH.md b/vendor/mdns-sd/LANSPREAD_PATCH.md new file mode 100644 index 0000000..c03ebb3 --- /dev/null +++ b/vendor/mdns-sd/LANSPREAD_PATCH.md @@ -0,0 +1,18 @@ +# Lanspread mdns-sd patch + +This directory vendors `mdns-sd` 0.21.1 under its original MIT/Apache-2.0 +license. Lanspread carries a narrow defensive patch because the upstream API +does not expose the source address of resolved service responses and its cache +and timer collections have no hard admission limits. + +The local changes: + +- retain the observed source IP with cached records and expose the source of a + resolved service; +- cap cached records at 1,024 globally and 128 per observed source; +- cap and deduplicate daemon timers at 4,096 entries; and +- wake the daemon at least once per second so dropped late timers cannot defer + expiry cleanup indefinitely. + +Keep this directory at the exact version recorded in `Cargo.lock`. Rebase the +patch explicitly when updating the upstream crate. diff --git a/vendor/mdns-sd/LICENSE-APACHE b/vendor/mdns-sd/LICENSE-APACHE new file mode 100644 index 0000000..a962b36 --- /dev/null +++ b/vendor/mdns-sd/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2021-2022] [Han Xu, keepsimple@gmail.com] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/mdns-sd/LICENSE-MIT b/vendor/mdns-sd/LICENSE-MIT new file mode 100644 index 0000000..96053ba --- /dev/null +++ b/vendor/mdns-sd/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2022, Han Xu, keepsimple@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/mdns-sd/README.md b/vendor/mdns-sd/README.md new file mode 100644 index 0000000..441452b --- /dev/null +++ b/vendor/mdns-sd/README.md @@ -0,0 +1,81 @@ +# mdns-sd + +[![Build](https://github.com/keepsimple1/mdns-sd/actions/workflows/build.yml/badge.svg)](https://github.com/keepsimple1/mdns-sd/actions) +[![Cargo](https://img.shields.io/crates/v/mdns-sd.svg)](https://crates.io/crates/mdns-sd) +[![docs.rs](https://img.shields.io/docsrs/mdns-sd)](https://docs.rs/mdns-sd/latest/mdns_sd/) +[![Rust version: 1.70+](https://img.shields.io/badge/rust%20version-1.70+-orange)](https://blog.rust-lang.org/2022/08/11/Rust-1.70.0.html) + +This is a small implementation of mDNS (Multicast DNS) based service discovery in safe Rust, with a small set of dependencies. Some highlights: + +- supports both the client (querier) and the server (responder) uses. +- supports macOS, Linux and Windows. +- supports IPv4 and IPv6. +- works with both sync and async code. +- no dependency on any async runtimes. + +## Approach + +We are not using async/.await internally, instead we create a new thread to run a mDNS daemon. + +The API interacts with the daemon via [`flume`](https://crates.io/crates/flume) channels that work easily with both sync and async code. For more details, please see the [documentation](https://docs.rs/mdns-sd). + +## Compatibility and Limitations + +This implementation is based on the following RFCs: +- mDNS: [RFC 6762](https://tools.ietf.org/html/rfc6762) +- DNS-SD: [RFC 6763](https://tools.ietf.org/html/rfc6763) +- DNS: [RFC 1035](https://tools.ietf.org/html/rfc1035) + +This is still beta software. We focus on the common use cases at hand. And we tested with some existing common tools (e.g. `Avahi` on Linux, `dns-sd` on MacOS, and `Bonjour` library on iOS) to verify the basic compatibility. + +The following table shows how much this implementation is compliant with RFCs regarding major features: + +| Feature | RFC section | Compliance | Notes | +| ------- | ----------- | ---------- | ----- | +| One-Shot Multicast DNS Queries (i.e. Legacy Unicast Responses) | RFC 6762 [section 5.1][ref1] [section 6.7][ref9] | ✅ | Reply unicast to the querier when the source port is not 5353, regardless of whether the query was multicast or unicast | +| Randomized Initial Query Delay | RFC 6762 [section 5.2][ref12] | ✅ | jitter the first query of a continuous-monitoring series by a random delay to avoid synchronization across queriers. ℹ️ we use a shorter 10-50 ms window instead of the RFC's 20-120 ms | +| Unicast Responses | RFC 6762 [section 5.4][ref2] | ❌ | +| Multicast Rate Limiting | RFC 6762 [section 6][ref13] | ✅ | a given record is not re-multicast on an interface until at least one second has elapsed. ℹ️ probe queries and legacy unicast responses (§6.7) are exempt per the RFC; the separate 250 ms probe-query interval is not yet implemented | +| Response Delay for Shared Records | RFC 6762 [section 6][ref13] | ✅ | delay responses to shared (e.g. PTR) queries by a random amount to allow aggregation and reduce collisions. ℹ️ we use a shorter window than the RFC's 20-120 ms | +| Known-Answer Suppression | RFC 6762 [section 7.1][ref3] | ✅ | +| Multipacket Known Answer Suppression querier | RFC 6762 [section 7.2][ref4] | ✅ | +| Multipacket Known Answer Suppression responder | RFC 6762 [section 7.2][ref4] | ❌ | because we don't support Unicast yet. | +| Probing | RFC 6762 [section 8.1][ref5] | ✅ | +| Simultaneous Probe Tiebreaking | RFC 6762 [section 8.2][ref6] | ✅ | +| Conflict Resolution | RFC 6762 [section 9][ref7] | ✅ | see `DnsNameChange` type | +| Goodbye Packets | RFC 6762 [section 10.1][ref10] | ✅ | +| Announcements to Flush Outdated Cache Entries | RFC 6762 [section 10.2][ref11] | ✅ | i.e. `cache-flush` bit | +| Cache Flush on Failure Indication | RFC 6762 [section 10.4][ref8] | ✅ | API: `ServiceDaemon::verify()` | +| Outgoing packet size | RFC 6762 [section 17][ref14] | ✅ | By default, outgoing packet max size is 1452 bytes, i.e. the Ethernet MTU. API: `ServiceDaemon::set_max_packet_size()` | + +[ref1]: https://datatracker.ietf.org/doc/html/rfc6762#section-5.1 +[ref2]: https://datatracker.ietf.org/doc/html/rfc6762#section-5.4 +[ref3]: https://datatracker.ietf.org/doc/html/rfc6762#section-7.1 +[ref4]: https://datatracker.ietf.org/doc/html/rfc6762#section-7.2 +[ref5]: https://datatracker.ietf.org/doc/html/rfc6762#section-8.1 +[ref6]: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 +[ref7]: https://datatracker.ietf.org/doc/html/rfc6762#section-9 +[ref8]: https://datatracker.ietf.org/doc/html/rfc6762#section-10.4 +[ref9]: https://datatracker.ietf.org/doc/html/rfc6762#section-6.7 +[ref10]: https://datatracker.ietf.org/doc/html/rfc6762#section-10.1 +[ref11]: https://datatracker.ietf.org/doc/html/rfc6762#section-10.2 +[ref12]: https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 +[ref13]: https://datatracker.ietf.org/doc/html/rfc6762#section-6 +[ref14]: https://datatracker.ietf.org/doc/html/rfc6762#section-17 + +## License + +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Contributions are welcome! Please open an issue in GitHub if any questions. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the above license(s), shall be +dual licensed as above, without any additional terms or conditions. diff --git a/vendor/mdns-sd/examples/query.rs b/vendor/mdns-sd/examples/query.rs new file mode 100644 index 0000000..fd43766 --- /dev/null +++ b/vendor/mdns-sd/examples/query.rs @@ -0,0 +1,100 @@ +//! A mDNS query client. +//! +//! Run with: +//! +//! cargo run --example query +//! +//! Example: +//! +//! cargo run --example query _my-service._udp +//! +//! Note: there is no '.' at the end as the program adds ".local." +//! automatically. +//! +//! Keeps listening for new events. + +use mdns_sd::{ServiceDaemon, ServiceEvent}; + +fn main() { + env_logger::builder().format_timestamp_millis().init(); + + let mut service_type = match std::env::args().nth(1) { + Some(arg) => arg, + None => { + print_usage(); + return; + } + }; + service_type.push_str(".local."); + + // Showcase `verify` functionality for IPv4 addresses + let should_verify = match std::env::args().nth(2) { + Some(arg) if arg == "--verify" => true, + _ => false, + }; + + // Create a daemon + let mdns = ServiceDaemon::new().expect("Failed to create daemon"); + + // Browse for the service type + let receiver = mdns.browse(&service_type).expect("Failed to browse"); + + let now = std::time::Instant::now(); + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "At {:?}: Resolved a new service: {}\n host: {}\n port: {}", + now.elapsed(), + info.fullname, + info.host, + info.port, + ); + let mut found_ipv4 = false; + for addr in info.addresses.iter() { + println!(" Address: {addr}"); + if addr.is_ipv4() { + found_ipv4 = true; + } + } + for prop in info.txt_properties.iter() { + println!(" Property: {}", prop); + } + + if should_verify && found_ipv4 { + println!("Will verify after 3 seconds..."); + std::thread::sleep(std::time::Duration::from_secs(3)); + + let instance_fullname = info.fullname; + let timeout = std::time::Duration::from_secs(2); + if let Err(e) = mdns.verify(instance_fullname, timeout) { + println!("Verify failed: {}", e); + } else { + println!("Verify started"); + } + } + } + ServiceEvent::ServiceRemoved(service_type, fullname) => { + println!( + "At {:?}: ** service removed **: {service_type}: {fullname}", + now.elapsed(), + ); + } + other_event => { + println!("At {:?}: {:?}", now.elapsed(), &other_event); + } + } + } +} + +fn print_usage() { + println!("Usage: cargo run --example query [--verify]"); + println!("Example: "); + println!("cargo run --example query _my-service._udp"); + println!(); + println!("Options:"); + println!("--verify: make the client attempt to verify IPv4 addresses of resolved services."); + println!(); + println!("You can also do a meta-query per RFC 6763 to find which services are available:"); + println!("cargo run --example query _services._dns-sd._udp"); +} diff --git a/vendor/mdns-sd/examples/register.rs b/vendor/mdns-sd/examples/register.rs new file mode 100644 index 0000000..ce5a925 --- /dev/null +++ b/vendor/mdns-sd/examples/register.rs @@ -0,0 +1,163 @@ +//! Registers a mDNS service. +//! +//! Run with: +//! +//! cargo run --example register [options] +//! +//! Example: +//! +//! cargo run --example register _my-hello._udp instance1 host1 +//! +//! Options: +//! "--unregister": automatically unregister after 2 seconds. +//! "--disable-ipv6": not to use IPv6 interfaces. +//! "--logfile": write debug log to a file instead of stderr. +//! +//! For example: to see the debug log, set the RUST_LOG environment variable and write a logfile: +//! +//! RUST_LOG=mdns_sd=debug cargo run --example register _my-hello._udp instance1 host1 --logfile + +use std::{ + env, + fs::File, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use mdns_sd::{DaemonEvent, IfKind, ServiceDaemon, ServiceInfo}; + +fn main() { + // Simple command line options. + let args: Vec = env::args().collect(); + let mut should_unreg = false; + let mut disable_ipv6 = false; + let mut use_logfile = false; + let mut include_apple_p2p = false; + + for arg in args.iter() { + if arg.as_str() == "--unregister" { + should_unreg = true; + } else if arg.as_str() == "--disable-ipv6" { + disable_ipv6 = true; + } else if arg.as_str() == "--logfile" { + use_logfile = true; + } else if arg.as_str() == "--include-apple-p2p" { + include_apple_p2p = true; + } + } + + // setup env_logger + let mut builder = env_logger::Builder::from_default_env(); + if use_logfile { + let now = SystemTime::now(); + let duration = now + .duration_since(UNIX_EPOCH) + .expect("Time went backwards: failed to get UNIX timestamp"); + let log_filename = format!("mdns-register-{}.log", duration.as_secs()); + let file = File::create(&log_filename).unwrap(); + builder.target(env_logger::Target::Pipe(Box::new(file))); + println!("Logging to file: {}\n", log_filename); + } + + // more precise timestamp. + builder.format_timestamp_millis().init(); + + // Create a new mDNS daemon. + let mdns = ServiceDaemon::new().expect("Could not create service daemon"); + + if disable_ipv6 { + mdns.disable_interface(IfKind::IPv6).unwrap(); + } + + if include_apple_p2p { + mdns.include_apple_p2p(true).unwrap(); + } + + let service_type = match args.get(1) { + Some(arg) => format!("{}.local.", arg), + None => { + print_usage(); + return; + } + }; + let instance_name = match args.get(2) { + Some(arg) => arg, + None => { + print_usage(); + return; + } + }; + let hostname = match args.get(3) { + Some(arg) => arg, + None => { + print_usage(); + return; + } + }; + + // With `enable_addr_auto()`, we can give empty addrs and let the lib find them. + // If the caller knows specific addrs to use, then assign the addrs here. + let my_addrs = ""; + let service_hostname = format!("{}.local.", hostname); + let port = 3456; + + // The key string in TXT properties is case insensitive. Only the first + // (key, val) pair will take effect. + let properties = [("PATH", "one"), ("Path", "two"), ("PaTh", "three")]; + + // Register a service. + let service_info = ServiceInfo::new( + &service_type, + instance_name, + &service_hostname, + my_addrs, + port, + &properties[..], + ) + .expect("valid service info") + .enable_addr_auto(); + + // Optionally, we can monitor the daemon events. + let monitor = mdns.monitor().expect("Failed to monitor the daemon"); + let service_fullname = service_info.get_fullname().to_string(); + mdns.register(service_info) + .expect("Failed to register mDNS service"); + + println!("Registered service {}.{}", &instance_name, &service_type); + + if should_unreg { + let wait_in_secs = 2; + println!("Sleeping {} seconds before unregister", wait_in_secs); + thread::sleep(Duration::from_secs(wait_in_secs)); + + let receiver = mdns.unregister(&service_fullname).unwrap(); + while let Ok(event) = receiver.recv() { + println!("unregister result: {:?}", &event); + } + } else { + // Monitor the daemon events. + while let Ok(event) = monitor.recv() { + println!("Daemon event: {:?}", &event); + if let DaemonEvent::Error(e) = event { + println!("Failed: {}", e); + break; + } + } + } +} + +fn print_usage() { + println!("Usage:"); + println!("cargo run --example register [options]"); + println!("\nOptions:\n"); + println!("--unregister: automatically unregister after 2 seconds"); + println!("--disable-ipv6: not to use IPv6 interfaces."); + println!("--logfile: write debug log to a file instead of stderr. The logfile is named 'mdns-register-.log'."); + println!("--include-apple-p2p: include Apple p2p interfaces (e.g., awdl, llw) for mDNS."); + println!(); + println!("For example:"); + println!("cargo run --example register _my-hello._udp instance1 host1"); + println!(""); + println!("To see the debug log, set the RUST_LOG environment variable and write a logfile:"); + println!("RUST_LOG=mdns_sd=debug cargo run --example register _my-hello._udp instance1 host1 --logfile"); +} diff --git a/vendor/mdns-sd/src/dns_cache.rs b/vendor/mdns-sd/src/dns_cache.rs new file mode 100644 index 0000000..1ed7888 --- /dev/null +++ b/vendor/mdns-sd/src/dns_cache.rs @@ -0,0 +1,1219 @@ +//! A cache for DNS records. +//! +//! This is an internal implementation, not visible to the public API. + +use std::{ + collections::{HashMap, HashSet}, + net::IpAddr, + ops::BitOr, +}; + +#[cfg(feature = "logging")] +use crate::log::{debug, trace}; +use crate::{ + current_time_millis, + dns_parser::{DnsAddress, DnsPointer, DnsRecordBox, DnsSrv, InterfaceId, RRType}, + service_info::{split_sub_domain, MyIntf}, + ScopedIp, +}; + +/// Hard bounds for data accepted from unauthenticated multicast responders. +/// Lanspread browses one service type, so these limits are deliberately much +/// larger than a normal LAN party while keeping attacker-controlled state +/// finite. +pub(crate) const MAX_CACHE_RECORDS: usize = 1_024; +pub(crate) const MAX_CACHE_RECORDS_PER_SOURCE: usize = 128; + +/// Bitflags-style type for filtering by IP version. +#[derive(Clone, Copy)] +pub(crate) struct IpType(u8); + +impl IpType { + pub const V4: Self = Self(0b01); + pub const V6: Self = Self(0b10); + pub const BOTH: Self = Self(0b11); + + fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } +} + +impl BitOr for IpType { + type Output = Self; + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } +} + +/// The result of removing records on a specific interface. +pub(crate) struct IntfRemovalResult { + /// Map of ty_domain -> set of fully removed instance names (PTR gone). + pub(crate) removed_instances: HashMap>, + /// Set of instance names that lost records but still have PTR entries. + pub(crate) modified_instances: HashSet, +} + +/// Associate a DnsRecord with the interface it was received on. +pub(crate) struct DnsRecordIntf { + pub(crate) record: DnsRecordBox, + pub(crate) src_intf: InterfaceId, + pub(crate) source_ip: IpAddr, +} + +/// A cache for all types of DNS records. +pub(crate) struct DnsCache { + /// DnsPointer records indexed by ty_domain + ptr: HashMap>, + + /// DnsSrv records indexed by the fullname of an instance + srv: HashMap>, + + /// DnsTxt records indexed by the fullname of an instance + txt: HashMap>, + + /// DnsAddr records indexed by the hostname in lowercase. + addr: HashMap>, + + /// A reverse lookup table from "instance fullname" to "subtype PTR name" + subtype: HashMap, + + /// Negative responses: + /// A map from "instance fullname" to DnsNSec. + nsec: HashMap>, +} + +impl DnsCache { + pub(crate) fn new() -> Self { + Self { + ptr: HashMap::new(), + srv: HashMap::new(), + txt: HashMap::new(), + addr: HashMap::new(), + subtype: HashMap::new(), + nsec: HashMap::new(), + } + } + + pub(crate) fn all_ptr(&self) -> &HashMap> { + &self.ptr + } + + /// Count all PTR records in the cache. + pub(crate) fn ptr_count(&self) -> usize { + self.ptr.values().map(|v| v.len()).sum() + } + + pub(crate) fn srv_count(&self) -> usize { + self.srv.values().map(|v| v.len()).sum() + } + + pub(crate) fn txt_count(&self) -> usize { + self.txt.values().map(|v| v.len()).sum() + } + + pub(crate) fn addr_count(&self) -> usize { + self.addr.values().map(|v| v.len()).sum() + } + + pub(crate) fn nsec_count(&self) -> usize { + self.nsec.values().map(|v| v.len()).sum() + } + + pub(crate) fn subtype_count(&self) -> usize { + self.subtype.len() + } + + fn record_count(&self) -> usize { + self.ptr_count() + + self.srv_count() + + self.txt_count() + + self.addr_count() + + self.nsec_count() + } + + fn record_count_from_source(&self, source_ip: IpAddr) -> usize { + self.ptr + .values() + .chain(self.srv.values()) + .chain(self.txt.values()) + .chain(self.addr.values()) + .chain(self.nsec.values()) + .flatten() + .filter(|record| record.source_ip == source_ip) + .count() + } + + pub(crate) fn get_ptr(&self, ty_domain: &str) -> Option<&Vec> { + self.ptr.get(ty_domain) + } + + pub(crate) fn get_srv(&self, fullname: &str) -> Option<&Vec> { + self.srv.get(fullname) + } + + pub(crate) fn get_txt(&self, fullname: &str) -> Option<&Vec> { + self.txt.get(fullname) + } + + pub(crate) fn get_addr(&self, hostname: &str) -> Option<&Vec> { + self.addr.get(&hostname.to_lowercase()) + } + + /// A reverse lookup table from "instance fullname" to "subtype PTR name" + pub(crate) fn get_subtype(&self, fullname: &str) -> Option<&String> { + self.subtype.get(fullname) + } + + /// Returns the list of instances that has `host` as its hostname. + pub(crate) fn get_instances_on_host(&self, host: &str) -> Vec { + self.srv + .iter() + .filter_map(|(instance, srv_list)| { + if let Some(item) = srv_list.first() { + if let Some(dns_srv) = item.record.any().downcast_ref::() { + if dns_srv.host() == host { + return Some(instance.clone()); + } + } + } + None + }) + .collect() + } + + /// Returns a hashmap of hostnames and their addresses for a given `host`. + /// + /// Note that the keys in the returned HashMap are the same hostname, with different cases + /// of letters (e.g. "example.local.", "Example.local.", "EXAMPLE.local."). + pub(crate) fn get_addresses_for_host(&self, host: &str) -> HashMap> { + let hostname_lower = host.to_lowercase(); + let mut result = HashMap::new(); + + if let Some(records) = self.addr.get(&hostname_lower) { + for record in records { + if let Some(dns_addr) = record.record.any().downcast_ref::() { + let record_name = record.record.get_name().to_string(); + let address = dns_addr.address(); + + // Use the entry API to insert or update the HashSet for the record_name + result + .entry(record_name) + .or_insert_with(HashSet::new) + .insert(address); + } + } + } + + result + } + + /// Returns a list of resource records (name, rr_type) that need to be queried in order to + /// verify the `instance`. + /// + /// If `expire_at` is not None, the resource records' expire time will be updated. + pub(crate) fn service_verify_queries( + &mut self, + instance: &str, + expire_at: Option, + ) -> Vec<(String, RRType)> { + let Some(srv_vec) = self.srv.get_mut(instance) else { + return Vec::new(); + }; + + let mut query_vec = vec![(instance.to_string(), RRType::SRV)]; + + for srv in srv_vec { + if let Some(new_expire) = expire_at { + srv.record.set_expire_sooner(new_expire); + } + + let Some(srv_record) = srv.record.any().downcast_ref::() else { + continue; + }; + + // Will verify addresses for the hostname. + query_vec.push((srv_record.host().to_string(), RRType::A)); + query_vec.push((srv_record.host().to_string(), RRType::AAAA)); + + if let Some(new_expire) = expire_at { + if let Some(addrs) = self.addr.get_mut(srv_record.host()) { + for addr in addrs { + addr.record.set_expire_sooner(new_expire); + } + } + } + } + + query_vec + } + + /// Update a DNSRecord TTL if already exists, otherwise insert a new record. + /// + /// Returns `None` if `incoming` is invalid / unrecognized, otherwise returns + /// (a new record, true) or (existing record with TTL updated, false). + /// + /// If you need to add new timers for related records, push into `timers`. + pub(crate) fn add_or_update( + &mut self, + intf: &MyIntf, + source_ip: IpAddr, + incoming: DnsRecordBox, + timers: &mut Vec, + is_for_us: bool, + ) -> Option<(&DnsRecordIntf, bool)> { + let entry_name = incoming.get_name().to_string(); + + // Look up the existing records without creating an entry yet. + let entry_name_lower = entry_name.to_lowercase(); + let existing_records = match incoming.get_type() { + RRType::PTR => self.ptr.get(&entry_name), + RRType::SRV => self.srv.get(&entry_name), + RRType::TXT => self.txt.get(&entry_name), + RRType::A | RRType::AAAA => self.addr.get(&entry_name_lower), + RRType::NSEC => self.nsec.get(&entry_name), + _ => return None, + }; + let empty_records = existing_records.is_none_or(Vec::is_empty); + let existing_record = existing_records.is_some_and(|records| { + records + .iter() + .any(|record| record.record.matches(incoming.as_ref())) + }); + + // No existing records for this name and type, and not for us. + if empty_records && !is_for_us { + trace!("add_or_update: not for us: {}", incoming.get_name()); + return None; + } + + // Exact refreshes allocate no cache record and remain allowed at the + // boundary. New attacker-controlled records must fit both limits. + if !existing_record + && (self.record_count() >= MAX_CACHE_RECORDS + || self.record_count_from_source(source_ip) >= MAX_CACHE_RECORDS_PER_SOURCE) + { + trace!("add_or_update: cache admission limit reached for source {source_ip}"); + return None; + } + + // Subtype state is derived only after the corresponding PTR record is + // admitted, so rejected names cannot grow the auxiliary map. + if incoming.get_type() == RRType::PTR && is_for_us { + let (_, subtype_opt) = split_sub_domain(&entry_name); + if let Some(subtype) = subtype_opt { + if let Some(ptr) = incoming.any().downcast_ref::() { + self.subtype + .entry(ptr.alias().to_string()) + .or_insert_with(|| subtype.to_string()); + } + } + } + + // We want to process this `incoming`. For convenience, repeats the lookup + // above but doing `entry().or_default()` to create an empty Vec as needed. + let record_vec = match incoming.get_type() { + RRType::PTR => self.ptr.entry(entry_name).or_default(), + RRType::SRV => self.srv.entry(entry_name).or_default(), + RRType::TXT => self.txt.entry(entry_name).or_default(), + RRType::A | RRType::AAAA => self.addr.entry(entry_name_lower).or_default(), + RRType::NSEC => self.nsec.entry(entry_name).or_default(), + _ => return None, + }; + + if incoming.get_cache_flush() { + apply_cache_flush(&incoming, record_vec, timers); + } + + // update TTL for existing record or create a new record. + let (idx, updated) = match record_vec + .iter_mut() + .enumerate() + .find(|(_idx, r)| r.record.matches(incoming.as_ref())) + { + Some((i, r)) => { + // It is possible that this record was just updated in cache_flush + // processing. That's okay. We can still reset here. + r.record.reset_ttl(incoming.as_ref()); + (i, false) + } + None => { + let new_record = DnsRecordIntf { + record: incoming, + src_intf: intf.into(), + source_ip, + }; + record_vec.insert(0, new_record); // A new record. + (0, true) + } + }; + + Some((record_vec.get(idx).unwrap(), updated)) + } + + /// Remove a record from the cache if exists, otherwise no-op + pub(crate) fn remove(&mut self, record: &DnsRecordBox) -> bool { + let mut found = false; + let record_name = record.get_name(); + let record_vec = match record.get_type() { + RRType::PTR => self.ptr.get_mut(record_name), + RRType::SRV => self.srv.get_mut(record_name), + RRType::TXT => self.txt.get_mut(record_name), + RRType::A | RRType::AAAA => self.addr.get_mut(record_name), + _ => return found, + }; + if let Some(record_vec) = record_vec { + record_vec.retain(|x| match x.record.matches(record.as_ref()) { + true => { + found = true; + false + } + false => true, + }); + } + self.cleanup_empty_buckets(); + found + } + + fn cleanup_empty_buckets(&mut self) { + self.ptr.retain(|_, records| !records.is_empty()); + self.srv.retain(|_, records| !records.is_empty()); + self.txt.retain(|_, records| !records.is_empty()); + self.addr.retain(|_, records| !records.is_empty()); + self.nsec.retain(|_, records| !records.is_empty()); + + let live_instances = self + .ptr + .values() + .flatten() + .filter_map(|record| record.record.any().downcast_ref::()) + .map(|ptr| ptr.alias().to_string()) + .collect::>(); + self.subtype + .retain(|instance, _| live_instances.contains(instance)); + } + + /// Iterates all ADDR records and remove ones that expired. + /// Returns the expired ones in a map of names and addresses. + pub(crate) fn evict_expired_addr(&mut self, now: u64) -> HashMap> { + let mut removed = HashMap::new(); + + self.addr.retain(|_, records| { + records.retain(|addr| { + let expired = addr.record.get_record().is_expired(now); + if expired { + if let Some(addr_record) = addr.record.any().downcast_ref::() { + trace!("evict expired ADDR: {:?}", addr_record); + removed + .entry(addr.record.get_name().to_string()) + .or_insert_with(HashSet::new) + .insert(addr_record.address()); + } + } + !expired + }); + + !records.is_empty() + }); + + removed + } + + /// Evicts expired PTR and SRV, TXT records for each ty_domain in the cache, and + /// returns the set of expired instance names for each ty_domain. + /// + /// An instance in the returned set indicates its PTR and/or SRV record has expired. + pub(crate) fn evict_expired_services(&mut self, now: u64) -> HashMap> { + let mut expired_instances = HashMap::new(); + + // Check all ty_domain in the cache by following all PTR records, regardless + // if the ty_domain is actively queried or not. + for (ty_domain, ptr_records) in self.ptr.iter_mut() { + for ptr in ptr_records.iter() { + if let Some(dns_ptr) = ptr.record.any().downcast_ref::() { + let instance_name = dns_ptr.alias(); + + // evict expired SRV records of this instance + if let Some(srv_records) = self.srv.get_mut(instance_name) { + srv_records.retain(|srv| { + let expired = srv.record.get_record().is_expired(now); + !expired + }); + + if srv_records.is_empty() { + debug!("expired SRV for {}: {:?}", ty_domain, instance_name); + expired_instances + .entry(ty_domain.to_string()) + .or_insert_with(HashSet::new) + .insert(instance_name.to_string()); + + // don't keep empty value for this key. + self.srv.remove(instance_name); + } + } + + // evict expired TXT records of this instance + if let Some(txt_records) = self.txt.get_mut(instance_name) { + txt_records.retain(|txt| !txt.record.get_record().is_expired(now)) + } + } + } + + // evict expired PTR records + ptr_records.retain(|x| { + let expired = x.record.get_record().is_expired(now); + if expired { + if let Some(dns_ptr) = x.record.any().downcast_ref::() { + trace!("expired PTR: domain:{ty_domain} record: {:?}", dns_ptr); + expired_instances + .entry(ty_domain.to_string()) + .or_insert_with(HashSet::new) + .insert(dns_ptr.alias().to_string()); + } + } + !expired + }); + } + + self.nsec.values_mut().for_each(|records| { + records.retain(|record| !record.record.get_record().is_expired(now)); + }); + self.cleanup_empty_buckets(); + + expired_instances + } + + /// Removes all records of a service type: PTR, SRV, TXT records and any ADDR records + /// that are not referenced by any SRV record. + pub(crate) fn remove_service_type(&mut self, ty_domain: &str) { + let Some(ptr_records) = self.ptr.get_mut(ty_domain) else { + return; + }; + + let mut hosts = HashSet::new(); + + for ptr in ptr_records.iter() { + if let Some(dns_ptr) = ptr.record.any().downcast_ref::() { + let instance_name = dns_ptr.alias(); + + // collect all hostnames from SRV records of this instance + if let Some(srv_records) = self.srv.get_mut(instance_name) { + for srv in srv_records.iter() { + if let Some(dns_srv) = srv.record.any().downcast_ref::() { + hosts.insert(dns_srv.host().to_lowercase()); + } + } + } + + // remove all SRV records of this instance + self.srv.remove(instance_name); + + // remove all TXT records of this instance + self.txt.remove(instance_name); + } + } + + self.ptr.remove(ty_domain); + self.cleanup_empty_buckets(); + + // Check all hostnames in `hosts`: for each hostname, check if any SRV record + // has `hostname` as its host. If no such SRV, remove the ADDR records of this hostname. + for host in hosts { + let mut has_srv = false; + for srv_records in self.srv.values() { + for srv in srv_records.iter() { + if let Some(dns_srv) = srv.record.any().downcast_ref::() { + if dns_srv.host().to_lowercase() == host { + has_srv = true; + break; + } + } + } + if has_srv { + break; + } + } + + if !has_srv { + self.addr.remove(&host); + } + } + } + + /// Checks refresh due for PTR records of `ty_domain`. + /// Returns all updated refresh time. + pub(crate) fn refresh_due_ptr(&mut self, ty_domain: &str) -> HashSet { + let now = current_time_millis(); + + // Check all PTR records for this ty_domain. + self.ptr + .get_mut(ty_domain) + .into_iter() + .flatten() + .filter_map(|record| record.record.updated_refresh_time(now)) + .collect() + } + + /// Returns a tuple of: + /// 1. the map of instance names together with RRType(s) that are due for refresh + /// its SRV or TXT records. + /// 2. the set of new timers that are due for refresh. + pub(crate) fn refresh_due_srv_txt( + &mut self, + ty_domain: &str, + ) -> (HashMap>, HashSet) { + let now = current_time_millis(); + + let instances: Vec<_> = self + .ptr + .get(ty_domain) + .into_iter() + .flatten() + .filter(|record| !record.record.get_record().is_expired(now)) + .filter_map(|record| { + record + .record + .any() + .downcast_ref::() + .map(|ptr| ptr.alias()) + }) + .collect(); + + let mut refresh_due: HashMap> = HashMap::new(); + let mut new_timers = HashSet::new(); + for instance in instances { + // Check SRV records. + let refresh_timers: HashSet = self + .srv + .get_mut(instance) + .into_iter() + .flatten() + .filter_map(|record| record.record.updated_refresh_time(now)) + .collect(); + + if !refresh_timers.is_empty() { + refresh_due + .entry(instance.to_string()) + .and_modify(|v| v.push(RRType::SRV)) + .or_insert(vec![RRType::SRV]); + new_timers.extend(refresh_timers); + } + + // Check TXT records. + let refresh_timers: HashSet = self + .txt + .get_mut(instance) + .into_iter() + .flatten() + .filter_map(|record| record.record.updated_refresh_time(now)) + .collect(); + + if !refresh_timers.is_empty() { + refresh_due + .entry(instance.to_string()) + .and_modify(|v| v.push(RRType::TXT)) + .or_insert(vec![RRType::TXT]); + new_timers.extend(refresh_timers); + } + } + + (refresh_due, new_timers) + } + + /// Returns the set of `host`, where refreshing the A / AAAA records is due + /// for a `ty_domain`. + pub(crate) fn refresh_due_hosts(&mut self, ty_domain: &str) -> (HashSet, HashSet) { + let now = current_time_millis(); + + let instances: Vec<_> = self + .ptr + .get(ty_domain) + .into_iter() + .flatten() + .filter(|record| !record.record.get_record().is_expired(now)) + .filter_map(|record| { + record + .record + .any() + .downcast_ref::() + .map(|ptr| ptr.alias()) + }) + .collect(); + + // Collect hostnames we have browsers for by SRV records. + let mut hostnames_browsed = HashSet::new(); + for instance in instances { + let hosts: HashSet = self + .srv + .get(instance) + .into_iter() + .flatten() + .filter_map(|record| { + record + .record + .any() + .downcast_ref::() + .map(|srv| srv.host().to_string()) + }) + .collect(); + + hostnames_browsed.extend(hosts); + } + let mut refresh_due = HashSet::new(); + let mut new_timers = HashSet::new(); + for hostname in hostnames_browsed { + let refresh_timers: HashSet = self + .addr + .get_mut(&hostname.to_lowercase()) + .into_iter() + .flatten() + .filter_map(|record| record.record.updated_refresh_time(now)) + .collect(); + + if !refresh_timers.is_empty() { + refresh_due.insert(hostname); + new_timers.extend(refresh_timers); + } + } + (refresh_due, new_timers) + } + + /// Returns the set of A/AAAA records that are due for refresh for a `hostname`. + /// + /// For these records, their refresh time will be updated so that they will not refresh again. + pub(crate) fn refresh_due_hostname_resolutions( + &mut self, + hostname: &str, + ) -> HashSet<(String, ScopedIp)> { + let now = current_time_millis(); + + self.addr + .get_mut(hostname) + .into_iter() + .flatten() + .filter_map(|record| { + let rec = record.record.get_record_mut(); + if rec.is_expired(now) || !rec.refresh_due(now) { + return None; + } + rec.refresh_no_more(); + + Some(( + hostname.to_owned(), + record + .record + .any() + .downcast_ref::() + .unwrap() + .address(), + )) + }) + .collect() + } + + /// Returns a list of Known Answer for a given question of `name` with `qtype`. + /// The timestamp `now` is passed in to check TTL. + /// + /// Reference: RFC 6762 section 7.1 + pub(crate) fn get_known_answers<'a>( + &'a self, + name: &str, + qtype: RRType, + now: u64, + ) -> Vec<&'a DnsRecordIntf> { + let records_opt = match qtype { + RRType::PTR => self.get_ptr(name), + RRType::SRV => self.get_srv(name), + RRType::A | RRType::AAAA => self.get_addr(name), + RRType::TXT => self.get_txt(name), + _ => None, + }; + + let records = match records_opt { + Some(items) => items, + None => return Vec::new(), + }; + + // From RFC 6762 section 7.1: + // ..Generally, this applies only to Shared records, not Unique records,.. + // + // ..a Multicast DNS querier SHOULD NOT include + // records in the Known-Answer list whose remaining TTL is less than + // half of their original TTL. + records + .iter() + .filter(move |r| { + !r.record.get_record().is_unique() && !r.record.get_record().halflife_passed(now) + }) + .collect() + } + + /// Removes cached address records on a disabled interface, filtered by IP version. + /// Use `IpType::V4` for A records only, `IpType::V6` for AAAA only, + /// or `IpType::V4 | IpType::V6` for both. + pub(crate) fn remove_addrs_on_disabled_intf( + &mut self, + disabled_if_index: u32, + ip_type: IpType, + ) { + for (host, records) in self.addr.iter_mut() { + records.retain(|record| { + let Some(dns_addr) = record.record.any().downcast_ref::() else { + return false; // invalid address record. + }; + + // Remove the record if it is on this interface and matches the IP version filter. + if dns_addr.interface_id.index == disabled_if_index { + let rr_type = dns_addr.record.entry.ty; + let version_matches = (rr_type == RRType::A && ip_type.contains(IpType::V4)) + || (rr_type == RRType::AAAA && ip_type.contains(IpType::V6)); + if version_matches { + debug!( + "removing ADDR on disabled intf: {:?} host {host}", + dns_addr.interface_id.name + ); + return false; + } + } + true + }); + } + } + + /// Removes all records that were received on `intf_id`. + /// Returns a tuple of: + /// 1. a map of fully removed instances per ty_domain (PTR gone). + /// 2. a set of modified instances that lost records but still have PTR entries. + pub(crate) fn remove_records_on_intf(&mut self, intf_id: InterfaceId) -> IntfRemovalResult { + let mut removed_instances = HashMap::new(); + let mut modified_instances = HashSet::new(); + + self.ptr.iter_mut().for_each(|(name, records)| { + let mut instances_on_intf: HashSet = HashSet::new(); + + // Remove PTR records on `intf_id` and collect their instance names. + records.retain(|r| { + if r.src_intf == intf_id { + if let Some(dns_ptr) = r.record.any().downcast_ref::() { + trace!("removing PTR on intf {:?}: {:?}", intf_id, dns_ptr); + instances_on_intf.insert(dns_ptr.alias().to_string()); + } + false + } else { + true + } + }); + + for instance in instances_on_intf { + // if no more record for this instance on any intf, we shall fully remove it. + if !records.iter().any(|r| { + if let Some(dns_ptr) = r.record.any().downcast_ref::() { + dns_ptr.alias() == instance + } else { + false + } + }) { + removed_instances + .entry(name.to_string()) + .or_insert_with(HashSet::new) + .insert(instance); + } + } + }); + + // Remove any PTR entry that no longer has records. + self.ptr.retain(|_, records| !records.is_empty()); + + // Clean up SRV and TXT records for fully removed instances. + let all_removed: HashSet<&String> = removed_instances.values().flatten().collect(); + self.srv + .retain(|instance, _| !all_removed.contains(instance)); + self.txt + .retain(|instance, _| !all_removed.contains(instance)); + + // Filter remaining SRV/TXT by intf_id + self.srv.iter_mut().for_each(|(instance, records)| { + let before = records.len(); + records.retain(|r| r.src_intf != intf_id); + if records.len() != before { + modified_instances.insert(instance.clone()); + } + }); + self.srv.retain(|_, records| !records.is_empty()); + + self.txt.iter_mut().for_each(|(instance, records)| { + let before = records.len(); + records.retain(|r| r.src_intf != intf_id); + if records.len() != before { + modified_instances.insert(instance.clone()); + } + }); + self.txt.retain(|_, records| !records.is_empty()); + + // For ADDR records, track which hostnames lost records. + let mut affected_hosts = HashSet::new(); + self.addr.iter_mut().for_each(|(host, records)| { + let before = records.len(); + records.retain(|r| r.src_intf != intf_id); + if records.len() != before { + // These hosts are in lower case. + affected_hosts.insert(host.clone()); + } + }); + self.addr.retain(|_, records| !records.is_empty()); + + // Map affected hosts back to instances via SRV hostname lookup. + if !affected_hosts.is_empty() { + for (instance, srv_records) in self.srv.iter() { + for srv in srv_records { + if let Some(dns_srv) = srv.record.any().downcast_ref::() { + if affected_hosts.contains(&dns_srv.host().to_lowercase()) + && !modified_instances.contains(instance) + { + modified_instances.insert(instance.clone()); + } + } + } + } + } + + self.nsec.values_mut().for_each(|records| { + records.retain(|r| r.src_intf != intf_id); + }); + self.nsec.retain(|_, records| !records.is_empty()); + + IntfRemovalResult { + removed_instances, + modified_instances, + } + } +} + +/// When a record has the cache flush bit set, we need to update the expire time of +/// existing records of the same type and class per RFC 6762 Section 10.2. +fn apply_cache_flush( + incoming: &DnsRecordBox, + existing_records: &mut [DnsRecordIntf], + timers: &mut Vec, +) { + let now = current_time_millis(); + let class = incoming.get_class(); + let rtype = incoming.get_type(); + + existing_records.iter_mut().for_each(|r| { + // When cache flush is asked, we set expire date to 1 second in the future if: + // - The record has the same rclass + // - The record was created more than 1 second ago. + // - The record expire is more than 1 second away. + // Ref: RFC 6762 Section 10.2 + // + // Note: when the updated record actually expires, it will trigger events properly. + let mut should_flush = false; + + if class == r.record.get_class() + && rtype == r.record.get_type() + && now > r.record.get_created() + 1000 + && r.record.get_expire() > now + 1000 + { + should_flush = true; + + // additional checks for address records. + if rtype == RRType::A || rtype == RRType::AAAA { + if let Some(addr) = r.record.any().downcast_ref::() { + if let Some(addr_b) = incoming.any().downcast_ref::() { + should_flush = addr.interface_id.index == addr_b.interface_id.index; + } + } + } + } + + if should_flush { + trace!("FLUSH one record: {:?}", &r.record); + let new_expire = now + 1000; + r.record.set_expire(new_expire); + + // Add a timer so the run loop will handle this expire. + timers.push(new_expire); + } + }); +} + +#[cfg(test)] +mod tests { + use std::{collections::HashSet, net::IpAddr}; + + use super::*; + use crate::{ + dns_parser::{DnsAddress, DnsPointer, DnsRecordExt, DnsSrv, DnsTxt, RRType, CLASS_IN}, + service_info::MyIntf, + MAX_PKT_DEFAULT, + }; + + fn make_intf(name: &str, index: u32) -> MyIntf { + MyIntf { + name: name.to_string(), + index, + addrs: HashSet::new(), + max_packet_size_v4: MAX_PKT_DEFAULT, + max_packet_size_v6: MAX_PKT_DEFAULT, + } + } + + /// Two interfaces discover the same service instance. All record types are + /// stored per name (ty_domain / instance / hostname) as the map key. For + /// PTR/SRV/TXT, `matches()` does not include interface_id, so a second + /// insert from another interface just resets the TTL — only one Vec entry + /// exists per logical record. For ADDR, `matches()` includes interface_id, + /// so each interface produces a separate Vec entry under the same hostname. + /// + /// Setup: PTR, SRV, TXT come from intf_b. ADDR exists on both intf_a and + /// intf_b (different IPs). Removing intf_a should leave PTR/SRV/TXT intact + /// but remove addr_a, producing a `modified_instances` entry. + #[test] + fn test_modified_instance_when_intf_removed() { + let ty_domain = "_http._tcp.local."; + let instance = "my-svc._http._tcp.local."; + let host = "myhost.local."; + let addr_a: IpAddr = "192.168.1.1".parse().unwrap(); + let addr_b: IpAddr = "192.168.2.1".parse().unwrap(); + + let intf_a = make_intf("en0", 1); + let intf_b = make_intf("en1", 2); + + let mut cache = DnsCache::new(); + let mut timers = Vec::new(); + + macro_rules! add { + ($intf:expr, $record:expr) => { + cache.add_or_update( + &$intf, + "192.0.2.1".parse().unwrap(), + $record.boxed(), + &mut timers, + true, + ) + }; + } + + // PTR, SRV, TXT from intf_b only — these survive when intf_a is removed. + add!( + intf_b, + DnsPointer::new(ty_domain, RRType::PTR, CLASS_IN, 4500, instance.to_string()) + ); + add!( + intf_b, + DnsSrv::new(instance, CLASS_IN, 4500, 0, 0, 80, host.to_string()) + ); + add!(intf_b, DnsTxt::new(instance, CLASS_IN, 4500, vec![])); + + // ADDR: each interface contributes its own address (interface is part of identity). + let intf_a_id = InterfaceId { + name: "en0".to_string(), + index: 1, + }; + let intf_b_id = InterfaceId { + name: "en1".to_string(), + index: 2, + }; + add!( + intf_a, + DnsAddress::new(host, RRType::A, CLASS_IN, 4500, addr_a, intf_a_id.clone()) + ); + add!( + intf_b, + DnsAddress::new(host, RRType::A, CLASS_IN, 4500, addr_b, intf_b_id) + ); + + // Remove interface A. + let result = cache.remove_records_on_intf(intf_a_id); + + // PTR still exists via intf_b — instance is not fully removed. + assert!( + result.removed_instances.is_empty(), + "expected no removed instances, got {:?}", + result.removed_instances + ); + + // addr_a was removed, so the instance is in modified_instances. + assert!( + result.modified_instances.contains(instance), + "expected {instance} in modified_instances, got {:?}", + result.modified_instances + ); + + // Only addr_b remains in the cache. + let addrs = cache.get_addresses_for_host(host); + let all_ips: HashSet = addrs + .values() + .flatten() + .map(|scoped| scoped.to_ip_addr()) + .collect(); + assert_eq!(all_ips, HashSet::from([addr_b])); + } + + /// A response record that is not for us (no querier) and has no existing + /// cache entry must be dropped *without* leaving an empty `Vec` behind in + /// the cache map. Otherwise the cache grows by one entry per distinct name + /// seen on the network, driven by other hosts' unsolicited traffic. + #[test] + fn test_not_for_us_record_leaves_no_empty_entry() { + let ty_domain = "_other._tcp.local."; + let instance = "someone-else._other._tcp.local."; + let host = "otherhost.local."; + let addr: IpAddr = "192.168.1.9".parse().unwrap(); + + let intf = make_intf("en0", 1); + let intf_id = InterfaceId { + name: "en0".to_string(), + index: 1, + }; + + let mut cache = DnsCache::new(); + let mut timers = Vec::new(); + + // Feed PTR / SRV / TXT / ADDR records with `is_for_us = false` and no + // pre-existing entries. Each must be dropped. + macro_rules! add_not_for_us { + ($record:expr) => {{ + let result = cache.add_or_update( + &intf, + "192.0.2.1".parse().unwrap(), + $record.boxed(), + &mut timers, + false, + ); + assert!( + result.is_none(), + "a not-for-us record should be dropped, got {:?}", + result.map(|(r, _)| r.record.get_name().to_string()) + ); + }}; + } + + add_not_for_us!(DnsPointer::new( + ty_domain, + RRType::PTR, + CLASS_IN, + 4500, + instance.to_string() + )); + add_not_for_us!(DnsSrv::new( + instance, + CLASS_IN, + 4500, + 0, + 0, + 80, + host.to_string() + )); + add_not_for_us!(DnsTxt::new(instance, CLASS_IN, 4500, vec![])); + add_not_for_us!(DnsAddress::new( + host, + RRType::A, + CLASS_IN, + 4500, + addr, + intf_id + )); + + // None of these should have created an entry (empty or otherwise). + assert!( + cache.ptr.is_empty(), + "ptr map leaked: {:?}", + cache.ptr.keys() + ); + assert!( + cache.srv.is_empty(), + "srv map leaked: {:?}", + cache.srv.keys() + ); + assert!( + cache.txt.is_empty(), + "txt map leaked: {:?}", + cache.txt.keys() + ); + assert!( + cache.addr.is_empty(), + "addr map leaked: {:?}", + cache.addr.keys() + ); + } + + #[test] + fn cache_admission_is_bounded_per_source_and_globally() { + let intf = make_intf("en0", 1); + let mut cache = DnsCache::new(); + let mut timers = Vec::new(); + + for index in 0..MAX_CACHE_RECORDS_PER_SOURCE { + let source_octet = (index / MAX_CACHE_RECORDS_PER_SOURCE + 1) as u8; + let source_ip = IpAddr::from([192, 0, 2, source_octet]); + let name = format!("service-{index}._bounded._tcp.local."); + let record = DnsTxt::new(&name, CLASS_IN, 4_500, vec![]); + assert!( + cache + .add_or_update(&intf, source_ip, record.boxed(), &mut timers, true) + .is_some(), + "record {} should fit the configured cache budgets", + index + ); + } + assert_eq!(cache.record_count(), MAX_CACHE_RECORDS_PER_SOURCE); + + let per_source_overflow = + DnsTxt::new("overflow._bounded._tcp.local.", CLASS_IN, 4_500, vec![]); + assert!(cache + .add_or_update( + &intf, + IpAddr::from([192, 0, 2, 1]), + per_source_overflow.boxed(), + &mut timers, + true, + ) + .is_none()); + + for index in MAX_CACHE_RECORDS_PER_SOURCE..MAX_CACHE_RECORDS { + let source_octet = (index / MAX_CACHE_RECORDS_PER_SOURCE + 1) as u8; + let source_ip = IpAddr::from([192, 0, 2, source_octet]); + let name = format!("service-{index}._bounded._tcp.local."); + let record = DnsTxt::new(&name, CLASS_IN, 4_500, vec![]); + assert!( + cache + .add_or_update(&intf, source_ip, record.boxed(), &mut timers, true) + .is_some(), + "record {} should fit the configured cache budgets", + index + ); + } + assert_eq!(cache.record_count(), MAX_CACHE_RECORDS); + + let global_overflow = DnsTxt::new( + "global-overflow._bounded._tcp.local.", + CLASS_IN, + 4_500, + vec![], + ); + assert!(cache + .add_or_update( + &intf, + IpAddr::from([198, 51, 100, 1]), + global_overflow.boxed(), + &mut timers, + true, + ) + .is_none()); + + // An exact refresh allocates no record and remains valid at capacity. + let refresh = DnsTxt::new("service-0._bounded._tcp.local.", CLASS_IN, 4_500, vec![]); + let (_, inserted) = cache + .add_or_update( + &intf, + IpAddr::from([192, 0, 2, 1]), + refresh.boxed(), + &mut timers, + true, + ) + .expect("exact refresh should be admitted at capacity"); + assert!(!inserted); + assert_eq!(cache.record_count(), MAX_CACHE_RECORDS); + } +} diff --git a/vendor/mdns-sd/src/dns_parser.rs b/vendor/mdns-sd/src/dns_parser.rs new file mode 100644 index 0000000..2bfc2f2 --- /dev/null +++ b/vendor/mdns-sd/src/dns_parser.rs @@ -0,0 +1,3654 @@ +//! DNS parsing utility. +//! +//! [DnsIncoming] is the logic representation of an incoming DNS packet. +//! [DnsOutgoing] is the logic representation of an outgoing DNS message of one or more packets. +//! [DnsOutPacket] is the encoded one packet for [DnsOutgoing]. + +use std::{ + any::Any, + cmp, + collections::HashMap, + convert::TryInto, + fmt, + hash::Hash, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str, +}; + +use if_addrs::Interface; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "logging")] +use crate::log::{debug, trace}; +use crate::{ + current_time_millis, + error::{e_fmt, Error, Result}, + service_info::{is_unicast_link_local, DnsRegistry, MyIntf, ServiceInfo}, +}; + +/// Represents a network interface identifier defined by the OS. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct InterfaceId { + /// Interface name, e.g. "en0", "wlan0", etc. + pub name: String, + + /// Interface index assigned by the OS, e.g. 1, 2, etc. + pub index: u32, +} + +impl InterfaceId { + /// Returns all IP addresses associated with this interface by querying the OS. + pub fn get_addrs(&self) -> Vec { + if_addrs::get_if_addrs() + .unwrap_or_default() + .into_iter() + .filter(|iface| iface.index == Some(self.index)) + .map(|iface| iface.ip()) + .collect() + } +} + +impl fmt::Display for InterfaceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}('{}')", self.index, self.name) + } +} + +impl From<&Interface> for InterfaceId { + fn from(interface: &Interface) -> Self { + InterfaceId { + name: interface.name.clone(), + index: interface.index.unwrap_or_default(), + } + } +} + +/// An IPv4 address with interface identifiers indicating which interfaces discovered it. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct ScopedIpV4 { + addr: Ipv4Addr, + /// The interfaces this address was discovered on. + interface_ids: Vec, +} + +impl ScopedIpV4 { + /// Creates a new `ScopedIpV4` with a single interface identifier. + pub fn new(addr: Ipv4Addr, interface_id: InterfaceId) -> Self { + Self { + addr, + interface_ids: vec![interface_id], + } + } + + /// Returns the IPv4 address. + pub const fn addr(&self) -> &Ipv4Addr { + &self.addr + } + + /// Returns the interfaces this address was discovered on. + pub fn interface_ids(&self) -> &[InterfaceId] { + &self.interface_ids + } + + /// Adds an interface identifier if not already present. + pub(crate) fn add_interface_id(&mut self, id: InterfaceId) { + if !self.interface_ids.contains(&id) { + self.interface_ids.push(id); + } + } +} + +/// An IPv6 address with scope_id (interface identifier). +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct ScopedIpV6 { + addr: Ipv6Addr, + scope_id: InterfaceId, +} + +impl ScopedIpV6 { + /// Returns the IPv6 address. + pub const fn addr(&self) -> &Ipv6Addr { + &self.addr + } + + /// Returns the scope_id for this IPv6 address. + pub const fn scope_id(&self) -> &InterfaceId { + &self.scope_id + } +} + +/// An IP address, either IPv4 or IPv6, that supports scope_id for IPv6. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum ScopedIp { + V4(ScopedIpV4), + V6(ScopedIpV6), +} + +impl ScopedIp { + pub const fn to_ip_addr(&self) -> IpAddr { + match self { + ScopedIp::V4(v4) => IpAddr::V4(v4.addr), + ScopedIp::V6(v6) => IpAddr::V6(v6.addr), + } + } + + pub const fn is_ipv4(&self) -> bool { + matches!(self, ScopedIp::V4(_)) + } + + pub const fn is_ipv6(&self) -> bool { + matches!(self, ScopedIp::V6(_)) + } + + pub const fn is_loopback(&self) -> bool { + match self { + ScopedIp::V4(v4) => v4.addr.is_loopback(), + ScopedIp::V6(v6) => v6.addr.is_loopback(), + } + } +} + +impl From for ScopedIp { + fn from(ip: IpAddr) -> Self { + match ip { + IpAddr::V4(v4) => ScopedIp::V4(ScopedIpV4 { + addr: v4, + interface_ids: vec![], + }), + IpAddr::V6(v6) => ScopedIp::V6(ScopedIpV6 { + addr: v6, + scope_id: InterfaceId::default(), + }), + } + } +} + +impl From<&Interface> for ScopedIp { + fn from(interface: &Interface) -> Self { + match interface.ip() { + IpAddr::V4(v4) => ScopedIp::V4(ScopedIpV4 { + addr: v4, + interface_ids: vec![InterfaceId::from(interface)], + }), + IpAddr::V6(v6) => ScopedIp::V6(ScopedIpV6 { + addr: v6, + scope_id: InterfaceId::from(interface), + }), + } + } +} + +impl fmt::Display for ScopedIp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScopedIp::V4(v4) => write!(f, "{}", v4.addr), + ScopedIp::V6(v6) => { + if v6.scope_id.index != 0 && is_unicast_link_local(&v6.addr) { + #[cfg(windows)] + { + write!(f, "{}%{}", v6.addr, v6.scope_id.index) + } + #[cfg(not(windows))] + { + write!(f, "{}%{}", v6.addr, v6.scope_id.name) + } + } else { + write!(f, "{}", v6.addr) + } + } + } + } +} + +/// DNS resource record types, stored as `u16`. Can do `as u16` when needed. +/// +/// See [RFC 1035 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2) +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[non_exhaustive] +#[repr(u16)] +pub enum RRType { + /// DNS record type for IPv4 address + A = 1, + + /// DNS record type for Canonical Name + CNAME = 5, + + /// DNS record type for Pointer + PTR = 12, + + /// DNS record type for Host Info + HINFO = 13, + + /// DNS record type for Text (properties) + TXT = 16, + + /// DNS record type for IPv6 address + AAAA = 28, + + /// DNS record type for Service + SRV = 33, + + /// DNS record type for Negative Responses + NSEC = 47, + + /// DNS record type for any records (wildcard) + ANY = 255, +} + +impl RRType { + /// Converts `u16` into `RRType` if possible. + pub const fn from_u16(value: u16) -> Option { + match value { + 1 => Some(RRType::A), + 5 => Some(RRType::CNAME), + 12 => Some(RRType::PTR), + 13 => Some(RRType::HINFO), + 16 => Some(RRType::TXT), + 28 => Some(RRType::AAAA), + 33 => Some(RRType::SRV), + 47 => Some(RRType::NSEC), + 255 => Some(RRType::ANY), + _ => None, + } + } +} + +impl fmt::Display for RRType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RRType::A => write!(f, "TYPE_A"), + RRType::CNAME => write!(f, "TYPE_CNAME"), + RRType::PTR => write!(f, "TYPE_PTR"), + RRType::HINFO => write!(f, "TYPE_HINFO"), + RRType::TXT => write!(f, "TYPE_TXT"), + RRType::AAAA => write!(f, "TYPE_AAAA"), + RRType::SRV => write!(f, "TYPE_SRV"), + RRType::NSEC => write!(f, "TYPE_NSEC"), + RRType::ANY => write!(f, "TYPE_ANY"), + } + } +} + +/// The class value for the Internet. +pub const CLASS_IN: u16 = 1; +pub const CLASS_MASK: u16 = 0x7FFF; + +/// Cache-flush bit: the most significant bit of the rrclass field of the resource record. +pub const CLASS_CACHE_FLUSH: u16 = 0x8000; + +/// Absolute max size of UDP datagram payload for an mDNS packet over IPv4. +/// +/// RFC 6762 section 17: +/// "Even when fragmentation is used, a Multicast DNS packet, including IP and UDP +/// headers, MUST NOT exceed 9000 bytes." +/// +/// It is calculated as: 9000 bytes - IPv4 header 20 bytes - UDP header 8 bytes. +pub(crate) const MAX_PKT_ABSOLUTE_IPV4: usize = 8972; + +/// Absolute max size of UDP datagram payload for an mDNS packet over IPv6. +/// +/// Same 9000-byte ceiling as [`MAX_PKT_ABSOLUTE_IPV4`], less the bigger IPv6 header: +/// 9000 bytes - IPv6 header 40 bytes - UDP header 8 bytes. +pub(crate) const MAX_PKT_ABSOLUTE_IPV6: usize = 8952; + +/// Absolute max size of an mDNS packet for the given IP version. +pub(crate) const fn max_pkt_absolute(is_ipv4: bool) -> usize { + if is_ipv4 { + MAX_PKT_ABSOLUTE_IPV4 + } else { + MAX_PKT_ABSOLUTE_IPV6 + } +} + +/// Default max size of a generated (i.e. outgoing) packet. +/// +/// Calculated as: 1500 bytes Ethernet MTU - IPv6 header 40 bytes - UDP header 8 bytes. +/// It is safe on both IPv4 and IPv6, at the cost of 20 unused bytes for IPv4. +/// +/// The idea is to keep generated packets unfragmented at IP layer. See RFC 6762 section 17. +pub const MAX_PKT_DEFAULT: usize = 1452; + +const MSG_HEADER_LEN: usize = 12; + +/// Max size of a single DNS label, in bytes. +/// +/// Reference: [RFC1035 section 2.3.4](https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4) +const MAX_LABEL_BYTES: usize = 63; + +/// Max size of a whole domain name, in bytes. +/// +/// Reference: [RFC1035 section 2.3.4](https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4) +const MAX_NAME_BYTES: usize = 255; + +/// Why a question or a record could not be written into a packet. +/// +/// In either case nothing is left behind in the packet: the caller rolls back +/// whatever was written and skips the item. +#[derive(Debug, PartialEq, Eq)] +pub enum WriteError { + /// A label in a name is longer than [`MAX_LABEL_BYTES`]. + NameTooLong, + + /// The packet would exceed its max size with this record. + PacketFull, +} + +/// `crate::error::Result` shadows the std alias here, hence the full path. +type WriteResult = core::result::Result<(), WriteError>; + +// Definitions for DNS message header "flags" field +// +// The "flags" field is 16-bit long, in this format: +// (RFC 1035 section 4.1.1) +// +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +// +pub const FLAGS_QR_MASK: u16 = 0x8000; // mask for query/response bit + +/// Flag bit to indicate a query +pub const FLAGS_QR_QUERY: u16 = 0x0000; + +/// Flag bit to indicate a response +pub const FLAGS_QR_RESPONSE: u16 = 0x8000; + +/// Flag bit for Authoritative Answer +pub const FLAGS_AA: u16 = 0x0400; + +/// mask for TC(Truncated) bit +/// +/// 2024-08-10: currently this flag is only supported on the querier side, +/// not supported on the responder side. I.e. the responder only +/// handles the first packet and ignore this bit. Since the +/// additional packets have 0 questions, the processing of them +/// is no-op. +/// In practice, this means the responder supports Known-Answer +/// only with single packet, not multi-packet. The querier supports +/// both single packet and multi-packet. +pub const FLAGS_TC: u16 = 0x0200; + +/// A convenience type alias for DNS record trait objects. +pub type DnsRecordBox = Box; + +impl Clone for DnsRecordBox { + fn clone(&self) -> Self { + self.clone_box() + } +} + +const U16_SIZE: usize = 2; + +/// Returns `RRType` for a given IP address. +#[inline] +pub const fn ip_address_rr_type(address: &IpAddr) -> RRType { + match address { + IpAddr::V4(_) => RRType::A, + IpAddr::V6(_) => RRType::AAAA, + } +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct DnsEntry { + pub(crate) name: String, // always lower case. + pub(crate) ty: RRType, + class: u16, + cache_flush: bool, +} + +impl DnsEntry { + const fn new(name: String, ty: RRType, class: u16) -> Self { + Self { + name, + ty, + class: class & CLASS_MASK, + cache_flush: (class & CLASS_CACHE_FLUSH) != 0, + } + } +} + +/// Common methods for all DNS entries: questions and resource records. +pub trait DnsEntryExt: fmt::Debug { + fn entry_name(&self) -> &str; + + fn entry_type(&self) -> RRType; +} + +/// A DNS question entry +#[derive(Debug)] +pub struct DnsQuestion { + pub(crate) entry: DnsEntry, +} + +impl DnsEntryExt for DnsQuestion { + fn entry_name(&self) -> &str { + &self.entry.name + } + + fn entry_type(&self) -> RRType { + self.entry.ty + } +} + +/// A DNS Resource Record - like a DNS entry, but has a TTL. +/// RFC: https://www.rfc-editor.org/rfc/rfc1035#section-3.2.1 +/// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.3 +#[derive(Debug, Clone)] +pub struct DnsRecord { + pub(crate) entry: DnsEntry, + ttl: u32, // in seconds, 0 means this record should not be cached + created: u64, // UNIX time in millis + expires: u64, // expires at this UNIX time in millis + + /// Support re-query an instance before its PTR record expires. + /// See https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 + refresh: u64, // UNIX time in millis + + /// If conflict resolution decides to change the name, this is the new one. + new_name: Option, +} + +impl DnsRecord { + fn new(name: &str, ty: RRType, class: u16, ttl: u32) -> Self { + let created = current_time_millis(); + + // From RFC 6762 section 5.2: + // "... The querier should plan to issue a query at 80% of the record + // lifetime, and then if no answer is received, at 85%, 90%, and 95%." + let refresh = get_expiration_time(created, ttl, 80); + + let expires = get_expiration_time(created, ttl, 100); + + Self { + entry: DnsEntry::new(name.to_string(), ty, class), + ttl, + created, + expires, + refresh, + new_name: None, + } + } + + pub const fn get_ttl(&self) -> u32 { + self.ttl + } + + pub const fn get_expire_time(&self) -> u64 { + self.expires + } + + pub const fn get_refresh_time(&self) -> u64 { + self.refresh + } + + pub const fn is_expired(&self, now: u64) -> bool { + now >= self.expires + } + + /// Returns whether record expires in 1 second. + /// + /// This is useful because mDNS sets TTL to 1 (not 0) for expiring records. + pub const fn expires_soon(&self, now: u64) -> bool { + now + 1000 >= self.expires + } + + pub const fn refresh_due(&self, now: u64) -> bool { + now >= self.refresh + } + + /// Returns whether `now` (in millis) has passed half of TTL. + pub fn halflife_passed(&self, now: u64) -> bool { + let halflife = get_expiration_time(self.created, self.ttl, 50); + now > halflife + } + + pub fn is_unique(&self) -> bool { + self.entry.cache_flush + } + + /// Updates the refresh time to be the same as the expire time so that + /// this record will not refresh again and will just expire. + pub fn refresh_no_more(&mut self) { + self.refresh = get_expiration_time(self.created, self.ttl, 100); + } + + /// Returns if this record is due for refresh. If yes, `refresh` time is updated. + pub fn refresh_maybe(&mut self, now: u64) -> bool { + if self.is_expired(now) || !self.refresh_due(now) { + return false; + } + + trace!( + "{} qtype {} is due to refresh", + &self.entry.name, + self.entry.ty + ); + + // From RFC 6762 section 5.2: + // "... The querier should plan to issue a query at 80% of the record + // lifetime, and then if no answer is received, at 85%, 90%, and 95%." + // + // If the answer is received in time, 'refresh' will be reset outside + // this function, back to 80% of the new TTL. + if self.refresh == get_expiration_time(self.created, self.ttl, 80) { + self.refresh = get_expiration_time(self.created, self.ttl, 85); + } else if self.refresh == get_expiration_time(self.created, self.ttl, 85) { + self.refresh = get_expiration_time(self.created, self.ttl, 90); + } else if self.refresh == get_expiration_time(self.created, self.ttl, 90) { + self.refresh = get_expiration_time(self.created, self.ttl, 95); + } else { + self.refresh_no_more(); + } + + true + } + + /// Returns the remaining TTL in seconds + fn get_remaining_ttl(&self, now: u64) -> u32 { + let remaining_millis = get_expiration_time(self.created, self.ttl, 100) - now; + cmp::max(0, remaining_millis / 1000) as u32 + } + + /// Return the absolute time for this record being created + pub const fn get_created(&self) -> u64 { + self.created + } + + /// Set the absolute expiration time in millis + fn set_expire(&mut self, expire_at: u64) { + self.expires = expire_at; + } + + fn reset_ttl(&mut self, other: &Self) { + self.ttl = other.ttl; + self.created = other.created; + self.expires = get_expiration_time(self.created, self.ttl, 100); + self.refresh = if self.ttl > 1 { + get_expiration_time(self.created, self.ttl, 80) + } else { + // If TTL is 1, it means this record is expiring, + // then we set refresh to the same time as expires. + self.expires + }; + } + + /// Modify TTL to reflect the remaining life time from `now`. + pub fn update_ttl(&mut self, now: u64) { + if now > self.created { + let elapsed = now - self.created; + self.ttl -= (elapsed / 1000) as u32; + } + } + + pub fn set_new_name(&mut self, new_name: String) { + if new_name == self.entry.name { + self.new_name = None; + } else { + self.new_name = Some(new_name); + } + } + + pub fn get_new_name(&self) -> Option<&str> { + self.new_name.as_deref() + } + + /// Return the new name if exists, otherwise the regular name in DnsEntry. + pub(crate) fn get_name(&self) -> &str { + self.new_name.as_deref().unwrap_or(&self.entry.name) + } + + pub fn get_original_name(&self) -> &str { + &self.entry.name + } +} + +impl PartialEq for DnsRecord { + fn eq(&self, other: &Self) -> bool { + self.entry == other.entry + } +} + +/// Common methods for DNS resource records. +pub trait DnsRecordExt: fmt::Debug { + fn get_record(&self) -> &DnsRecord; + fn get_record_mut(&mut self) -> &mut DnsRecord; + /// Writes the rdata of this record into `packet`. + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult; + fn any(&self) -> &dyn Any; + + /// Returns whether `other` record is considered the same except TTL. + fn matches(&self, other: &dyn DnsRecordExt) -> bool; + + /// Returns whether `other` record has the same rdata. + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool; + + /// Returns the result based on a byte-level comparison of `rdata`. + /// If `other` is not valid, returns `Greater`. + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering; + + /// Returns the result based on "lexicographically later" defined below. + fn compare(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + /* + RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 + + ... The determination of "lexicographically later" is performed by first + comparing the record class (excluding the cache-flush bit described + in Section 10.2), then the record type, then raw comparison of the + binary content of the rdata without regard for meaning or structure. + If the record classes differ, then the numerically greater class is + considered "lexicographically later". Otherwise, if the record types + differ, then the numerically greater type is considered + "lexicographically later". If the rrtype and rrclass both match, + then the rdata is compared. ... + */ + match self.get_class().cmp(&other.get_class()) { + cmp::Ordering::Equal => match self.get_type().cmp(&other.get_type()) { + cmp::Ordering::Equal => self.compare_rdata(other), + not_equal => not_equal, + }, + not_equal => not_equal, + } + } + + /// Returns a human-readable string of rdata. + fn rdata_print(&self) -> String; + + /// Returns the class only, excluding class_flush / unique bit. + fn get_class(&self) -> u16 { + self.get_record().entry.class + } + + fn get_cache_flush(&self) -> bool { + self.get_record().entry.cache_flush + } + + /// Return the new name if exists, otherwise the regular name in DnsEntry. + fn get_name(&self) -> &str { + self.get_record().get_name() + } + + fn get_type(&self) -> RRType { + self.get_record().entry.ty + } + + /// Resets TTL using `other` record. + /// `self.refresh` and `self.expires` are also reset. + fn reset_ttl(&mut self, other: &dyn DnsRecordExt) { + self.get_record_mut().reset_ttl(other.get_record()); + } + + fn get_created(&self) -> u64 { + self.get_record().get_created() + } + + fn get_expire(&self) -> u64 { + self.get_record().get_expire_time() + } + + fn set_expire(&mut self, expire_at: u64) { + self.get_record_mut().set_expire(expire_at); + } + + /// Set expire as `expire_at` if it is sooner than the current `expire`. + fn set_expire_sooner(&mut self, expire_at: u64) { + if expire_at < self.get_expire() { + self.get_record_mut().set_expire(expire_at); + } + } + + /// Returns true if the record expires in 1 second from `now`. + fn expires_soon(&self, now: u64) -> bool { + self.get_record().expires_soon(now) + } + + /// Given `now`, if the record is due to refresh, this method updates the refresh time + /// and returns the new refresh time. Otherwise, returns None. + fn updated_refresh_time(&mut self, now: u64) -> Option { + if self.get_record_mut().refresh_maybe(now) { + Some(self.get_record().get_refresh_time()) + } else { + None + } + } + + /// Returns true if another record has matched content, + /// and if its TTL is at least half of this record's. + fn suppressed_by_answer(&self, other: &dyn DnsRecordExt) -> bool { + self.matches(other) && (other.get_record().ttl > self.get_record().ttl / 2) + } + + /// Required by RFC 6762 Section 7.1: Known-Answer Suppression. + fn suppressed_by(&self, msg: &DnsIncoming) -> bool { + for answer in msg.answers.iter() { + if self.suppressed_by_answer(answer.as_ref()) { + return true; + } + } + false + } + + fn clone_box(&self) -> DnsRecordBox; + + fn boxed(self) -> DnsRecordBox; +} + +/// Resource Record for IPv4 address or IPv6 address. +#[derive(Debug, Clone)] +pub(crate) struct DnsAddress { + pub(crate) record: DnsRecord, + address: IpAddr, + pub(crate) interface_id: InterfaceId, +} + +impl DnsAddress { + pub fn new( + name: &str, + ty: RRType, + class: u16, + ttl: u32, + address: IpAddr, + interface_id: InterfaceId, + ) -> Self { + let record = DnsRecord::new(name, ty, class, ttl); + Self { + record, + address, + interface_id, + } + } + + pub fn address(&self) -> ScopedIp { + match self.address { + IpAddr::V4(v4) => ScopedIp::V4(ScopedIpV4 { + addr: v4, + interface_ids: vec![self.interface_id.clone()], + }), + IpAddr::V6(v6) => ScopedIp::V6(ScopedIpV6 { + addr: v6, + scope_id: self.interface_id.clone(), + }), + } + } +} + +impl DnsRecordExt for DnsAddress { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + match self.address { + IpAddr::V4(addr) => packet.write_bytes(addr.octets().as_ref()), + IpAddr::V6(addr) => packet.write_bytes(addr.octets().as_ref()), + }; + Ok(()) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_a) = other.any().downcast_ref::() { + return self.address == other_a.address + && self.record.entry == other_a.record.entry + && self.interface_id == other_a.interface_id; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_a) = other.any().downcast_ref::() { + return self.address == other_a.address; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + if let Some(other_a) = other.any().downcast_ref::() { + self.address.cmp(&other_a.address) + } else { + cmp::Ordering::Greater + } + } + + fn rdata_print(&self) -> String { + format!("{}", self.address) + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +/// Resource Record for a DNS pointer +#[derive(Debug, Clone)] +pub struct DnsPointer { + record: DnsRecord, + alias: String, // the full name of Service Instance +} + +impl DnsPointer { + pub fn new(name: &str, ty: RRType, class: u16, ttl: u32, alias: String) -> Self { + let record = DnsRecord::new(name, ty, class, ttl); + Self { record, alias } + } + + pub fn alias(&self) -> &str { + &self.alias + } +} + +impl DnsRecordExt for DnsPointer { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + packet.write_name(&self.alias) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_ptr) = other.any().downcast_ref::() { + return self.alias == other_ptr.alias && self.record.entry == other_ptr.record.entry; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_ptr) = other.any().downcast_ref::() { + return self.alias == other_ptr.alias; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + if let Some(other_ptr) = other.any().downcast_ref::() { + self.alias.cmp(&other_ptr.alias) + } else { + cmp::Ordering::Greater + } + } + + fn rdata_print(&self) -> String { + self.alias.clone() + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +/// Resource Record for a DNS service. +#[derive(Debug, Clone)] +pub struct DnsSrv { + pub(crate) record: DnsRecord, + pub(crate) priority: u16, // lower number means higher priority. Should be 0 in common cases. + pub(crate) weight: u16, // Should be 0 in common cases + host: String, + port: u16, +} + +impl DnsSrv { + pub fn new( + name: &str, + class: u16, + ttl: u32, + priority: u16, + weight: u16, + port: u16, + host: String, + ) -> Self { + let record = DnsRecord::new(name, RRType::SRV, class, ttl); + Self { + record, + priority, + weight, + host, + port, + } + } + + pub fn host(&self) -> &str { + &self.host + } + + pub fn port(&self) -> u16 { + self.port + } + + pub fn set_host(&mut self, host: String) { + self.host = host; + } +} + +impl DnsRecordExt for DnsSrv { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + packet.write_short(self.priority); + packet.write_short(self.weight); + packet.write_short(self.port); + packet.write_name(&self.host) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_svc) = other.any().downcast_ref::() { + return self.host == other_svc.host + && self.port == other_svc.port + && self.weight == other_svc.weight + && self.priority == other_svc.priority + && self.record.entry == other_svc.record.entry; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_srv) = other.any().downcast_ref::() { + return self.host == other_srv.host + && self.port == other_srv.port + && self.weight == other_srv.weight + && self.priority == other_srv.priority; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + let Some(other_srv) = other.any().downcast_ref::() else { + return cmp::Ordering::Greater; + }; + + // 1. compare `priority` + match self + .priority + .to_be_bytes() + .cmp(&other_srv.priority.to_be_bytes()) + { + cmp::Ordering::Equal => { + // 2. compare `weight` + match self + .weight + .to_be_bytes() + .cmp(&other_srv.weight.to_be_bytes()) + { + cmp::Ordering::Equal => { + // 3. compare `port`. + match self.port.to_be_bytes().cmp(&other_srv.port.to_be_bytes()) { + cmp::Ordering::Equal => self.host.cmp(&other_srv.host), + not_equal => not_equal, + } + } + not_equal => not_equal, + } + } + not_equal => not_equal, + } + } + + fn rdata_print(&self) -> String { + format!( + "priority: {}, weight: {}, port: {}, host: {}", + self.priority, self.weight, self.port, self.host + ) + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +/// Resource Record for a DNS TXT record. +/// +/// From [RFC 6763 section 6]: +/// +/// The format of each constituent string within the DNS TXT record is a +/// single length byte, followed by 0-255 bytes of text data. +/// +/// DNS-SD uses DNS TXT records to store arbitrary key/value pairs +/// conveying additional information about the named service. Each +/// key/value pair is encoded as its own constituent string within the +/// DNS TXT record, in the form "key=value" (without the quotation +/// marks). Everything up to the first '=' character is the key (Section +/// 6.4). Everything after the first '=' character to the end of the +/// string (including subsequent '=' characters, if any) is the value +#[derive(Clone)] +pub struct DnsTxt { + pub(crate) record: DnsRecord, + text: Vec, +} + +impl DnsTxt { + pub fn new(name: &str, class: u16, ttl: u32, text: Vec) -> Self { + let record = DnsRecord::new(name, RRType::TXT, class, ttl); + Self { record, text } + } + + pub fn text(&self) -> &[u8] { + &self.text + } +} + +impl DnsRecordExt for DnsTxt { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + packet.write_bytes(&self.text); + Ok(()) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_txt) = other.any().downcast_ref::() { + return self.text == other_txt.text && self.record.entry == other_txt.record.entry; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_txt) = other.any().downcast_ref::() { + return self.text == other_txt.text; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + if let Some(other_txt) = other.any().downcast_ref::() { + self.text.cmp(&other_txt.text) + } else { + cmp::Ordering::Greater + } + } + + fn rdata_print(&self) -> String { + format!("{:?}", decode_txt(&self.text)) + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +impl fmt::Debug for DnsTxt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let properties = decode_txt(&self.text); + write!( + f, + "DnsTxt {{ record: {:?}, text: {:?} }}", + self.record, properties + ) + } +} + +// Convert from DNS TXT record content to key/value pairs +fn decode_txt(txt: &[u8]) -> Vec { + let mut properties = Vec::new(); + let mut offset = 0; + while offset < txt.len() { + let length = txt[offset] as usize; + if length == 0 { + break; // reached the end + } + offset += 1; // move over the length byte + + let offset_end = offset + length; + if offset_end > txt.len() { + trace!("ERROR: DNS TXT: size given for property is out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len()); + break; // Skipping the rest of the record content, as the size for this property would already be out of range. + } + let kv_bytes = &txt[offset..offset_end]; + + // split key and val using the first `=` + let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else( + || (kv_bytes.to_vec(), None), + |idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())), + ); + + // Make sure the key can be stored in UTF-8. + match String::from_utf8(k) { + Ok(k_string) => { + properties.push(TxtProperty { + key: k_string, + val: v, + }); + } + Err(e) => trace!("ERROR: convert to String from key: {}", e), + } + + offset += length; + } + + properties +} + +/// Represents a property in a TXT record. +#[derive(Clone, PartialEq, Eq)] +pub struct TxtProperty { + /// The name of the property. The original cases are kept. + key: String, + + /// RFC 6763 says values are bytes, not necessarily UTF-8. + /// It is also possible that there is no value, in which case + /// the key is a boolean key. + val: Option>, +} + +impl TxtProperty { + /// Returns the value of a property as str. + pub fn val_str(&self) -> &str { + self.val + .as_ref() + .map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default()) + } +} + +/// Supports constructing from a tuple. +impl From<&(K, V)> for TxtProperty +where + K: ToString, + V: ToString, +{ + fn from(prop: &(K, V)) -> Self { + Self { + key: prop.0.to_string(), + val: Some(prop.1.to_string().into_bytes()), + } + } +} + +impl From<(K, V)> for TxtProperty +where + K: ToString, + V: AsRef<[u8]>, +{ + fn from(prop: (K, V)) -> Self { + Self { + key: prop.0.to_string(), + val: Some(prop.1.as_ref().into()), + } + } +} + +/// Support a property that has no value. +impl From<&str> for TxtProperty { + fn from(key: &str) -> Self { + Self { + key: key.to_string(), + val: None, + } + } +} + +impl fmt::Display for TxtProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}={}", self.key, self.val_str()) + } +} + +/// Mimic the default debug output for a struct, with a twist: +/// - If self.var is UTF-8, will output it as a string in double quotes. +/// - If self.var is not UTF-8, will output its bytes as in hex. +impl fmt::Debug for TxtProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val_string = self.val.as_ref().map_or_else( + || "None".to_string(), + |v| { + std::str::from_utf8(&v[..]).map_or_else( + |_| format!("Some({})", u8_slice_to_hex(&v[..])), + |s| format!("Some(\"{s}\")"), + ) + }, + ); + + write!( + f, + "TxtProperty {{key: \"{}\", val: {}}}", + &self.key, &val_string, + ) + } +} + +const HEX_TABLE: [char; 16] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', +]; + +/// Create a hex string from `slice`, with a "0x" prefix. +/// +/// For example, [1u8, 2u8] -> "0x0102" +fn u8_slice_to_hex(slice: &[u8]) -> String { + let mut hex = String::with_capacity(slice.len() * 2 + 2); + hex.push_str("0x"); + for b in slice { + hex.push(HEX_TABLE[(b >> 4) as usize]); + hex.push(HEX_TABLE[(b & 0x0F) as usize]); + } + hex +} + +/// A DNS host information record +#[derive(Debug, Clone)] +struct DnsHostInfo { + record: DnsRecord, + cpu: String, + os: String, +} + +impl DnsHostInfo { + fn new(name: &str, ty: RRType, class: u16, ttl: u32, cpu: String, os: String) -> Self { + let record = DnsRecord::new(name, ty, class, ttl); + Self { record, cpu, os } + } +} + +impl DnsRecordExt for DnsHostInfo { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + debug!("Writing HInfo: cpu {} os {}", &self.cpu, &self.os); + packet.write_bytes(self.cpu.as_bytes()); + packet.write_bytes(self.os.as_bytes()); + Ok(()) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_hinfo) = other.any().downcast_ref::() { + return self.cpu == other_hinfo.cpu + && self.os == other_hinfo.os + && self.record.entry == other_hinfo.record.entry; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_hinfo) = other.any().downcast_ref::() { + return self.cpu == other_hinfo.cpu && self.os == other_hinfo.os; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + if let Some(other_hinfo) = other.any().downcast_ref::() { + match self.cpu.cmp(&other_hinfo.cpu) { + cmp::Ordering::Equal => self.os.cmp(&other_hinfo.os), + ordering => ordering, + } + } else { + cmp::Ordering::Greater + } + } + + fn rdata_print(&self) -> String { + format!("cpu: {}, os: {}", self.cpu, self.os) + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +/// Resource Record for negative responses +/// +/// [RFC4034 section 4.1](https://datatracker.ietf.org/doc/html/rfc4034#section-4.1) +/// and +/// [RFC6762 section 6.1](https://datatracker.ietf.org/doc/html/rfc6762#section-6.1) +#[derive(Debug, Clone)] +pub struct DnsNSec { + record: DnsRecord, + next_domain: String, + type_bitmap: Vec, +} + +impl DnsNSec { + pub fn new( + name: &str, + class: u16, + ttl: u32, + next_domain: String, + type_bitmap: Vec, + ) -> Self { + let record = DnsRecord::new(name, RRType::NSEC, class, ttl); + Self { + record, + next_domain, + type_bitmap, + } + } + + /// Returns the types marked by `type_bitmap` + pub fn _types(&self) -> Vec { + // From RFC 4034: 4.1.2 The Type Bit Maps Field + // https://datatracker.ietf.org/doc/html/rfc4034#section-4.1.2 + // + // Each bitmap encodes the low-order 8 bits of RR types within the + // window block, in network bit order. The first bit is bit 0. For + // window block 0, bit 1 corresponds to RR type 1 (A), bit 2 corresponds + // to RR type 2 (NS), and so forth. + + let mut bit_num = 0; + let mut results = Vec::new(); + + for byte in self.type_bitmap.iter() { + let mut bit_mask: u8 = 0x80; // for bit 0 in network bit order + + // check every bit in this byte, one by one. + for _ in 0..8 { + if (byte & bit_mask) != 0 { + results.push(bit_num); + } + bit_num += 1; + bit_mask >>= 1; // mask for the next bit + } + } + results + } +} + +impl DnsRecordExt for DnsNSec { + fn get_record(&self) -> &DnsRecord { + &self.record + } + + fn get_record_mut(&mut self) -> &mut DnsRecord { + &mut self.record + } + + fn write(&self, packet: &mut DnsOutPacket) -> WriteResult { + packet.write_bytes(self.next_domain.as_bytes()); + packet.write_bytes(&self.type_bitmap); + Ok(()) + } + + fn any(&self) -> &dyn Any { + self + } + + fn matches(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_record) = other.any().downcast_ref::() { + return self.next_domain == other_record.next_domain + && self.type_bitmap == other_record.type_bitmap + && self.record.entry == other_record.record.entry; + } + false + } + + fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { + if let Some(other_record) = other.any().downcast_ref::() { + return self.next_domain == other_record.next_domain + && self.type_bitmap == other_record.type_bitmap; + } + false + } + + fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { + if let Some(other_nsec) = other.any().downcast_ref::() { + match self.next_domain.cmp(&other_nsec.next_domain) { + cmp::Ordering::Equal => self.type_bitmap.cmp(&other_nsec.type_bitmap), + ordering => ordering, + } + } else { + cmp::Ordering::Greater + } + } + + fn rdata_print(&self) -> String { + format!( + "next_domain: {}, type_bitmap len: {}", + self.next_domain, + self.type_bitmap.len() + ) + } + + fn clone_box(&self) -> DnsRecordBox { + Box::new(self.clone()) + } + + fn boxed(self) -> DnsRecordBox { + Box::new(self) + } +} + +/// Which section of a DNS message an item belongs to. +#[derive(Clone, Copy, Debug)] +enum Section { + Question, + Answer, + Authority, + Additional, +} + +/// A single packet for outgoing DNS message. +pub struct DnsOutPacket { + /// All bytes in `data` is the actual packet on the wire. + data: Vec, + + /// k: name, v: offset + names: HashMap, + + /// Max byte size of `data`. i.e. the max packet size. + max_size: usize, + + /// How many items `data` holds in each section, i.e. the header counts. + question_count: u16, + answer_count: u16, + auth_count: u16, + addi_count: u16, +} + +impl DnsOutPacket { + fn new(max_size: usize) -> Self { + Self { + data: vec![0; MSG_HEADER_LEN], + names: HashMap::new(), + max_size, + question_count: 0, + answer_count: 0, + auth_count: 0, + addi_count: 0, + } + } + + pub fn size(&self) -> usize { + self.data.len() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + /// True if nothing has been written into this packet yet. + fn is_empty(&self) -> bool { + self.question_count == 0 + && self.answer_count == 0 + && self.auth_count == 0 + && self.addi_count == 0 + } + + /// Counts one more item in `section`. + fn bump(&mut self, section: Section) { + match section { + Section::Question => self.question_count += 1, + Section::Answer => self.answer_count += 1, + Section::Authority => self.auth_count += 1, + Section::Additional => self.addi_count += 1, + } + } + + fn write_question(&mut self, question: &DnsQuestion) -> WriteResult { + let start_size = self.size(); + + self.write_name(&question.entry.name).map_err(|e| { + self.rollback(start_size); + e + })?; + self.write_short(question.entry.ty as u16); + self.write_short(question.entry.class); + + if self.size() > self.max_size { + self.rollback(start_size); + return Err(WriteError::PacketFull); + } + + Ok(()) + } + + /// Discards everything written since `start_size`, including the name + /// compression offsets that point into the discarded bytes. + fn rollback(&mut self, start_size: usize) { + self.data.truncate(start_size); + self.names + .retain(|_, offset| (*offset as usize) < start_size); + } + + /// Writes a record (answer, authoritative answer, additional). + /// + /// In error cases nothing is written to the packet. + fn write_record(&mut self, record_ext: &dyn DnsRecordExt, now: u64) -> WriteResult { + let start_size = self.size(); + + let record = record_ext.get_record(); + self.write_name(record.get_name())?; + self.write_short(record.entry.ty as u16); + if record.entry.cache_flush { + // check "multicast" + self.write_short(record.entry.class | CLASS_CACHE_FLUSH); + } else { + self.write_short(record.entry.class); + } + + if now == 0 { + self.write_u32(record.ttl); + } else { + self.write_u32(record.get_remaining_ttl(now)); + } + + // Placeholder for record size + self.write_short(0); + let record_offset = self.size(); + + if let Err(e) = record_ext.write(self) { + self.rollback(start_size); + return Err(e); + } + + self.set_short_at(record_offset - 2, (self.size() - record_offset) as u16); + + if self.size() > self.max_size { + self.rollback(start_size); + return Err(WriteError::PacketFull); + } + + Ok(()) + } + + fn set_short_at(&mut self, index: usize, value: u16) { + self.data[index..index + 2].copy_from_slice(&value.to_be_bytes()); + } + + /// Parses a DNS name that may contain escaped characters according to RFC 6763 Section 4.3. + /// Returns a vector of labels where each label is the unescaped content. + /// + /// Escape sequences: + /// - \\. becomes . (literal dot) + /// - \\\\ becomes \\ (literal backslash) + fn parse_escaped_name(name: &str) -> Vec { + let mut labels = Vec::new(); + let mut current_label = String::new(); + let mut chars = name.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // Backslash escape sequence + if let Some(&next_ch) = chars.peek() { + match next_ch { + '.' | '\\' => { + // \\. or \\\\ - consume the backslash and add the escaped char + chars.next(); + current_label.push(next_ch); + } + _ => { + // Not a recognized escape - treat backslash literally + current_label.push(ch); + } + } + } else { + // Trailing backslash - add it literally + current_label.push(ch); + } + } + '.' => { + // Unescaped dot - label separator + if !current_label.is_empty() { + labels.push(current_label.clone()); + current_label.clear(); + } + } + _ => { + current_label.push(ch); + } + } + } + + // Add the last label if not empty + if !current_label.is_empty() { + labels.push(current_label); + } + + labels + } + + // Write name to packet + // + // [RFC1035] + // 4.1.4. Message compression + // + // In order to reduce the size of messages, the domain system utilizes a + // compression scheme which eliminates the repetition of domain names in a + // message. In this scheme, an entire domain name or a list of labels at + // the end of a domain name is replaced with a pointer to a prior occurrence + // of the same name. + // The pointer takes the form of a two octet sequence: + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | 1 1| OFFSET | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // The first two bits are ones. This allows a pointer to be distinguished + // from a label, since the label must begin with two zero bits because + // labels are restricted to 63 octets or less. (The 10 and 01 combinations + // are reserved for future use.) The OFFSET field specifies an offset from + // the start of the message (i.e., the first octet of the ID field in the + // domain header). A zero offset specifies the first byte of the ID field, + // etc. + // + // This function also handles RFC 6763 Section 4.3 escaping where dots and backslashes + // in instance names are escaped (e.g., "My\\.Service" represents a single label "My.Service"). + // The actual name sent over the wire is the unescaped version. + fn write_name(&mut self, name: &str) -> WriteResult { + // Remove trailing dot if present + let name_to_parse = name.strip_suffix('.').unwrap_or(name); + + // Parse the name considering escape sequences + let labels = Self::parse_escaped_name(name_to_parse); + + if labels.is_empty() { + self.write_byte(0); + return Ok(()); + } + + // Validate before writing anything. + if labels.iter().any(|label| label.len() > MAX_LABEL_BYTES) { + return Err(WriteError::NameTooLong); + } + + // Write each label + for (i, label) in labels.iter().enumerate() { + // Build the remaining name for compression (with dots as separators) + let remaining: String = labels[i..].join("."); + + // Check if we can use compression for the remaining part + const POINTER_MASK: u16 = 0xC000; + if let Some(&offset) = self.names.get(&remaining) { + let pointer = offset | POINTER_MASK; + self.write_short(pointer); + return Ok(()); + } + + // Store this position for potential future compression + self.names.insert(remaining, self.size() as u16); + + // Write the label + self.write_utf8(label)?; + } + + // Write terminating zero byte + self.write_byte(0); + Ok(()) + } + + fn write_byte(&mut self, v: u8) { + self.data.push(v); + } + + fn write_bytes(&mut self, s: &[u8]) { + self.data.extend(s); + } + + /// Writes a single label. Nothing is written if the label is too long to + /// be encoded. + fn write_utf8(&mut self, s: &str) -> WriteResult { + if s.len() > MAX_LABEL_BYTES { + return Err(WriteError::NameTooLong); + } + self.write_byte(s.len() as u8); + self.write_bytes(s.as_bytes()); + Ok(()) + } + + fn write_u32(&mut self, v: u32) { + self.data.extend(&v.to_be_bytes()); + } + + fn write_short(&mut self, v: u16) { + self.data.extend(&v.to_be_bytes()); + } + + /// Marks this finished packet as truncated, i.e. the message continues in + /// the next packet. + fn set_truncated(&mut self) { + let flags = u16::from_be_bytes([self.data[2], self.data[3]]); + self.set_short_at(2, flags | FLAGS_TC); + } + + /// Writes the header fields and finish the packet. + /// This function should be only called when finishing a packet. + /// + /// The header format is based on RFC 1035 section 4.1.1: + /// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 + // + // 1 1 1 1 1 1 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ID | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // |QR| Opcode |AA|TC|RD|RA| Z | RCODE | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | QDCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ANCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | NSCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | ARCOUNT | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // + fn write_header(&mut self, id: u16, flags: u16) { + self.set_short_at(0, id); + self.set_short_at(2, flags); + self.set_short_at(4, self.question_count); + self.set_short_at(6, self.answer_count); + self.set_short_at(8, self.auth_count); + self.set_short_at(10, self.addi_count); + } +} + +/// Encodes a [`DnsOutgoing`] into one or more [`DnsOutPacket`], starting a new +/// packet whenever the current one runs out of room. +struct PacketBuilder<'a> { + out: &'a DnsOutgoing, + + /// Max size of a packet that holds more than one record. + max_size: usize, + + /// IP version these packets are bound for, which decides their absolute + /// ceiling: see [`max_pkt_absolute`]. + is_ipv4: bool, + + finished: Vec, + current: DnsOutPacket, +} + +impl<'a> PacketBuilder<'a> { + fn new(out: &'a DnsOutgoing, max_size: usize, is_ipv4: bool) -> Self { + Self { + out, + max_size, + is_ipv4, + finished: Vec::new(), + current: DnsOutPacket::new(max_size), + } + } + + /// Writes one question or record into the current packet, starting a new + /// packet if it does not fit in the current one. + /// + /// An item that cannot be encoded at all is skipped, leaving the packet as + /// it was. Sections are written in message order, so an item that spills + /// never lands ahead of one already written. + fn add(&mut self, section: Section, write: F) + where + F: Fn(&mut DnsOutPacket) -> WriteResult, + { + match write(&mut self.current) { + Ok(()) => { + self.current.bump(section); + return; + } + // The item can never be encoded: skip it. + Err(WriteError::NameTooLong) => return, + Err(WriteError::PacketFull) => {} + } + + // Packet is full. Flush the current and create a new one. + if !self.current.is_empty() { + self.flush(); + + match write(&mut self.current) { + Ok(()) => { + self.current.bump(section); + return; + } + Err(WriteError::NameTooLong) => return, + Err(WriteError::PacketFull) => {} + } + } + + // Packet is still full. A question such big is not legitimate. + if matches!(section, Section::Question) { + return; + } + + // Packet is still full. We will send this single record. + + // RFC 6762 section 17: + // "a record too large for one MTU-sized packet SHOULD be sent alone, in a + // single IP datagram". + self.current.max_size = max_pkt_absolute(self.is_ipv4); + + if write(&mut self.current).is_ok() { + self.current.bump(section); + self.flush(); + } else { + // Too big even for the hard ceiling: skip the record and carry on. + self.current.max_size = self.max_size; + debug!( + "Record too big for absolute max size, skipping: {:?}", + section + ); + } + } + + /// Finishes the current packet and starts a new empty one. + fn flush(&mut self) { + self.current + .write_header(self.out.wire_id(), self.out.flags); + + let next = DnsOutPacket::new(self.max_size); + self.finished + .push(std::mem::replace(&mut self.current, next)); + } + + fn finish(mut self) -> Vec { + // Always produce at least one packet, even an empty one, but never leave a + // trailing empty packet behind a full one. + if !self.current.is_empty() || self.finished.is_empty() { + self.flush(); + } + + let mut packets = self.finished; + + /* + RFC 6762 section 7.2: https://datatracker.ietf.org/doc/html/rfc6762#section-7.2 + ... + When a Multicast DNS querier sends a query to which it already knows some + answers, it ... sets the TC (Truncated) bit in the header ... [so that the + responder knows] to wait for the remaining known answers before responding. + */ + if self.out.is_query() { + if let Some((_last, rest)) = packets.split_last_mut() { + for packet in rest { + packet.set_truncated(); + } + } + } + + packets + } +} + +/// Representation of one outgoing DNS message that could be sent in one or more packet(s). +#[derive(Debug)] +pub struct DnsOutgoing { + flags: u16, + id: u16, + multicast: bool, + questions: Vec, + answers: Vec<(DnsRecordBox, u64)>, + authorities: Vec, + additionals: Vec, + known_answer_count: i64, // for internal maintenance only +} + +impl DnsOutgoing { + pub fn new(flags: u16) -> Self { + Self { + flags, + id: 0, + multicast: true, + questions: Vec::new(), + answers: Vec::new(), + authorities: Vec::new(), + additionals: Vec::new(), + known_answer_count: 0, + } + } + + pub fn questions(&self) -> &[DnsQuestion] { + &self.questions + } + + /// For testing purposes only. + pub(crate) fn _answers(&self) -> &[(DnsRecordBox, u64)] { + &self.answers + } + + pub fn answers_count(&self) -> usize { + self.answers.len() + } + + pub fn authorities(&self) -> &[DnsRecordBox] { + &self.authorities + } + + pub fn additionals(&self) -> &[DnsRecordBox] { + &self.additionals + } + + pub fn known_answer_count(&self) -> i64 { + self.known_answer_count + } + + pub fn set_id(&mut self, id: u16) { + self.id = id; + } + + /// The id to put in the header, always 0 for multicast. + const fn wire_id(&self) -> u16 { + if self.multicast { + 0 + } else { + self.id + } + } + + pub const fn is_query(&self) -> bool { + (self.flags & FLAGS_QR_MASK) == FLAGS_QR_QUERY + } + + // Adds an additional answer + + // From: RFC 6763, DNS-Based Service Discovery, February 2013 + + // 12. DNS Additional Record Generation + + // DNS has an efficiency feature whereby a DNS server may place + // additional records in the additional section of the DNS message. + // These additional records are records that the client did not + // explicitly request, but the server has reasonable grounds to expect + // that the client might request them shortly, so including them can + // save the client from having to issue additional queries. + + // This section recommends which additional records SHOULD be generated + // to improve network efficiency, for both Unicast and Multicast DNS-SD + // responses. + + // 12.1. PTR Records + + // When including a DNS-SD Service Instance Enumeration or Selective + // Instance Enumeration (subtype) PTR record in a response packet, the + // server/responder SHOULD include the following additional records: + + // o The SRV record(s) named in the PTR rdata. + // o The TXT record(s) named in the PTR rdata. + // o All address records (type "A" and "AAAA") named in the SRV rdata. + + // 12.2. SRV Records + + // When including an SRV record in a response packet, the + // server/responder SHOULD include the following additional records: + + // o All address records (type "A" and "AAAA") named in the SRV rdata. + pub fn add_additional_answer(&mut self, answer: impl DnsRecordExt + 'static) { + trace!("add_additional_answer: {:?}", &answer); + self.additionals.push(answer.boxed()); + } + + /// A workaround as Rust doesn't allow us to pass DnsRecordBox in as `impl DnsRecordExt` + pub fn add_answer_box(&mut self, answer_box: DnsRecordBox) { + self.answers.push((answer_box, 0)); + } + + pub fn add_authority(&mut self, record: DnsRecordBox) { + self.authorities.push(record); + } + + /// Retains only the answers for which `keep` returns true. + pub(crate) fn retain_answers(&mut self, mut keep: F) + where + F: FnMut(&DnsRecordBox) -> bool, + { + self.answers.retain(|(record, _)| keep(record)); + } + + /// Retains only the additional records for which `keep` returns true. + pub(crate) fn retain_additionals(&mut self, mut keep: F) + where + F: FnMut(&DnsRecordBox) -> bool, + { + self.additionals.retain(|record| keep(record)); + } + + /// Returns true if `answer` is added to the outgoing msg. + /// Returns false if `answer` was not added as it expired or suppressed by the incoming `msg`. + pub fn add_answer( + &mut self, + msg: &DnsIncoming, + answer: impl DnsRecordExt + Send + 'static, + ) -> bool { + trace!("Check for add_answer"); + if answer.suppressed_by(msg) { + trace!("my answer is suppressed by incoming msg"); + self.known_answer_count += 1; + return false; + } + + self.add_answer_at_time(answer, 0) + } + + /// Returns true if `answer` is added to the outgoing msg. + /// Returns false if the answer is expired `now` hence not added. + /// If `now` is 0, do not check if the answer expires. + pub fn add_answer_at_time( + &mut self, + answer: impl DnsRecordExt + Send + 'static, + now: u64, + ) -> bool { + if now == 0 || !answer.get_record().is_expired(now) { + trace!("add_answer push: {:?}", &answer); + self.answers.push((answer.boxed(), now)); + return true; + } + false + } + + /// Adds a PTR answer for `service` along with recommended additional records + /// (SRV, TXT, and address records) per [RFC 6763 Section 12.1]. + /// + /// Resolves any name conflicts via `dns_registry` and selects addresses + /// matching the given interface. Does nothing if no addresses are available + /// on `intf` or if the PTR answer is suppressed by known-answer entries in `msg`. + /// + /// [RFC 6763 Section 12.1]: https://tools.ietf.org/html/rfc6763#section-12.1 + pub(crate) fn add_answer_with_additionals( + &mut self, + msg: &DnsIncoming, + service: &ServiceInfo, + intf: &MyIntf, + dns_registry: &DnsRegistry, + is_ipv4: bool, + ) { + let intf_addrs = if is_ipv4 { + service.get_addrs_on_my_intf_v4(intf) + } else { + service.get_addrs_on_my_intf_v6(intf) + }; + if intf_addrs.is_empty() { + trace!("No addrs on LAN of intf {:?}", intf); + return; + } + + // check if we changed our name due to conflicts. + let service_fullname = dns_registry.resolve_name(service.get_fullname()); + let hostname = dns_registry.resolve_name(service.get_hostname()); + + let ptr_added = self.add_answer( + msg, + DnsPointer::new( + service.get_type(), + RRType::PTR, + CLASS_IN, + service.get_other_ttl(), + service_fullname.to_string(), + ), + ); + + if !ptr_added { + trace!("answer was not added for msg {:?}", msg); + return; + } + + if let Some(sub) = service.get_subtype() { + trace!("Adding subdomain {}", sub); + self.add_additional_answer(DnsPointer::new( + sub, + RRType::PTR, + CLASS_IN, + service.get_other_ttl(), + service_fullname.to_string(), + )); + } + + // Add recommended additional answers according to + // https://tools.ietf.org/html/rfc6763#section-12.1. + self.add_additional_answer(DnsSrv::new( + service_fullname, + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_host_ttl(), + service.get_priority(), + service.get_weight(), + service.get_port(), + hostname.to_string(), + )); + + self.add_additional_answer(DnsTxt::new( + service_fullname, + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_other_ttl(), + service.generate_txt(), + )); + + for address in intf_addrs { + self.add_additional_answer(DnsAddress::new( + hostname, + ip_address_rr_type(&address), + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_host_ttl(), + address, + intf.into(), + )); + } + } + + pub fn add_question(&mut self, name: &str, qtype: RRType) { + let q = DnsQuestion { + entry: DnsEntry::new(name.to_string(), qtype, CLASS_IN), + }; + self.questions.push(q); + } + + /// Clear the cache-flush (unique) bit on every answer and additional + /// record. Required for RFC 6762 §6.7 (Legacy Unicast Responses) and + /// §10.2 — a legacy resolver doesn't know about the cache-flush bit + /// and may misinterpret responses where it is set. + pub fn clear_cache_flush_bits(&mut self) { + for (rec, _) in &mut self.answers { + rec.get_record_mut().entry.cache_flush = false; + } + for rec in &mut self.additionals { + rec.get_record_mut().entry.cache_flush = false; + } + for rec in &mut self.authorities { + rec.get_record_mut().entry.cache_flush = false; + } + } + + /// Returns a list of actual DNS packet data to be sent on the wire, each no + /// bigger than `max_size`, over the IP version given by `is_ipv4`. + /// + /// Most callers want [`MAX_PKT_DEFAULT`] for `max_size`. + pub fn to_data_on_wire(&self, max_size: usize, is_ipv4: bool) -> Vec> { + let packet_list = self.to_packets(max_size, is_ipv4); + packet_list.into_iter().map(|p| p.data).collect() + } + + /// Encode self into one or more packets, each no bigger than `max_size`. + /// + /// Questions and records are written in message order and spill into a new + /// packet whenever the current one is full, so none is dropped for lack of + /// room. The one exception is a single record too big to fit in an otherwise + /// empty packet: it is sent alone in an oversized packet, per RFC 6762 + /// section 17. + /// + /// `is_ipv4` tells which IP version the packets are bound for, and so how big + /// that lone oversized packet may get: see [`max_pkt_absolute`]. A record too + /// big even for that could not be sent at all, and is dropped. + /// + /// `max_size` must be no bigger than [`MAX_PKT_ABSOLUTE_IPV6`], the RFC 6762 + /// section 17 ceiling that is legal over either IP version; + /// [`ServiceDaemon::set_max_packet_size`](crate::ServiceDaemon::set_max_packet_size) + /// caps what it accepts. Most callers want [`MAX_PKT_DEFAULT`]. + pub fn to_packets(&self, max_size: usize, is_ipv4: bool) -> Vec { + debug_assert!( + max_size <= MAX_PKT_ABSOLUTE_IPV6, + "max_size {} exceeds the RFC 6762 section 17 ceiling", + max_size + ); + let mut builder = PacketBuilder::new(self, max_size, is_ipv4); + + for question in self.questions.iter() { + builder.add(Section::Question, |packet| packet.write_question(question)); + } + + for (answer, time) in self.answers.iter() { + builder.add(Section::Answer, |packet| { + packet.write_record(answer.as_ref(), *time) + }); + } + + for auth in self.authorities.iter() { + builder.add(Section::Authority, |packet| { + packet.write_record(auth.as_ref(), 0) + }); + } + + for addi in self.additionals.iter() { + builder.add(Section::Additional, |packet| { + packet.write_record(addi.as_ref(), 0) + }); + } + + builder.finish() + } +} + +/// An incoming DNS message. It could be a query or a response. +#[derive(Debug)] +pub struct DnsIncoming { + offset: usize, + data: Vec, + questions: Vec, + answers: Vec, + authorities: Vec, + additional: Vec, + id: u16, + flags: u16, + num_questions: u16, + num_answers: u16, + num_authorities: u16, + num_additionals: u16, + interface_id: InterfaceId, +} + +impl DnsIncoming { + pub fn new(data: Vec, interface_id: InterfaceId) -> Result { + let mut incoming = Self { + offset: 0, + data, + questions: Vec::new(), + answers: Vec::new(), + authorities: Vec::new(), + additional: Vec::new(), + id: 0, + flags: 0, + num_questions: 0, + num_answers: 0, + num_authorities: 0, + num_additionals: 0, + interface_id, + }; + + /* + RFC 1035 section 4.1: https://datatracker.ietf.org/doc/html/rfc1035#section-4.1 + ... + All communications inside of the domain protocol are carried in a single + format called a message. The top level format of message is divided + into 5 sections (some of which are empty in certain cases) shown below: + + +---------------------+ + | Header | + +---------------------+ + | Question | the question for the name server + +---------------------+ + | Answer | RRs answering the question + +---------------------+ + | Authority | RRs pointing toward an authority + +---------------------+ + | Additional | RRs holding additional information + +---------------------+ + */ + if let Err(e) = incoming.read_sections() { + // Annotate the failure with the raw packet, so a malformed message + // can be inspected or decoded offline without a separate capture. + return Err(Error::Msg(format!( + "{e}; raw packet ({} bytes): {:02x?}", + incoming.data.len(), + incoming.data, + ))); + } + + Ok(incoming) + } + + /// Reads the five message sections in order. Kept separate from `new` so a + /// parse failure can be annotated with the raw packet bytes. + fn read_sections(&mut self) -> Result<()> { + self.read_header()?; + self.read_questions()?; + self.read_answers()?; + self.read_authorities()?; + self.read_additional()?; + Ok(()) + } + + pub fn id(&self) -> u16 { + self.id + } + + pub fn questions(&self) -> &[DnsQuestion] { + &self.questions + } + + pub fn answers(&self) -> &[DnsRecordBox] { + &self.answers + } + + pub fn authorities(&self) -> &[DnsRecordBox] { + &self.authorities + } + + pub fn additionals(&self) -> &[DnsRecordBox] { + &self.additional + } + + pub fn answers_mut(&mut self) -> &mut Vec { + &mut self.answers + } + + pub fn authorities_mut(&mut self) -> &mut Vec { + &mut self.authorities + } + + pub fn additionals_mut(&mut self) -> &mut Vec { + &mut self.additional + } + + pub fn all_records(self) -> impl Iterator { + self.answers + .into_iter() + .chain(self.authorities) + .chain(self.additional) + } + + pub fn num_additionals(&self) -> u16 { + self.num_additionals + } + + pub fn num_authorities(&self) -> u16 { + self.num_authorities + } + + pub fn num_questions(&self) -> u16 { + self.num_questions + } + + pub const fn is_query(&self) -> bool { + (self.flags & FLAGS_QR_MASK) == FLAGS_QR_QUERY + } + + pub const fn is_response(&self) -> bool { + (self.flags & FLAGS_QR_MASK) == FLAGS_QR_RESPONSE + } + + fn read_header(&mut self) -> Result<()> { + if self.data.len() < MSG_HEADER_LEN { + return Err(e_fmt!( + "DNS incoming: header is too short: {} bytes", + self.data.len() + )); + } + + let data = &self.data[0..]; + self.id = u16_from_be_slice(&data[..2]); + self.flags = u16_from_be_slice(&data[2..4]); + self.num_questions = u16_from_be_slice(&data[4..6]); + self.num_answers = u16_from_be_slice(&data[6..8]); + self.num_authorities = u16_from_be_slice(&data[8..10]); + self.num_additionals = u16_from_be_slice(&data[10..12]); + + self.offset = MSG_HEADER_LEN; + + trace!( + "read_header: id {}, {} questions {} answers {} authorities {} additionals", + self.id, + self.num_questions, + self.num_answers, + self.num_authorities, + self.num_additionals + ); + Ok(()) + } + + fn read_questions(&mut self) -> Result<()> { + trace!("read_questions: {}", &self.num_questions); + for i in 0..self.num_questions { + let name = self.read_name()?; + + let data = &self.data[self.offset..]; + if data.len() < 4 { + return Err(Error::Msg(format!( + "DNS incoming: question idx {} too short: {}", + i, + data.len() + ))); + } + let ty = u16_from_be_slice(&data[..2]); + let class = u16_from_be_slice(&data[2..4]); + self.offset += 4; + + let Some(rr_type) = RRType::from_u16(ty) else { + return Err(Error::Msg(format!( + "DNS incoming: question idx {i} qtype unknown: {ty}", + ))); + }; + + self.questions.push(DnsQuestion { + entry: DnsEntry::new(name, rr_type, class), + }); + } + Ok(()) + } + + fn read_answers(&mut self) -> Result<()> { + self.answers = self.read_rr_records(self.num_answers)?; + Ok(()) + } + + fn read_authorities(&mut self) -> Result<()> { + self.authorities = self.read_rr_records(self.num_authorities)?; + Ok(()) + } + + fn read_additional(&mut self) -> Result<()> { + self.additional = self.read_rr_records(self.num_additionals)?; + Ok(()) + } + + /// Decodes a sequence of RR records (in answers, authorities and additionals). + fn read_rr_records(&mut self, count: u16) -> Result> { + trace!("read_rr_records: {}", count); + let mut rr_records = Vec::new(); + + // RFC 1035: https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.1 + // + // All RRs have the same top level format shown below: + // 1 1 1 1 1 1 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | | + // / / + // / NAME / + // | | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | TYPE | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | CLASS | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | TTL | + // | | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | RDLENGTH | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| + // / RDATA / + // / / + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + // Muse have at least TYPE, CLASS, TTL, RDLENGTH fields: 10 bytes. + const RR_HEADER_REMAIN: usize = 10; + + for _ in 0..count { + let name = self.read_name()?; + let slice = &self.data[self.offset..]; + + if slice.len() < RR_HEADER_REMAIN { + return Err(Error::Msg(format!( + "read_others: RR '{}' is too short after name: {} bytes", + &name, + slice.len() + ))); + } + + let ty = u16_from_be_slice(&slice[..2]); + let class = u16_from_be_slice(&slice[2..4]); + let mut ttl = u32_from_be_slice(&slice[4..8]); + if ttl == 0 && self.is_response() { + // RFC 6762 section 10.1: + // "...Queriers receiving a Multicast DNS response with a TTL of zero SHOULD + // NOT immediately delete the record from the cache, but instead record + // a TTL of 1 and then delete the record one second later." + // See https://datatracker.ietf.org/doc/html/rfc6762#section-10.1 + + ttl = 1; + } + let rdata_len = u16_from_be_slice(&slice[8..10]) as usize; + self.offset += RR_HEADER_REMAIN; + let next_offset = self.offset + rdata_len; + + // Sanity check for RDATA length. + if next_offset > self.data.len() { + return Err(Error::Msg(format!( + "RR {name} RDATA length {rdata_len} is invalid: remain data len: {}", + self.data.len() - self.offset + ))); + } + + // Decode the RDATA based on the record type. A single record with + // malformed RDATA must not discard the whole message: skip just + // that record and resume at the next one using RDLENGTH. + match self.read_rdata(ty, class, ttl, rdata_len, &name) { + Ok(Some(record)) => { + if self.offset == next_offset { + trace!("read_rr_records: {:?}", &record); + rr_records.push(record); + } else { + debug!( + "skipping record '{}' (type {}): RDATA ended at {}, expected {}", + &name, ty, self.offset, next_offset + ); + } + } + Ok(None) => { + trace!("Unsupported DNS record type: {} name: {}", ty, &name); + } + Err(e) => { + debug!( + "skipping record '{}' (type {}) with invalid RDATA: {}", + &name, ty, e, + ); + } + } + + // Re-anchor to the record boundary defined by RDLENGTH, regardless + // of how the RDATA decoded, so the next record is read from the + // correct offset. + self.offset = next_offset; + } + + Ok(rr_records) + } + + /// Decodes the RDATA of a single record whose header fields have already + /// been read, returning `None` for record types we do not parse. + /// + /// On success the read cursor is left at the end of the RDATA; the caller + /// verifies that against RDLENGTH. Errors are per-record: the caller skips + /// the offending record and continues with the rest of the message. + fn read_rdata( + &mut self, + ty: u16, + class: u16, + ttl: u32, + rdata_len: usize, + name: &str, + ) -> Result> { + let rec: Option = match RRType::from_u16(ty) { + None => None, + + Some(rr_type) => match rr_type { + RRType::CNAME | RRType::PTR => { + Some(DnsPointer::new(name, rr_type, class, ttl, self.read_name()?).boxed()) + } + RRType::TXT => { + Some(DnsTxt::new(name, class, ttl, self.read_vec(rdata_len)?).boxed()) + } + RRType::SRV => Some( + DnsSrv::new( + name, + class, + ttl, + self.read_u16()?, + self.read_u16()?, + self.read_u16()?, + self.read_name()?, + ) + .boxed(), + ), + RRType::HINFO => Some( + DnsHostInfo::new( + name, + rr_type, + class, + ttl, + self.read_char_string()?, + self.read_char_string()?, + ) + .boxed(), + ), + RRType::A => Some( + DnsAddress::new( + name, + rr_type, + class, + ttl, + self.read_ipv4()?.into(), + self.interface_id.clone(), + ) + .boxed(), + ), + RRType::AAAA => Some( + DnsAddress::new( + name, + rr_type, + class, + ttl, + self.read_ipv6()?.into(), + self.interface_id.clone(), + ) + .boxed(), + ), + RRType::NSEC => Some( + DnsNSec::new( + name, + class, + ttl, + self.read_name()?, + self.read_type_bitmap()?, + ) + .boxed(), + ), + _ => None, + }, + }; + + Ok(rec) + } + + fn read_char_string(&mut self) -> Result { + let length = self.data[self.offset]; + self.offset += 1; + self.read_string(length as usize) + } + + fn read_u16(&mut self) -> Result { + let slice = &self.data[self.offset..]; + if slice.len() < U16_SIZE { + return Err(Error::Msg(format!( + "read_u16: slice len is only {}", + slice.len() + ))); + } + let num = u16_from_be_slice(&slice[..U16_SIZE]); + self.offset += U16_SIZE; + Ok(num) + } + + /// Reads the "Type Bit Map" block for a DNS NSEC record. + fn read_type_bitmap(&mut self) -> Result> { + // From RFC 6762: 6.1. Negative Responses + // https://datatracker.ietf.org/doc/html/rfc6762#section-6.1 + // o The Type Bit Map block number is 0. + // o The Type Bit Map block length byte is a value in the range 1-32. + // o The Type Bit Map data is 1-32 bytes, as indicated by length + // byte. + + // Sanity check: at least 2 bytes to read. + if self.data.len() < self.offset + 2 { + return Err(Error::Msg(format!( + "DnsIncoming is too short: {} at NSEC Type Bit Map offset {}", + self.data.len(), + self.offset + ))); + } + + let block_num = self.data[self.offset]; + self.offset += 1; + if block_num != 0 { + return Err(Error::Msg(format!( + "NSEC block number is not 0: {block_num}" + ))); + } + + let block_len = self.data[self.offset] as usize; + if !(1..=32).contains(&block_len) { + return Err(Error::Msg(format!( + "NSEC block length must be in the range 1-32: {block_len}" + ))); + } + self.offset += 1; + + let end = self.offset + block_len; + if end > self.data.len() { + return Err(Error::Msg(format!( + "NSEC block overflow: {} over RData len {}", + end, + self.data.len() + ))); + } + let bitmap = self.data[self.offset..end].to_vec(); + self.offset += block_len; + + Ok(bitmap) + } + + fn read_vec(&mut self, length: usize) -> Result> { + if self.data.len() < self.offset + length { + return Err(e_fmt!( + "DNS Incoming: not enough data to read a chunk of data" + )); + } + + let v = self.data[self.offset..self.offset + length].to_vec(); + self.offset += length; + Ok(v) + } + + fn read_ipv4(&mut self) -> Result { + if self.data.len() < self.offset + 4 { + return Err(e_fmt!("DNS Incoming: not enough data to read an IPV4")); + } + + let bytes: [u8; 4] = self.data[self.offset..self.offset + 4] + .try_into() + .map_err(|_| e_fmt!("DNS incoming: Not enough bytes for reading an IPV4"))?; + self.offset += bytes.len(); + Ok(Ipv4Addr::from(bytes)) + } + + fn read_ipv6(&mut self) -> Result { + if self.data.len() < self.offset + 16 { + return Err(e_fmt!("DNS Incoming: not enough data to read an IPV6")); + } + + let bytes: [u8; 16] = self.data[self.offset..self.offset + 16] + .try_into() + .map_err(|_| e_fmt!("DNS incoming: Not enough bytes for reading an IPV6"))?; + self.offset += bytes.len(); + Ok(Ipv6Addr::from(bytes)) + } + + fn read_string(&mut self, length: usize) -> Result { + if self.data.len() < self.offset + length { + return Err(e_fmt!("DNS Incoming: not enough data to read a string")); + } + + let s = str::from_utf8(&self.data[self.offset..self.offset + length]) + .map_err(|e| Error::Msg(e.to_string()))?; + self.offset += length; + Ok(s.to_string()) + } + + /// Reads a domain name at the current location of `self.data`. + /// + /// See https://datatracker.ietf.org/doc/html/rfc1035#section-3.1 for + /// domain name encoding. + fn read_name(&mut self) -> Result { + let mut name = String::new(); + self.offset = self.read_labels(self.offset, &mut name)?; + Ok(name) + } + + /// Appends the labels encoded at `offset` to `name`, and returns the offset + /// just past that encoding: past the terminating zero byte, or past the + /// compression pointer that ended the name. + /// + /// A name is a sequence of labels, where each label is a length byte + /// followed by that many bytes. The name ends either with a zero length + /// byte, or with a "compression pointer" (top 2 bits set) that redirects + /// to a name written earlier in the same packet. + /// + /// For example, a packet where the question name `_http._tcp.local.` is + /// written out in full at offset 12, and the answer name + /// `myprinter._http._tcp.local.` at offset 40 reuses it via compression: + /// + /// ```text + /// offset: 12 13..17 18 19..22 23 24..28 29 + /// +----+---------+----+--------+----+---------+----+ + /// bytes: | 05 | "_http" | 04 | "_tcp" | 05 | "local" | 00 | + /// +----+---------+----+--------+----+---------+----+ + /// ^len ^len ^len ^ zero byte: end of name + /// + /// offset: 40 41..49 50 51 + /// +----+-------------+----+----+ + /// bytes: | 09 | "myprinter" | C0 | 0C | + /// +----+-------------+----+----+ + /// ^len ^ pointer: 0xC00C ^ 0xC000 = 12, jump back to offset 12 + /// ``` + /// + /// Takes `&self` so that following a pointer cannot move the read cursor. + fn read_labels(&self, mut offset: usize, name: &mut String) -> Result { + let data = &self.data[..]; + + // From RFC1035: + // "...Domain names in messages are expressed in terms of a sequence of labels. + // Each label is represented as a one octet length field followed by that + // number of octets." + // + // "...The compression scheme allows a domain name in a message to be + // represented as either: + // - a sequence of labels ending in a zero octet + // - a pointer + // - a sequence of labels ending with a pointer" + loop { + if offset >= data.len() { + return Err(Error::Msg(format!( + "read_labels: offset: {} data len {}. DnsIncoming: {:?}", + offset, + data.len(), + self + ))); + } + let length = data[offset]; + + // From RFC1035: + // "...a domain name is terminated by a length byte of zero." + if length == 0 { + return Ok(offset + 1); // The end of the name. + } + + // Check the first 2 bits for possible "Message compression". + match length & 0xC0 { + 0x00 => { + // regular utf8 string with length + offset += 1; + let ending = offset + length as usize; + + // Never read beyond the whole data length. + if ending > data.len() { + return Err(Error::Msg(format!( + "read_labels: ending {} exceeds data length {}", + ending, + data.len() + ))); + } + + let label = str::from_utf8(&data[offset..ending]) + .map_err(|e| Error::Msg(format!("read_labels: from_utf8: {e}")))?; + + // `MAX_NAME_BYTES` bounds a possible loop where pointer targets a label that + // is already part of the current name. For example: + // + // offset: 12 13..17 18 19 + // +----+---------+----+----+ + // bytes: | 05 | "_http" | C0 | 0C | + // +----+---------+----+----+ + // ^len ^pointer targets offset 12. + if name.len() + label.len() + 1 > MAX_NAME_BYTES { + return Err(Error::Msg(format!( + "read_labels: name exceeds {MAX_NAME_BYTES} bytes: {name}" + ))); + } + + *name += label; + *name += "."; + offset = ending; + } + 0xC0 => { + // Message compression: a pointer marks the end of a domain name. + self.follow_pointer(offset, name)?; + return Ok(offset + U16_SIZE); + } + _ => { + return Err(Error::Msg(format!( + "Bad name with invalid length: 0x{:x} offset {}, data (so far): {:x?}", + length, + offset, + &data[..offset] + ))); + } + }; + } + } + + /// Follows the compression pointer at offset `at`, appending the labels it + /// names to `name`. + /// + /// See https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4 for + /// message compression. + fn follow_pointer(&self, at: usize, name: &mut String) -> Result<()> { + let data = &self.data[..]; + let mut pointer_at = at; + + // Resolve a run of pointers that target other pointers, so that the + // recursive call below always lands on a label or on the end of a name. + let target = loop { + let slice = &data[pointer_at..]; + if slice.len() < U16_SIZE { + return Err(Error::Msg(format!( + "follow_pointer: u16 slice len is only {}", + slice.len() + ))); + } + let target = (u16_from_be_slice(slice) ^ 0xC000) as usize; + + // RFC1035 section 4.1.4 compresses a name into "a pointer to a prior + // occurrence", so a pointer always points strictly backwards. + if target >= pointer_at { + return Err(Error::Msg(format!( + "Invalid name compression: pointer {target} at offset {pointer_at} must point backwards" + ))); + } + + if data[target] & 0xC0 != 0xC0 { + break target; + } + + // The target is itself a pointer, so follow it. + pointer_at = target; + }; + + self.read_labels(target, name)?; + Ok(()) + } +} + +const fn u16_from_be_slice(bytes: &[u8]) -> u16 { + let u8_array: [u8; 2] = [bytes[0], bytes[1]]; + u16::from_be_bytes(u8_array) +} + +const fn u32_from_be_slice(s: &[u8]) -> u32 { + let u8_array: [u8; 4] = [s[0], s[1], s[2], s[3]]; + u32::from_be_bytes(u8_array) +} + +/// Returns the UNIX time in millis at which this record will have expired +/// by a certain percentage. +const fn get_expiration_time(created: u64, ttl: u32, percent: u32) -> u64 { + // 'created' is in millis, 'ttl' is in seconds, hence: + // ttl * 1000 * (percent / 100) => ttl * percent * 10 + created + (ttl as u64 * percent as u64 * 10) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, + }; + + use super::{ + u16_from_be_slice, + DnsAddress, + DnsHostInfo, + DnsIncoming, + DnsOutPacket, + DnsOutgoing, + DnsPointer, + DnsTxt, + RRType, + CLASS_CACHE_FLUSH, + CLASS_IN, + FLAGS_QR_QUERY, + FLAGS_QR_RESPONSE, + FLAGS_TC, + MAX_PKT_ABSOLUTE_IPV6, + MAX_PKT_DEFAULT, + MSG_HEADER_LEN, + }; + use crate::InterfaceId; + + /// The `is_ipv4` argument of `to_packets`. IPv6 has the smaller of the two + /// absolute ceilings, so it is the stricter one to encode for. + const IPV6: bool = false; + + #[test] + fn test_dns_outgoing_serialization_empty() { + let out = DnsOutgoing::new(0); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].as_bytes(), &[0; 12]); + let expected_names = HashMap::new(); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_question() { + let mut out = DnsOutgoing::new(0); + out.add_question("123.test", RRType::A); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // Header + // Payload + 3, 49, 50, 51, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("123.test".to_string(), 12); + expected_names.insert("test".to_string(), 16); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_question_with_authority() { + let mut out = DnsOutgoing::new(0); + out.add_question("123.test", RRType::ANY); + out.add_authority(Box::new(DnsTxt::new( + "124.test", + CLASS_IN, + 0x00112233, + b"help".to_vec(), + ))); + out.add_authority(Box::new(DnsHostInfo::new( + "124.test", + RRType::CNAME, + CLASS_IN, + 0x00112233, + "arm".to_string(), + "linux".to_string(), + ))); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, // Header + // Payload + 3, 49, 50, 51, 4, 116, 101, 115, 116, 0, 0, 255, 0, 1, 3, 49, 50, 52, 192, 16, 0, + 16, 0, 1, 0, 17, 34, 51, 0, 4, 104, 101, 108, 112, 192, 26, 0, 5, 0, 1, 0, 17, 34, + 51, 0, 8, 97, 114, 109, 108, 105, 110, 117, 120, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("123.test".to_string(), 12); + expected_names.insert("test".to_string(), 16); + expected_names.insert("124.test".to_string(), 26); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_additional_answer() { + let mut out = DnsOutgoing::new(0); + out.add_additional_answer(DnsAddress::new( + "test.local", + RRType::A, + CLASS_IN | CLASS_CACHE_FLUSH, + 0xdead_beef, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + InterfaceId::default(), + )); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // Header + // Payload + 4, 116, 101, 115, 116, 5, 108, 111, 99, 97, 108, 0, 0, 1, 128, 1, 222, 173, 190, + 239, 0, 4, 127, 0, 0, 1, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("test.local".to_string(), 12); + expected_names.insert("local".to_string(), 17); + assert_eq!(&packets[0].names, &expected_names); + } + + #[test] + fn test_dns_outgoing_serialization_answer_at_time() { + let mut out = DnsOutgoing::new(0); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::PTR, + CLASS_IN, + 0xaaaa5555, + "test-service".to_string(), + ), + 0, + ); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, // Header + // Payload + 4, 116, 101, 115, 116, 0, 0, 12, 0, 1, 170, 170, 85, 85, 0, 14, 12, 116, 101, 115, + 116, 45, 115, 101, 114, 118, 105, 99, 101, 0, + ] + ); + + let mut out = DnsOutgoing::new(0); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::CNAME, + CLASS_IN, + 0xaaaa5555, + "test-service.local".to_string(), + ), + 0, + ); + out.add_answer_at_time( + DnsPointer::new( + "test", + RRType::AAAA, + CLASS_IN, + 0xffffffff, + "test-service.local".to_string(), + ), + 0, + ); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, // Header + // Payload + 4, 116, 101, 115, 116, 0, 0, 5, 0, 1, 170, 170, 85, 85, 0, 20, 12, 116, 101, 115, + 116, 45, 115, 101, 114, 118, 105, 99, 101, 5, 108, 111, 99, 97, 108, 0, 192, 12, 0, + 28, 0, 1, 255, 255, 255, 255, 0, 2, 192, 28, + ] + ); + let mut expected_names = HashMap::new(); + expected_names.insert("test".to_string(), 12); + expected_names.insert("test-service.local".to_string(), 28); + expected_names.insert("local".to_string(), 41); + assert_eq!(&packets[0].names, &expected_names); + } + + /// A question whose name has a label longer than 63 bytes cannot be + /// encoded. It must be skipped, not panic. (Note the question count in the + /// header must reflect the questions actually written.) + #[test] + fn test_dns_outgoing_question_label_too_long() { + let long_label = "a".repeat(64); + let mut out = DnsOutgoing::new(0); + out.add_question(&format!("{long_label}.local"), RRType::PTR); + out.add_question("123.test", RRType::A); + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!( + packets[0].as_bytes(), + &[ + 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // Header: 1 question + // Payload: only "123.test" made it in. + 3, 49, 50, 51, 4, 116, 101, 115, 116, 0, 0, 1, 0, 1, + ] + ); + + // The rolled back name must not leave a stale compression offset behind. + let mut expected_names = HashMap::new(); + expected_names.insert("123.test".to_string(), 12); + expected_names.insert("test".to_string(), 16); + assert_eq!(&packets[0].names, &expected_names); + } + + /// A record whose rdata carries an unencodable name (here a PTR alias) is + /// dropped as a whole, leaving the rest of the packet intact. + #[test] + fn test_dns_outgoing_record_label_too_long() { + let long_label = "a".repeat(64); + let mut out = DnsOutgoing::new(0); + out.add_answer_at_time( + DnsPointer::new( + "_test._tcp.local.", + RRType::PTR, + CLASS_IN, + 0, + format!("{long_label}._test._tcp.local."), + ), + 0, + ); + out.add_answer_at_time( + DnsPointer::new( + "_test._tcp.local.", + RRType::PTR, + CLASS_IN, + 0, + "ok._test._tcp.local.".to_string(), + ), + 0, + ); + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + + // Header answer count is 1: the first answer was dropped. + assert_eq!(&packets[0].as_bytes()[6..8], &[0, 1]); + + // Re-parsing must succeed and yield only the good answer. + let incoming = DnsIncoming::new( + packets[0].as_bytes().to_vec(), + InterfaceId { + name: "test".to_string(), + index: 1, + }, + ) + .unwrap(); + assert_eq!(incoming.answers().len(), 1); + } + + /// A name learned from the network can hold a label that ends with a + /// backslash, which escapes the following label separator. Unescaping such + /// a name on the way out merges two 63-byte labels into a 127-byte one. + /// This used to panic the daemon thread. See issue #483. + #[test] + fn test_incoming_name_with_merged_labels_does_not_panic() { + // A query with one question: "aa..a\" + "bb..b", 63 bytes each. + let mut data: Vec = vec![0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; + data.push(63); + data.extend(vec![b'a'; 62]); + data.push(b'\\'); + data.push(63); + data.extend(vec![b'b'; 63]); + data.push(0); + data.extend([0, 12, 0, 1]); // PTR, IN + + let incoming = DnsIncoming::new( + data, + InterfaceId { + name: "test".to_string(), + index: 1, + }, + ) + .unwrap(); + let name = incoming.questions()[0].entry.name.clone(); + + // The two labels merged: the trailing backslash escaped the separator. + assert!(name.starts_with("aaa")); + assert!(name.contains("\\.bbb")); + + // Re-emitting it must drop the question rather than panic. + let mut out = DnsOutgoing::new(0); + out.add_question(&name, RRType::PTR); + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].as_bytes(), &[0; MSG_HEADER_LEN]); + } + + /// A pointer that points into the name currently being read is a loop: + /// following it re-reads the same labels and arrives at the same pointer + /// again. `read_name` must reject such a name instead of hanging. + #[test] + fn test_read_name_pointer_loop_is_rejected() { + // A response with one PTR record. Its name starts at offset 12 and is + // encoded as: label "local", label "_x", then a pointer back to 12, + // i.e. to the "local" label of this very name. + let mut data: Vec = vec![0, 0, 0x84, 0, 0, 0, 0, 1, 0, 0, 0, 0]; + data.extend_from_slice(&[5, b'l', b'o', b'c', b'a', b'l']); // offset 12 + data.extend_from_slice(&[2, b'_', b'x']); // offset 18 + data.extend_from_slice(&[0xC0, 12]); // offset 21: pointer to 12 + data.extend_from_slice(&[0, 12, 0, 1]); // PTR, IN + data.extend_from_slice(&[0, 0, 0, 120]); // TTL + data.extend_from_slice(&[0, 2]); // RDLENGTH + data.extend_from_slice(&[0xC0, 12]); // RDATA: pointer to 12 + + assert!(DnsIncoming::new(data, test_interface_id()).is_err()); + } + + /// A legal name that follows a pointer backwards and then meets a second + /// pointer whose target sits *after* the start of the name being read, yet + /// still strictly *before* that second pointer's own position. + /// + /// Such a message probably never appears in reality, but it still has to parse. + /// Reading the answer's name walks: 700 -> 640 -> 62-byte label -> 703 -> + /// 702 -> zero byte, name complete. + #[test] + fn test_read_name_pointer_after_backward_jump() { + /// Appends a question: one label of `label_len` 'a' bytes, PTR, IN. + fn push_question(data: &mut Vec, label_len: usize) { + data.push(label_len as u8); + data.extend(vec![b'a'; label_len]); + data.push(0); // end of the name + data.extend_from_slice(&[0, 12]); // QTYPE: PTR + data.extend_from_slice(&[0, 1]); // QCLASS: IN + } + + let mut data: Vec = vec![ + 0, 0, // ID + 0, 0, // flags: a query + 0, 11, // 11 questions + 0, 1, // 1 answer + 0, 0, 0, 0, // no authorities, no additionals + ]; + + // Questions #1 to #10, 66 bytes each: 12 + 660 = 672. + for _ in 0..10 { + push_question(&mut data, 60); + } + assert_eq!(data.len(), 672); + + // Question #11, 28 bytes, so that the answer record starts at 700. + push_question(&mut data, 22); + assert_eq!(data.len(), 700); + + // Plant the label length inside question #10's label. + data[640] = 62; + + // The answer record. + data.extend_from_slice(&[0xC2, 0x80]); // 700: name: pointer to 640 + data.extend_from_slice(&[0x00, 0xC2]); // 702: TYPE, unknown type 194 + data.extend_from_slice(&[0xBE, 0x01]); // 704: CLASS. 703..705 is a pointer to 702 + data.extend_from_slice(&[0, 0, 0, 120]); // TTL + data.extend_from_slice(&[0, 0]); // RDLENGTH: no RDATA + + // Both pointers point backwards from where they are. + assert_eq!(u16_from_be_slice(&data[700..702]) ^ 0xC000, 640); + assert_eq!(u16_from_be_slice(&data[703..705]) ^ 0xC000, 702); + + let incoming = DnsIncoming::new(data, test_interface_id()) + .expect("a name whose pointers all point backwards must parse"); + assert_eq!(incoming.questions().len(), 11); + + // The answer's type is unknown to us, so the record itself is skipped. + assert_eq!(incoming.answers().len(), 0); + } + + /// Two pointers at offsets 23 and 25 that target each other (23 -> 25 -> + /// 23). Both sit below offset 27, where the name starts. + /// + /// `follow_pointer` requires each target to be strictly below the + /// pointer's *own* position. A cycle always contains at least one + /// non-backward hop, so this rule breaks every cycle. + #[test] + fn test_read_name_mutual_pointers_are_rejected() { + let mut data: Vec = vec![0, 0, 0x84, 0, 0, 0, 0, 2, 0, 0, 0, 0]; + + // Answer #1: the root name, then an unknown type, so its RDATA is skipped. + data.push(0); // 12: the root name + data.extend_from_slice(&[0x00, 0xC2]); // 13: TYPE: unknown type 194 + data.extend_from_slice(&[0x00, 0x01]); // 15: CLASS: IN + data.extend_from_slice(&[0, 0, 0, 120]); // 17: TTL + data.extend_from_slice(&[0x00, 0x04]); // 21: RDLENGTH + data.extend_from_slice(&[0xC0, 25]); // 23: RDATA: pointer to 25 + data.extend_from_slice(&[0xC0, 23]); // 25: RDATA: pointer to 23 + assert_eq!(data.len(), 27); + + // Answer #2, whose name points into that RDATA. + data.extend_from_slice(&[0xC0, 23]); // 27: name: pointer to 23 + data.extend_from_slice(&[0x00, 0xC2, 0x00, 0x01]); // TYPE, CLASS + data.extend_from_slice(&[0, 0, 0, 120]); // TTL + data.extend_from_slice(&[0, 0]); // RDLENGTH: no RDATA + + // Every pointer targets an offset below the start of the name at 27. + assert_eq!(u16_from_be_slice(&data[27..29]) ^ 0xC000, 23); + assert_eq!(u16_from_be_slice(&data[23..25]) ^ 0xC000, 25); + assert_eq!(u16_from_be_slice(&data[25..27]) ^ 0xC000, 23); + + assert!(DnsIncoming::new(data, test_interface_id()).is_err()); + } + + /// A label whose read carries the cursor onto a pointer that jumps back to + /// that same label. Every pointer here points backwards from its own + /// position, so no comparison of offsets rejects it: the cycle is broken + /// only by the name growing past [`MAX_NAME_BYTES`]. + #[test] + fn test_read_name_label_cycle_is_rejected() { + let mut data: Vec = vec![0, 0, 0x84, 0, 0, 0, 0, 2, 0, 0, 0, 0]; + + // Answer #1, again an unknown type so that its RDATA is skipped. + data.push(0); // 12: the root name + data.extend_from_slice(&[0x00, 0xC2]); // 13: TYPE: unknown type 194 + data.extend_from_slice(&[0x00, 0x01]); // 15: CLASS: IN + data.extend_from_slice(&[0, 0, 0, 120]); // 17: TTL + data.extend_from_slice(&[0x00, 0x07]); // 21: RDLENGTH + data.push(0x04); // 23: RDATA: a label of 4 bytes, ending at 28 + data.extend_from_slice(b"aaaa"); // 24 + data.extend_from_slice(&[0xC0, 23]); // 28: RDATA: pointer to 23 + assert_eq!(data.len(), 30); + + // Answer #2, whose name enters the cycle. + data.extend_from_slice(&[0xC0, 23]); // 30: name: pointer to 23 + data.extend_from_slice(&[0x00, 0xC2, 0x00, 0x01]); // TYPE, CLASS + data.extend_from_slice(&[0, 0, 0, 120]); // TTL + data.extend_from_slice(&[0, 0]); // RDLENGTH: no RDATA + + // Reading the label at 23 leaves the cursor on the pointer at 28, which + // points backwards from 28 and lands back on the label. + assert_eq!(u16_from_be_slice(&data[28..30]) ^ 0xC000, 23); + assert_eq!(u16_from_be_slice(&data[30..32]) ^ 0xC000, 23); + + assert!(DnsIncoming::new(data, test_interface_id()).is_err()); + } + + /// A real `_miio._udp.local.` response captured behind an avahi mDNS + /// reflector (see issue #468). It has 5 answers, one of which is an NSEC + /// whose Next Domain Name is a compression pointer to its own offset (a + /// self-reference, offset 121 -> 121). That one record is malformed, but + /// the other four (PTR, A, SRV, TXT) are fine, and lenient parsers such as + /// tcpdump decode the whole packet. + /// + /// The parser must skip only the malformed NSEC and keep the good records, + /// rather than discarding the entire message. + #[test] + fn test_malformed_nsec_record_is_skipped() { + let data: Vec = vec![ + 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x05, 0x5f, + 0x6d, 0x69, 0x69, 0x6f, 0x04, 0x5f, 0x75, 0x64, 0x70, 0x05, 0x6c, 0x6f, 0x63, 0x61, + 0x6c, 0x00, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x24, 0x21, 0x64, + 0x72, 0x65, 0x61, 0x6d, 0x65, 0x2d, 0x76, 0x61, 0x63, 0x75, 0x75, 0x6d, 0x2d, 0x70, + 0x32, 0x30, 0x32, 0x39, 0x5f, 0x6d, 0x69, 0x69, 0x6f, 0x34, 0x34, 0x37, 0x33, 0x30, + 0x35, 0x32, 0x34, 0x37, 0xc0, 0x0c, 0x21, 0x64, 0x72, 0x65, 0x61, 0x6d, 0x65, 0x2d, + 0x76, 0x61, 0x63, 0x75, 0x75, 0x6d, 0x2d, 0x70, 0x32, 0x30, 0x32, 0x39, 0x5f, 0x6d, + 0x69, 0x69, 0x6f, 0x34, 0x34, 0x37, 0x33, 0x30, 0x35, 0x32, 0x34, 0x37, 0x00, 0x00, + 0x2f, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x09, 0xc0, 0x79, 0x00, 0x05, 0x40, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x4c, 0x00, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, + 0x00, 0x04, 0x0a, 0x2a, 0x02, 0x32, 0xc0, 0x28, 0x00, 0x21, 0x80, 0x01, 0x00, 0x00, + 0x00, 0x78, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0xd4, 0x31, 0xc0, 0x4c, 0xc0, 0x28, + 0x00, 0x10, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x0f, 0x0e, 0x70, 0x61, 0x74, + 0x68, 0x3d, 0x2f, 0x6d, 0x79, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + ]; + + // The offending record: the NSEC's Next Domain Name at offset 121 is a + // pointer to offset 121 (itself). + assert_eq!(u16_from_be_slice(&data[121..123]) ^ 0xC000, 121); + + let incoming = DnsIncoming::new(data, test_interface_id()) + .expect("one malformed record must not fail the whole packet"); + + // Four of the five records survive; only the NSEC is dropped. + assert_eq!(incoming.answers().len(), 4); + assert!( + !incoming + .answers() + .iter() + .any(|r| r.get_type() == RRType::NSEC), + "the malformed NSEC record must be skipped" + ); + } + + fn test_interface_id() -> InterfaceId { + InterfaceId { + name: "test".to_string(), + index: 1, + } + } + + /// The "flags" field of a finished packet. + fn packet_flags(packet: &DnsOutPacket) -> u16 { + let bytes = packet.as_bytes(); + u16::from_be_bytes([bytes[2], bytes[3]]) + } + + fn ptr_answer(index: usize) -> DnsPointer { + DnsPointer::new( + "_spill._tcp.local.", + RRType::PTR, + CLASS_IN, + 4500, + format!("instance-{index:04}._spill._tcp.local."), + ) + } + + /// Re-parses each packet and returns the total number of answers found, which + /// checks the header counts against what each packet actually holds. + fn parsed_answer_count(packets: &[DnsOutPacket]) -> usize { + packets + .iter() + .map(|packet: &DnsOutPacket| { + let parsed = DnsIncoming::new(packet.as_bytes().to_vec(), test_interface_id()) + .expect("each packet must parse on its own"); + assert!( + !parsed.answers().is_empty(), + "a spilled packet must not be empty" + ); + parsed.answers().len() + }) + .sum() + } + + /// A response too big for one packet spills into more packets. Every record + /// must survive: before, records that did not fit were silently dropped. + #[test] + fn test_dns_outgoing_response_spills_into_packets() { + const ANSWER_COUNT: usize = 100; + + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + for i in 0..ANSWER_COUNT { + out.add_answer_at_time(ptr_answer(i), 0); + } + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert!( + packets.len() > 1, + "{} answers should not fit in one packet", + ANSWER_COUNT + ); + + for packet in &packets { + assert!( + packet.size() <= MAX_PKT_DEFAULT, + "packet of {} bytes exceeds the limit", + packet.size() + ); + + // A multi-packet response is a series of independent responses: unlike + // a query's known answers, it does not use the TC bit. + assert_eq!(packet_flags(packet) & FLAGS_TC, 0); + } + + assert_eq!(parsed_answer_count(&packets), ANSWER_COUNT); + } + + /// RFC 6762 section 7.2: a querier sending known answers in more than one + /// packet sets the TC bit in every packet but the last. + #[test] + fn test_dns_outgoing_query_truncation_bit() { + let mut out = DnsOutgoing::new(FLAGS_QR_QUERY); + out.add_question("_spill._tcp.local.", RRType::PTR); + for i in 0..100 { + out.add_answer_box(Box::new(ptr_answer(i))); + } + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert!( + packets.len() > 1, + "known answers should not fit in one packet" + ); + + let (last, rest) = packets.split_last().expect("at least one packet"); + for packet in rest { + assert_ne!( + packet_flags(packet) & FLAGS_TC, + 0, + "a packet with more known answers to follow must set TC" + ); + } + assert_eq!( + packet_flags(last) & FLAGS_TC, + 0, + "the last packet must not set TC" + ); + + // The question goes in the first packet only, and no answer is lost. + assert_eq!(packets[0].as_bytes()[4..6], 1u16.to_be_bytes()); + for packet in rest.iter().skip(1) { + assert_eq!(packet.as_bytes()[4..6], [0, 0]); + } + assert_eq!(parsed_answer_count(&packets), 100); + } + + /// RFC 6762 section 17: a record too large for one MTU-sized packet is sent + /// alone in an oversized packet, rather than dropped. It must be alone, since + /// a fragmented packet "MUST NOT contain more than one resource record". + #[test] + fn test_dns_outgoing_oversized_record_sent_alone() { + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time(ptr_answer(0), 0); + out.add_answer_at_time( + DnsTxt::new("big._spill._tcp.local.", CLASS_IN, 4500, vec![b'x'; 2000]), + 0, + ); + out.add_answer_at_time(ptr_answer(1), 0); + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert_eq!(packets.len(), 3, "the big record needs a packet to itself"); + + assert!(packets[0].size() <= MAX_PKT_DEFAULT); + assert!( + packets[1].size() > MAX_PKT_DEFAULT, + "the oversized record must not be dropped" + ); + // Still small enough that the send path will let it out. + assert!(packets[1].size() <= MAX_PKT_ABSOLUTE_IPV6); + assert!(packets[2].size() <= MAX_PKT_DEFAULT); + + // One record per packet here, the middle one being the big TXT. + let parsed = DnsIncoming::new(packets[1].as_bytes().to_vec(), test_interface_id()).unwrap(); + assert_eq!(parsed.answers().len(), 1); + assert_eq!(parsed.answers()[0].get_name(), "big._spill._tcp.local."); + assert_eq!(parsed_answer_count(&packets), 3); + } + + /// A record over the RFC 6762 section 17 ceiling could not go out on the wire + /// even in a packet of its own, so it is dropped while its neighbors survive. + #[test] + fn test_dns_outgoing_record_over_absolute_ceiling_dropped() { + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time(ptr_answer(0), 0); + out.add_answer_at_time( + DnsTxt::new( + "huge._spill._tcp.local.", + CLASS_IN, + 4500, + vec![b'x'; MAX_PKT_ABSOLUTE_IPV6], + ), + 0, + ); + out.add_answer_at_time(ptr_answer(1), 0); + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + for packet in &packets { + assert!( + packet.size() <= MAX_PKT_ABSOLUTE_IPV6, + "an unsendable packet must never be generated" + ); + } + assert_eq!( + parsed_answer_count(&packets), + 2, + "only the huge record is dropped" + ); + } + + /// Authorities and additionals spill too, and stay in their own sections. + #[test] + fn test_dns_outgoing_all_sections_spill() { + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + for i in 0..40 { + out.add_answer_at_time(ptr_answer(i), 0); + } + for i in 40..80 { + out.add_authority(Box::new(ptr_answer(i))); + } + for i in 80..120 { + out.add_additional_answer(ptr_answer(i)); + } + + let packets = out.to_packets(MAX_PKT_DEFAULT, IPV6); + assert!(packets.len() > 1); + + let mut answers = 0; + let mut authorities = 0; + let mut additionals = 0; + for packet in &packets { + assert!(packet.size() <= MAX_PKT_DEFAULT); + let parsed = DnsIncoming::new(packet.as_bytes().to_vec(), test_interface_id()).unwrap(); + answers += parsed.answers().len(); + authorities += parsed.authorities().len(); + additionals += parsed.additionals().len(); + } + + assert_eq!(answers, 40); + assert_eq!(authorities, 40); + assert_eq!(additionals, 40); + } +} diff --git a/vendor/mdns-sd/src/error.rs b/vendor/mdns-sd/src/error.rs new file mode 100644 index 0000000..cb6dfff --- /dev/null +++ b/vendor/mdns-sd/src/error.rs @@ -0,0 +1,51 @@ +use std::fmt; + +/// A basic error type from this library. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum Error { + /// Like a classic `EAGAIN`. Returned by [`ServiceDaemon`](crate::ServiceDaemon) + /// methods when the daemon's bounded command queue is temporarily full, + /// so the command could not be enqueued. The caller can retry after a + /// short delay. + Again, + + /// The daemon thread has exited and its command channel is closed, so the + /// command could not be delivered. Returned by [`ServiceDaemon`](crate::ServiceDaemon) + /// methods after [`shutdown`](crate::ServiceDaemon::shutdown) has been + /// called or after the daemon thread has terminated for another reason. + /// Retrying will not help; callers should log and move on (or create a + /// new [`ServiceDaemon`](crate::ServiceDaemon)). + DaemonShutdown, + + /// A generic error message. + Msg(String), + + /// Error during parsing of ip address + ParseIpAddr(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Msg(s) => write!(f, "{s}"), + Self::ParseIpAddr(s) => write!(f, "parsing of ip addr failed, reason: {s}"), + Self::Again => write!(f, "try again"), + Self::DaemonShutdown => write!(f, "daemon has shut down"), + } + } +} + +impl std::error::Error for Error {} + +/// One and only `Result` type from this library crate. +pub type Result = core::result::Result; + +/// A simple macro to report all kinds of errors. +macro_rules! e_fmt { + ($($arg:tt)+) => { + Error::Msg(format!($($arg)+)) + }; + } + +pub(crate) use e_fmt; diff --git a/vendor/mdns-sd/src/lib.rs b/vendor/mdns-sd/src/lib.rs new file mode 100644 index 0000000..15991ec --- /dev/null +++ b/vendor/mdns-sd/src/lib.rs @@ -0,0 +1,219 @@ +//! A small and safe library for Multicast DNS-SD (Service Discovery). +//! +//! This library creates one new thread to run a mDNS daemon, and exposes +//! its API that interacts with the daemon via a +//! [`flume`](https://crates.io/crates/flume) channel. The channel supports +//! both `recv()` and `recv_async()`. +//! +//! For example, a client querying (browsing) a service behaves like this: +//!```text +//! Client mDNS daemon thread +//! | | starts its run-loop. +//! | --- Browse --> | +//! | | detects services +//! | | finds service instance A +//! | <-- Found A -- | +//! | ... | resolves service A +//! | <-- Resolved A -- | +//! | ... | +//!``` +//! All commands in the public API are sent to the daemon using the unblocking `try_send()` +//! so that the caller can use it with both sync and async code, with no dependency on any +//! particular async runtimes. +//! +//! # Usage +//! +//! The user starts with creating a daemon by calling [`ServiceDaemon::new()`]. +//! Then as a mDNS querier, the user would call [`browse`](`ServiceDaemon::browse`) to +//! search for services, and/or as a mDNS responder, call [`register`](`ServiceDaemon::register`) +//! to publish (i.e. announce) its own service. And, the daemon type can be cloned and passed +//! around between threads. +//! +//! The user can also call [`resolve_hostname`](`ServiceDaemon::resolve_hostname`) to +//! resolve a hostname to IP addresses using mDNS, regardless if the host publishes a service name. +//! +//! ## Example: a client querying for a service type. +//! +//! ```rust +//! use mdns_sd::{ServiceDaemon, ServiceEvent}; +//! +//! // Create a daemon +//! let mdns = ServiceDaemon::new().expect("Failed to create daemon"); +//! +//! // Browse for a service type. +//! let service_type = "_mdns-sd-my-test._udp.local."; +//! let receiver = mdns.browse(service_type).expect("Failed to browse"); +//! +//! // Receive the browse events in sync or async. Here is +//! // an example of using a thread. Users can call `receiver.recv_async().await` +//! // if running in async environment. +//! std::thread::spawn(move || { +//! while let Ok(event) = receiver.recv() { +//! match event { +//! ServiceEvent::ServiceResolved(resolved) => { +//! println!("Resolved a new service: {}", resolved.fullname); +//! } +//! other_event => { +//! println!("Received other event: {:?}", &other_event); +//! } +//! } +//! } +//! }); +//! +//! // Gracefully shutdown the daemon. +//! std::thread::sleep(std::time::Duration::from_secs(1)); +//! mdns.shutdown().unwrap(); +//! ``` +//! +//! ## Example: a server publishs a service and responds to queries. Use `monitor()` to +//! receive events from the daemon, especially errors. +//! +//! ```rust +//! use mdns_sd::{ServiceDaemon, ServiceInfo}; +//! use std::collections::HashMap; +//! +//! // Create a daemon +//! let mdns = ServiceDaemon::new().expect("Failed to create daemon"); +//! +//! // Optional: setup a monitor channel to receive events, especially errors from the daemon. +//! let receiver = mdns.monitor().expect("Failed to monitor daemon"); +//! std::thread::spawn(move || { +//! while let Ok(event) = receiver.recv() { +//! match event { +//! mdns_sd::DaemonEvent::Error(error) => { +//! eprintln!("Daemon error: {error}"); +//! } +//! _ => {} +//! } +//! } +//! }); +//! +//! // Create a service info. +//! // Make sure that the service name: "mdns-sd-my-test" is not longer than the max length limit (15 by default). +//! let service_type = "_mdns-sd-my-test._udp.local."; +//! let instance_name = "my_instance"; +//! let ip = "192.168.1.12"; +//! let host_name = "192.168.1.12.local."; +//! let port = 5200; +//! let properties = [("property_1", "test"), ("property_2", "1234")]; +//! +//! let my_service = ServiceInfo::new( +//! service_type, +//! instance_name, +//! host_name, +//! ip, +//! port, +//! &properties[..], +//! ).unwrap(); +//! +//! // Register with the daemon, which publishes the service. +//! mdns.register(my_service).expect("Failed to register our service"); +//! +//! // Gracefully shutdown the daemon +//! std::thread::sleep(std::time::Duration::from_secs(1)); +//! mdns.shutdown().unwrap(); +//! ``` +//! +//! ## Conflict resolution +//! +//! When a service responder receives another DNS record with the same name as its own record, a conflict occurs. +//! The mDNS [RFC 6762 section 9](https://datatracker.ietf.org/doc/html/rfc6762#section-9) defines a conflict resolution +//! mechanism, which is implemented in this library. When an application wishes to be notified of conflict resolutions, +//! it follows the steps below: +//! +//! 1. The application calls [`ServiceDaemon::monitor()`] to monitor all events from the daemon service responder. +//! 2. When a conflict resolution causes a name change, the library sends an event to the application: [`DaemonEvent::NameChange`], +//! which provides [`DnsNameChange`] with details. +//! +//! # Limitations +//! +//! This implementation is based on the following RFCs: +//! - mDNS: [RFC 6762](https://tools.ietf.org/html/rfc6762) +//! - DNS-SD: [RFC 6763](https://tools.ietf.org/html/rfc6763) +//! - DNS: [RFC 1035](https://tools.ietf.org/html/rfc1035) +//! +//! We focus on the common use cases at first, and currently have the following limitations: +//! - Only support multicast, not unicast send/recv. +//! - Only support 32-bit or bigger platforms, not 16-bit platforms. +//! +//! # Use logging in tests and examples +//! +//! Often times it is helpful to enable logging running tests or examples to examine the details. +//! For tests and examples, we use [`env_logger`](https://docs.rs/env_logger/latest/env_logger/) +//! as the logger and use [`test-log`](https://docs.rs/test-log/latest/test_log/) to enable logging for tests. +//! For instance you can show all test logs using: +//! +//! ```shell +//! RUST_LOG=debug cargo test integration_success -- --nocapture +//! ``` +//! +//! We also enabled the logging for the examples. For instance you can do: +//! +//! ```shell +//! RUST_LOG=debug cargo run --example query _printer._tcp +//! ``` +//! + +#![forbid(unsafe_code)] +#![allow(clippy::single_component_path_imports)] + +// log for logging (optional). +#[cfg(feature = "logging")] +use log; + +#[cfg(not(feature = "logging"))] +#[macro_use] +mod log { + macro_rules! trace ( ($($tt:tt)*) => {{}} ); + macro_rules! debug ( ($($tt:tt)*) => {{}} ); + macro_rules! info ( ($($tt:tt)*) => {{}} ); + macro_rules! warn ( ($($tt:tt)*) => {{}} ); + macro_rules! error ( ($($tt:tt)*) => {{}} ); +} + +mod dns_cache; +mod dns_parser; +mod error; +mod service_daemon; +mod service_info; + +use std::time::SystemTime; + +pub use dns_parser::{InterfaceId, RRType, ScopedIp, ScopedIpV4, ScopedIpV6, MAX_PKT_DEFAULT}; +pub use error::{Error, Result}; +/// A handler to receive messages from [ServiceDaemon]. Re-export from `flume` crate. +pub use flume::Receiver; +/// Errors returned by the receiving methods of `Receiver`. Re-export from `flume` crate. +pub use flume::{RecvError, RecvTimeoutError, TryRecvError}; +pub use service_daemon::{ + DaemonEvent, + DaemonStatus, + DnsNameChange, + HostnameResolutionEvent, + IfKind, + IfPredicate, + Metrics, + ServiceDaemon, + ServiceEvent, + UnregisterStatus, + IP_CHECK_INTERVAL_IN_SECS_DEFAULT, + MDNS_PORT, + SERVICE_NAME_LEN_MAX_DEFAULT, + VERIFY_TIMEOUT_DEFAULT, +}; +pub use service_info::{ + AsIpAddrs, + IntoTxtProperties, + ResolvedService, + ServiceInfo, + TxtProperties, + TxtProperty, +}; + +/// Returns the current time in milliseconds since the UNIX epoch. +pub(crate) fn current_time_millis() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("failed to get current UNIX time") + .as_millis() as u64 +} diff --git a/vendor/mdns-sd/src/service_daemon.rs b/vendor/mdns-sd/src/service_daemon.rs new file mode 100644 index 0000000..7a044b8 --- /dev/null +++ b/vendor/mdns-sd/src/service_daemon.rs @@ -0,0 +1,6681 @@ +//! Service daemon for mDNS Service Discovery. + +// How DNS-based Service Discovery works in a nutshell: +// +// (excerpt from RFC 6763) +// .... that a particular service instance can be +// described using a DNS SRV [RFC2782] and DNS TXT [RFC1035] record. +// The SRV record has a name of the form ".." +// and gives the target host and port where the service instance can be +// reached. The DNS TXT record of the same name gives additional +// information about this instance, in a structured form using key/value +// pairs, described in Section 6. A client discovers the list of +// available instances of a given service type using a query for a DNS +// PTR [RFC1035] record with a name of the form ".", +// which returns a set of zero or more names, which are the names of the +// aforementioned DNS SRV/TXT record pairs. +// +// Some naming conventions in this source code: +// +// `ty_domain` refers to service type together with domain name, i.e. .. +// Every consists of two labels: service itself and "_udp." or "_tcp". +// See RFC 6763 section 7 Service Names. +// for example: `_my-service._udp.local.` +// +// `fullname` refers to a full Service Instance Name, i.e. .. +// for example: `my_home._my-service._udp.local.` +// +// In mDNS and DNS, the basic data structure is "Resource Record" (RR), where +// in Service Discovery, the basic data structure is "Service Info". One Service Info +// corresponds to a set of DNS Resource Records. +use std::{ + cmp::{self, Reverse}, + collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet}, + fmt, + io, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket}, + str, + thread, + time::Duration, + vec, +}; + +use flume::{bounded, Sender, TrySendError}; +use if_addrs::{IfAddr, Interface}; +use mio::{event::Source, net::UdpSocket as MioUdpSocket, Interest, Poll, Registry, Token}; +use socket2::Domain; +use socket_pktinfo::PktInfoUdpSocket; + +#[cfg(feature = "logging")] +use crate::log::{debug, error, trace}; +use crate::{ + current_time_millis, + dns_cache::{DnsCache, IpType}, + dns_parser::{ + ip_address_rr_type, + max_pkt_absolute, + DnsAddress, + DnsEntryExt, + DnsIncoming, + DnsOutgoing, + DnsPointer, + DnsRecordBox, + DnsRecordExt, + DnsSrv, + DnsTxt, + InterfaceId, + RRType, + ScopedIp, + CLASS_CACHE_FLUSH, + CLASS_IN, + FLAGS_AA, + FLAGS_QR_QUERY, + FLAGS_QR_RESPONSE, + MAX_PKT_ABSOLUTE_IPV6, + MAX_PKT_DEFAULT, + }, + error::{e_fmt, Error, Result}, + service_info::{ + valid_ip_on_intf, + DnsRegistry, + MyIntf, + Probe, + ServiceInfo, + ServiceStatus, + MULTICAST_RATE_LIMIT_MILLIS, + }, + Receiver, + ResolvedService, + TxtProperties, +}; + +/// 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; + +/// The default interval for checking IP changes automatically. +pub const IP_CHECK_INTERVAL_IN_SECS_DEFAULT: u32 = 5; + +/// The default time out for [ServiceDaemon::verify] is 10 seconds, per +/// [RFC 6762 section 10.4](https://datatracker.ietf.org/doc/html/rfc6762#section-10.4) +pub const VERIFY_TIMEOUT_DEFAULT: Duration = Duration::from_secs(10); + +/// The smallest value accepted by [`ServiceDaemon::set_max_packet_size`]. +pub(crate) const MIN_MAX_PACKET_SIZE: usize = 512; + +/// The mDNS port number per RFC 6762. +pub const MDNS_PORT: u16 = 5353; + +const GROUP_ADDR_V4: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); +const GROUP_ADDR_V6: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0xfb); +const LOOPBACK_V4: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); + +const RESOLVE_WAIT_IN_MILLIS: u64 = 500; +const MAX_TIMERS: usize = 4_096; +const MAX_POLL_SLEEP: Duration = Duration::from_secs(1); + +/// RFC 6762 §8.3: the two unsolicited announcements are sent "one second apart". +/// We schedule the second one strictly wider than the §6 multicast rate-limit +/// window ([`MULTICAST_RATE_LIMIT_MILLIS`]) so that scheduling skew — the few +/// millis between capturing this base time and actually stamping the records as +/// multicast — can never make the rate limit throttle the second announcement +/// away. A small random jitter is added on top (see `ANNOUNCE_SECOND_JITTER_MILLIS`) +/// to de-synchronize announcements across hosts and services. +const ANNOUNCE_SECOND_DELAY_MILLIS: u64 = MULTICAST_RATE_LIMIT_MILLIS + 100; + +/// Upper bound (exclusive) of the random jitter added to the second announcement +/// delay. Kept small so the spacing stays close to the RFC's "one second". +const ANNOUNCE_SECOND_JITTER_MILLIS: u64 = 50; + +// The §8.3 announcement spacing MUST stay strictly wider than the §6 rate-limit +// window, or the rate limit throttles the second announcement away (leaving only +// one unsolicited response). Enforced at compile time so the two can't drift. +#[allow(clippy::assertions_on_constants)] +const _: () = assert!(ANNOUNCE_SECOND_DELAY_MILLIS > MULTICAST_RATE_LIMIT_MILLIS); + +/// RFC 6762 §6: +/// In any case where there may be multiple responses, such as queries +/// where the answer is a member of a shared resource record set, each +/// responder SHOULD delay its response by a random amount of time +/// selected with uniform random distribution in the range 20-120 ms. +/// +/// 20ms suggested in the RFC is a bit too long for min. Use 10ms instead. +const SHARED_RESPONSE_DELAY_MIN_MILLIS: u64 = 10; + +/// 120ms suggested in the RFC is too long for max, use 50ms instead. +const SHARED_RESPONSE_DELAY_MAX_MILLIS: u64 = 50; + +/// RFC 6762 §5.2: to avoid accidental synchronization when multiple clients +/// begin querying at exactly the same moment (e.g. because of some common +/// external trigger event), a querier SHOULD delay the first query of a +/// continuous-monitoring series by a randomly chosen amount in the range +/// 20-120 ms. +/// +/// Like the responder delay above, we use a shorter 10-50 ms window. +const INITIAL_QUERY_DELAY_MIN_MILLIS: u64 = 10; +const INITIAL_QUERY_DELAY_MAX_MILLIS: u64 = 50; + +/// Response status code for the service `unregister` call. +#[derive(Debug)] +pub enum UnregisterStatus { + /// Unregister was successful. + OK, + /// The service was not found in the registration. + NotFound, +} + +/// Status code for the service daemon. +#[derive(Debug, PartialEq, Clone, Eq)] +#[non_exhaustive] +pub enum DaemonStatus { + /// The daemon is running as normal. + Running, + + /// The daemon has been shutdown. + Shutdown, +} + +/// Different counters included in the metrics. +/// Currently all counters are for outgoing packets. +#[derive(Hash, Eq, PartialEq)] +enum Counter { + Register, + RegisterResend, + Unregister, + UnregisterResend, + Browse, + ResolveHostname, + Respond, + CacheRefreshPTR, + CacheRefreshSrvTxt, + CacheRefreshAddr, + KnownAnswerSuppression, + CachedPTR, + CachedSRV, + CachedAddr, + CachedTxt, + CachedNSec, + CachedSubtype, + DnsRegistryProbe, + DnsRegistryActive, + DnsRegistryTimer, + DnsRegistryNameChange, + Timer, +} + +impl fmt::Display for Counter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Register => write!(f, "register"), + Self::RegisterResend => write!(f, "register-resend"), + Self::Unregister => write!(f, "unregister"), + Self::UnregisterResend => write!(f, "unregister-resend"), + Self::Browse => write!(f, "browse"), + Self::ResolveHostname => write!(f, "resolve-hostname"), + Self::Respond => write!(f, "respond"), + Self::CacheRefreshPTR => write!(f, "cache-refresh-ptr"), + Self::CacheRefreshSrvTxt => write!(f, "cache-refresh-srv-txt"), + Self::CacheRefreshAddr => write!(f, "cache-refresh-addr"), + Self::KnownAnswerSuppression => write!(f, "known-answer-suppression"), + Self::CachedPTR => write!(f, "cached-ptr"), + Self::CachedSRV => write!(f, "cached-srv"), + Self::CachedAddr => write!(f, "cached-addr"), + Self::CachedTxt => write!(f, "cached-txt"), + Self::CachedNSec => write!(f, "cached-nsec"), + Self::CachedSubtype => write!(f, "cached-subtype"), + Self::DnsRegistryProbe => write!(f, "dns-registry-probe"), + Self::DnsRegistryActive => write!(f, "dns-registry-active"), + Self::DnsRegistryTimer => write!(f, "dns-registry-timer"), + Self::DnsRegistryNameChange => write!(f, "dns-registry-name-change"), + Self::Timer => write!(f, "timer"), + } + } +} + +#[derive(Debug)] +enum InternalError { + IntfAddrInvalid(Interface), +} + +impl fmt::Display for InternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InternalError::IntfAddrInvalid(iface) => write!(f, "interface addr invalid: {iface:?}"), + } + } +} + +type MyResult = core::result::Result; + +/// A wrapper around UDP socket used by the mDNS daemon. +/// +/// We do this because `mio` does not support PKTINFO and +/// does not provide a way to implement `Source` trait directly and safely. +struct MyUdpSocket { + /// The underlying socket that supports control messages like + /// `IP_PKTINFO` for IPv4 and `IPV6_PKTINFO` for IPv6. + pktinfo: PktInfoUdpSocket, + + /// The mio UDP socket that is a clone of `pktinfo` and + /// is used for event polling. + mio: MioUdpSocket, +} + +impl MyUdpSocket { + pub fn new(pktinfo: PktInfoUdpSocket) -> io::Result { + let std_sock = pktinfo.try_clone_std()?; + let mio = MioUdpSocket::from_std(std_sock); + + Ok(Self { pktinfo, mio }) + } +} + +/// Implements the mio `Source` trait so that we can use `MyUdpSocket` with `Poll`. +impl Source for MyUdpSocket { + fn register( + &mut self, + registry: &Registry, + token: Token, + interests: Interest, + ) -> io::Result<()> { + self.mio.register(registry, token, interests) + } + + fn reregister( + &mut self, + registry: &Registry, + token: Token, + interests: Interest, + ) -> io::Result<()> { + self.mio.reregister(registry, token, interests) + } + + fn deregister(&mut self, registry: &Registry) -> std::io::Result<()> { + self.mio.deregister(registry) + } +} + +/// The metrics is a HashMap of (name_key, i64_value). +/// The main purpose is to help monitoring the mDNS packet traffic. +pub type Metrics = HashMap; + +const IPV4_SOCK_EVENT_KEY: usize = 4; // Pick a key just to indicate IPv4. +const IPV6_SOCK_EVENT_KEY: usize = 6; // Pick a key just to indicate IPv6. +const SIGNAL_SOCK_EVENT_KEY: usize = usize::MAX - 1; // avoid to overlap with zc.poll_ids + +/// A daemon thread for mDNS +/// +/// This struct provides a handle and an API to the daemon. It is cloneable. +#[derive(Clone)] +pub struct ServiceDaemon { + /// Sender handle of the channel to the daemon. + sender: Sender, + + /// Send to this addr to signal that a `Command` is coming. + /// + /// The daemon listens on this addr together with other mDNS sockets, + /// to avoid busy polling the flume channel. If there is a way to poll + /// the channel and mDNS sockets together, then this can be removed. + signal_addr: SocketAddr, +} + +impl ServiceDaemon { + /// Creates a new daemon and spawns a thread to run the daemon. + /// + /// Creates a new mDNS service daemon using the default port (5353). + /// + /// For development/testing with custom ports, use [`ServiceDaemon::new_with_port`]. + /// + /// # Errors + /// + /// Returns [`Error::Msg`] if the daemon cannot be initialized. This wraps an + /// underlying OS-level failure. + /// + /// Note that this constructor does not open the mDNS multicast sockets — those + /// are opened lazily by the daemon thread once it starts, so platform issues + /// such as "multicast not permitted" are surfaced later via [`DaemonEvent`] from + /// [`monitor`](Self::monitor) rather than here. + pub fn new() -> Result { + Self::new_with_port(MDNS_PORT) + } + + /// Creates a new mDNS service daemon using a custom port. + /// + /// # Arguments + /// + /// * `port` - The UDP port to bind for mDNS communication. + /// - In production, this should be `MDNS_PORT` (5353) per RFC 6762. + /// - For development/testing, you can use a non-standard port (e.g., 5454) + /// to avoid conflicts with system mDNS services. + /// - Both publisher and browser must use the same port to communicate. + /// + /// # Example + /// + /// ```no_run + /// use mdns_sd::ServiceDaemon; + /// + /// // Use standard mDNS port (production) + /// let daemon = ServiceDaemon::new_with_port(5353)?; + /// + /// // Use custom port for development (avoids macOS Bonjour conflict) + /// let daemon_dev = ServiceDaemon::new_with_port(5454)?; + /// # Ok::<(), mdns_sd::Error>(()) + /// ``` + /// + /// # Errors + /// + /// See [`new`](Self::new) for the set of OS-level failures that may surface + /// here. Note that `port` is *not* validated against the kernel until the + /// daemon thread tries to bind the mDNS sockets, so an unusable `port` + /// (e.g., already in use, requires elevated privileges) will not be + /// reported by this constructor — listen for such failures via + /// [`monitor`](Self::monitor). + pub fn new_with_port(port: u16) -> Result { + // Use port 0 to allow the system assign a random available port, + // no need for a pre-defined port number. + let signal_addr = SocketAddrV4::new(LOOPBACK_V4, 0); + + let signal_sock = UdpSocket::bind(signal_addr) + .map_err(|e| e_fmt!("failed to create signal_sock for daemon: {}", e))?; + + // Get the socket with the OS chosen port + let signal_addr = signal_sock + .local_addr() + .map_err(|e| e_fmt!("failed to get signal sock addr: {}", e))?; + + // Must be nonblocking so we can listen to it together with mDNS sockets. + signal_sock + .set_nonblocking(true) + .map_err(|e| e_fmt!("failed to set nonblocking for signal socket: {}", e))?; + + let poller = Poll::new().map_err(|e| e_fmt!("failed to create mio Poll: {e}"))?; + + let (sender, receiver) = bounded(100); + + // Spawn the daemon thread + let mio_sock = MioUdpSocket::from_std(signal_sock); + let cmd_sender = sender.clone(); + thread::Builder::new() + .name("mDNS_daemon".to_string()) + .spawn(move || { + Self::daemon_thread(mio_sock, poller, receiver, port, cmd_sender, signal_addr) + }) + .map_err(|e| e_fmt!("thread builder failed to spawn: {}", e))?; + + Ok(Self { + sender, + signal_addr, + }) + } + + /// Sends `cmd` to the daemon via its channel, and sends a signal + /// to its sock addr to notify. + fn send_cmd(&self, cmd: Command) -> Result<()> { + let cmd_name = cmd.to_string(); + + // First, send to the flume channel. + self.sender.try_send(cmd).map_err(|e| match e { + TrySendError::Full(_) => Error::Again, + TrySendError::Disconnected(_) => Error::DaemonShutdown, + })?; + + // Second, send a signal to notify the daemon. + let addr = SocketAddrV4::new(LOOPBACK_V4, 0); + let socket = UdpSocket::bind(addr) + .map_err(|e| e_fmt!("Failed to create socket to send signal: {}", e))?; + socket + .send_to(cmd_name.as_bytes(), self.signal_addr) + .map_err(|e| { + e_fmt!( + "signal socket send_to {} ({}) failed: {}", + self.signal_addr, + cmd_name, + e + ) + })?; + + Ok(()) + } + + /// Starts browsing for a specific service type. + /// + /// `service_type` must end with a valid mDNS domain: '._tcp.local.' or '._udp.local.' + /// + /// Returns a channel `Receiver` to receive events about the service. The caller + /// can call `.recv_async().await` on this receiver to handle events in an + /// async environment or call `.recv()` in a sync environment. + /// + /// When a new instance is found, the daemon automatically tries to resolve, i.e. + /// finding more details, i.e. SRV records and TXT records. + /// + /// # Errors + /// + /// Returns [`Error::Msg`] if `service_type` does not end with + /// `._tcp.local.` or `._udp.local.`. + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn browse(&self, service_type: &str) -> Result> { + check_domain_suffix(service_type)?; + + let (resp_s, resp_r) = bounded(10); + self.send_cmd(Command::Browse(service_type.to_string(), 1, false, resp_s))?; + Ok(resp_r) + } + + /// Preforms a "cache-only" browse. + /// + /// `service_type` must end with a valid mDNS domain: '._tcp.local.' or '._udp.local.' + /// + /// The functionality is identical to 'browse', but the service events are based solely on the contents + /// of the daemon's cache. No actual mDNS query is sent to the network. + /// + /// See [accept_unsolicited](Self::accept_unsolicited) if you want to do cache-only browsing. + /// + /// # Errors + /// + /// Same error conditions as [`browse`](Self::browse). + pub fn browse_cache(&self, service_type: &str) -> Result> { + check_domain_suffix(service_type)?; + + let (resp_s, resp_r) = bounded(10); + self.send_cmd(Command::Browse(service_type.to_string(), 1, true, resp_s))?; + Ok(resp_r) + } + + /// Stops searching for a specific service type. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn stop_browse(&self, ty_domain: &str) -> Result<()> { + self.send_cmd(Command::StopBrowse(ty_domain.to_string())) + } + + /// Starts querying for the ip addresses of a hostname. + /// + /// Returns a channel `Receiver` to receive events about the hostname. + /// The caller can call `.recv_async().await` on this receiver to handle events in an + /// async environment or call `.recv()` in a sync environment. + /// + /// The `timeout` is specified in milliseconds. + /// + /// # Errors + /// + /// Returns [`Error::Msg`] if: + /// + /// - `hostname` does not end with `.local.`; + /// - `hostname` is exactly `.local.` (the label before `.local.` is empty); + /// - `hostname` is longer than 255 bytes. + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn resolve_hostname( + &self, + hostname: &str, + timeout: Option, + ) -> Result> { + check_hostname(hostname)?; + let (resp_s, resp_r) = bounded(10); + self.send_cmd(Command::ResolveHostname( + hostname.to_string(), + 1, + resp_s, + timeout, + ))?; + Ok(resp_r) + } + + /// Stops querying for the ip addresses of a hostname. + /// + /// # Errors + /// + /// Same error conditions as [`stop_browse`](Self::stop_browse). + pub fn stop_resolve_hostname(&self, hostname: &str) -> Result<()> { + self.send_cmd(Command::StopResolveHostname(hostname.to_string())) + } + + /// Registers a service provided by this host. + /// + /// If `service_info` has no addresses yet and its `addr_auto` is enabled, + /// this method will automatically fill in addresses from the host. + /// + /// To re-announce a service with an updated `service_info`, just call + /// this `register` function again. No need to call `unregister` first. + /// + /// # Errors + /// + /// Returns [`Error::Msg`] if the [`ServiceInfo`] is malformed, for example: + /// + /// - the fullname does not end with `._tcp.local.` or `._udp.local.`; + /// - the hostname does not end with `.local.`, is exactly `.local.`, or + /// is longer than 255 bytes. + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn register(&self, service_info: ServiceInfo) -> Result<()> { + check_service_name(service_info.get_fullname())?; + check_hostname(service_info.get_hostname())?; + + self.send_cmd(Command::Register(service_info.into())) + } + + /// Unregisters a service. This is a graceful shutdown of a service. + /// + /// Returns a channel receiver that is used to receive the status code + /// of the unregister. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn unregister(&self, fullname: &str) -> Result> { + let (resp_s, resp_r) = bounded(1); + self.send_cmd(Command::Unregister(fullname.to_lowercase(), resp_s))?; + Ok(resp_r) + } + + /// Starts to monitor events from the daemon. + /// + /// Returns a channel [`Receiver`] of [`DaemonEvent`]. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn monitor(&self) -> Result> { + let (resp_s, resp_r) = bounded(100); + self.send_cmd(Command::Monitor(resp_s))?; + Ok(resp_r) + } + + /// Shuts down the daemon thread and returns a channel to receive the status. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn shutdown(&self) -> Result> { + let (resp_s, resp_r) = bounded(1); + self.send_cmd(Command::Exit(resp_s))?; + Ok(resp_r) + } + + /// Returns the status of the daemon. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn status(&self) -> Result> { + let (resp_s, resp_r) = bounded(1); + + if self.sender.is_disconnected() { + resp_s + .send(DaemonStatus::Shutdown) + .map_err(|e| e_fmt!("failed to send daemon status to the client: {}", e))?; + } else { + self.send_cmd(Command::GetStatus(resp_s))?; + } + + Ok(resp_r) + } + + /// Returns a channel receiver for the metrics, e.g. input/output counters. + /// + /// The metrics returned is a snapshot. Hence the caller should call + /// this method repeatedly if they want to monitor the metrics continuously. + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn get_metrics(&self) -> Result> { + let (resp_s, resp_r) = bounded(1); + self.send_cmd(Command::GetMetrics(resp_s))?; + 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. + /// + /// # Errors + /// + /// Returns [`Error::Msg`] if `len_max` exceeds the internal cap (30). + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + 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 {len_max} is too large" + ))); + } + + self.send_cmd(Command::SetOption(DaemonOption::ServiceNameLenMax(len_max))) + } + + /// Change the max byte size of a packet this daemon generates on the interfaces + /// matching `if_kind`. Use `IfKind::All` to change it on every interface. Messages + /// that don't fit are split across multiple packets. A single record that doesn't + /// fit in a packet is sent alone in a packet of up to 8952 bytes over IPv6 or 8972 + /// bytes over IPv4, per RFC 6762 section 17. + /// + /// The default is `MAX_PKT_DEFAULT` (1452 bytes), small enough to fit in one + /// Ethernet frame over either IPv4 or IPv6. + /// + /// `size` must be in the range `512..=8952`. The minimum of 512 bytes is the classic + /// UDP DNS message size of RFC 1035. The maximum of 8952 bytes follows from RFC 6762 + /// section 17, which caps an mDNS packet at 9000 bytes: we subtract the + /// bigger of the two IP headers so that a generated packet is legal over either + /// IP version. + pub fn set_max_packet_size(&self, if_kind: impl IntoIfKindVec, size: usize) -> Result<()> { + if size < MIN_MAX_PACKET_SIZE { + return Err(Error::Msg(format!( + "max packet size {size} is too small, must be at least {MIN_MAX_PACKET_SIZE}" + ))); + } + + if size > MAX_PKT_ABSOLUTE_IPV6 { + return Err(Error::Msg(format!( + "max packet size {size} is too big, must be at most {MAX_PKT_ABSOLUTE_IPV6}" + ))); + } + + let if_kind_vec = if_kind.into_vec(); + self.send_cmd(Command::SetOption(DaemonOption::MaxPacketSize( + if_kind_vec.kinds, + size, + ))) + } + + /// Change the interval for checking IP changes automatically. + /// + /// Setting the interval to 0 disables the IP check. + /// + /// See [`IP_CHECK_INTERVAL_IN_SECS_DEFAULT`] for the default interval. + pub fn set_ip_check_interval(&self, interval_in_secs: u32) -> Result<()> { + let interval_in_millis = interval_in_secs as u64 * 1000; + self.send_cmd(Command::SetOption(DaemonOption::IpCheckInterval( + interval_in_millis, + ))) + } + + /// Get the current interval in seconds for checking IP changes automatically. + pub fn get_ip_check_interval(&self) -> Result { + let (resp_s, resp_r) = bounded(1); + self.send_cmd(Command::GetOption(resp_s))?; + + let option = resp_r + .recv_timeout(Duration::from_secs(10)) + .map_err(|e| e_fmt!("failed to receive ip check interval: {}", e))?; + let ip_check_interval_in_secs = option.ip_check_interval / 1000; + Ok(ip_check_interval_in_secs as u32) + } + + /// 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, + ))) + } + + /// If `accept` is true, accept and cache all responses, even if there is no active querier + /// for a given service type. This is useful / necessary when doing cache-only browsing. See + /// [browse_cache](Self::browse_cache). + /// + /// If `accept` is false (default), accept only responses matching queries that we have initiated. + /// + /// For example: + /// ```ignore + /// daemon.accept_unsolicited(true)?; + /// ``` + pub fn accept_unsolicited(&self, accept: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::AcceptUnsolicited(accept))) + } + + /// Include or exclude Apple P2P interfaces, e.g. "awdl0", "llw0". + /// By default, they are excluded. + pub fn include_apple_p2p(&self, include: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::IncludeAppleP2P(include))) + } + + #[cfg(test)] + pub fn test_down_interface(&self, ifname: &str) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::TestDownInterface( + ifname.to_string(), + ))) + } + + #[cfg(test)] + pub fn test_up_interface(&self, ifname: &str) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::TestUpInterface( + ifname.to_string(), + ))) + } + + /// Enable or disable the loopback for locally sent multicast packets in IPv4. + /// + /// By default, multicast loop is enabled for IPv4. When disabled, a querier will not + /// receive announcements from a responder on the same host. + /// + /// Reference: + /// + /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than + /// the UNIX version of the IP_MULTICAST_LOOP option: + /// + /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. + /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." + /// + /// Which means, in order NOT to receive localhost announcements, you want to call + /// this API on the querier side on Windows, but on the responder side on Unix. + pub fn set_multicast_loop_v4(&self, on: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV4(on))) + } + + /// Enable or disable the loopback for locally sent multicast packets in IPv6. + /// + /// By default, multicast loop is enabled for IPv6. When disabled, a querier will not + /// receive announcements from a responder on the same host. + /// + /// Reference: + /// + /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than + /// the UNIX version of the IP_MULTICAST_LOOP option: + /// + /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. + /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." + /// + /// Which means, in order NOT to receive localhost announcements, you want to call + /// this API on the querier side on Windows, but on the responder side on Unix. + pub fn set_multicast_loop_v6(&self, on: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV6(on))) + } + + /// Proactively confirms whether a service instance still valid. + /// + /// This call will issue queries for a service instance's SRV record and Address records. + /// + /// For `timeout`, most users should use [VERIFY_TIMEOUT_DEFAULT] + /// unless there is a reason not to follow RFC. + /// + /// If no response is received within `timeout`, the current resource + /// records will be flushed, and if needed, `ServiceRemoved` event will be + /// sent to active queriers. + /// + /// Reference: [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-10.4) + /// + /// # Errors + /// + /// Returns [`Error::Again`] if the daemon's command queue is full. + /// + /// Returns [`Error::DaemonShutdown`] if the daemon thread has already exited. + pub fn verify(&self, instance_fullname: String, timeout: Duration) -> Result<()> { + self.send_cmd(Command::Verify(instance_fullname, timeout)) + } + + fn daemon_thread( + signal_sock: MioUdpSocket, + poller: Poll, + receiver: Receiver, + port: u16, + cmd_sender: Sender, + signal_addr: SocketAddr, + ) { + let mut zc = Zeroconf::new(signal_sock, poller, port, cmd_sender, signal_addr); + + if let Some(cmd) = zc.run(receiver) { + match cmd { + Command::Exit(resp_s) => { + // It is guaranteed that the receiver already dropped, + // i.e. the daemon command channel closed. + if let Err(e) = resp_s.send(DaemonStatus::Shutdown) { + debug!("exit: failed to send response of shutdown: {}", e); + } + } + _ => { + debug!("Unexpected command: {:?}", cmd); + } + } + } + } +} + +/// Creates a new UDP socket that uses `intf` to send and recv multicast. +fn _new_socket_bind(intf: &Interface, should_loop: bool) -> Result { + // Use the same socket for receiving and sending multicast packets. + // Such socket has to bind to INADDR_ANY or IN6ADDR_ANY. + let intf_ip = &intf.ip(); + match intf_ip { + IpAddr::V4(ip) => { + let addr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), MDNS_PORT); + let sock = new_socket(addr.into(), true)?; + + // Join mDNS group to receive packets. + sock.join_multicast_v4(&GROUP_ADDR_V4, ip) + .map_err(|e| e_fmt!("join multicast group on addr {}: {}", intf_ip, e))?; + + // Set IP_MULTICAST_IF to send packets. + sock.set_multicast_if_v4(ip) + .map_err(|e| e_fmt!("set multicast_if on addr {}: {}", ip, e))?; + + // Per RFC 6762 section 11: + // "All Multicast DNS responses (including responses sent via unicast) SHOULD + // be sent with IP TTL set to 255." + // Here we set the TTL to 255 for multicast as we don't support unicast yet. + sock.set_multicast_ttl_v4(255) + .map_err(|e| e_fmt!("set set_multicast_ttl_v4 on addr {}: {}", ip, e))?; + + if !should_loop { + sock.set_multicast_loop_v4(false) + .map_err(|e| e_fmt!("failed to set multicast loop v4 for {ip}: {e}"))?; + } + + // Test if we can send packets successfully. + let multicast_addr = SocketAddrV4::new(GROUP_ADDR_V4, MDNS_PORT).into(); + let test_packets = DnsOutgoing::new(0).to_data_on_wire(MAX_PKT_DEFAULT, true); + for packet in test_packets { + sock.send_to(&packet, &multicast_addr) + .map_err(|e| e_fmt!("send multicast packet on addr {}: {}", ip, e))?; + } + MyUdpSocket::new(sock) + .map_err(|e| e_fmt!("failed to create MySocket for interface {}: {e}", intf.name)) + } + IpAddr::V6(ip) => { + let addr = SocketAddrV6::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), MDNS_PORT, 0, 0); + let sock = new_socket(addr.into(), true)?; + + let if_index = intf.index.unwrap_or(0); + + // Join mDNS group to receive packets. + sock.join_multicast_v6(&GROUP_ADDR_V6, if_index) + .map_err(|e| e_fmt!("join multicast group on addr {}: {}", ip, e))?; + + // Set IPV6_MULTICAST_IF to send packets. + sock.set_multicast_if_v6(if_index) + .map_err(|e| e_fmt!("set multicast_if 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)". + + MyUdpSocket::new(sock) + .map_err(|e| e_fmt!("failed to create MySocket for interface {}: {e}", intf.name)) + } + } +} + +/// Creates a new UDP socket to bind to `port` with REUSEPORT option. +/// `non_block` indicates whether to set O_NONBLOCK for the socket. +fn new_socket(addr: SocketAddr, non_block: bool) -> Result { + let domain = match addr { + SocketAddr::V4(_) => socket2::Domain::IPV4, + SocketAddr::V6(_) => socket2::Domain::IPV6, + }; + + let fd = PktInfoUdpSocket::new(domain).map_err(|e| e_fmt!("create socket failed: {}", e))?; + + fd.set_reuse_address(true) + .map_err(|e| e_fmt!("set ReuseAddr failed: {}", e))?; + #[cfg(unix)] + if let Err(e) = fd.set_reuse_port(true) { + debug!( + "SO_REUSEPORT is not supported, continuing without it: {}", + e + ); + } + + if non_block { + fd.set_nonblocking(true) + .map_err(|e| e_fmt!("set O_NONBLOCK: {}", e))?; + } + + fd.bind(&addr.into()) + .map_err(|e| e_fmt!("socket bind to {} failed: {}", &addr, e))?; + + trace!("new socket bind to {}", &addr); + Ok(fd) +} + +/// Specify a UNIX timestamp in millis to run `command` for the next time. +struct ReRun { + /// UNIX timestamp in millis. + next_time: u64, + command: Command, +} + +/// A query response deferred per RFC 6762 §6 (shared response). +struct DelayedResponse { + /// UNIX timestamp in millis at which to send `out`. + next_time: u64, + out: DnsOutgoing, + if_index: u32, + is_ipv4: bool, +} + +/// 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. + /// This is used to look up the interface. The semantics is to identify an interface of + /// IPv4 or IPv6, not a specific address on the interface. + Addr(IpAddr), + + /// 127.0.0.1 (or anything in 127.0.0.0/8), enabled by default. + /// + /// Loopback interfaces are required by some use cases (e.g., OSCQuery) for publishing. + LoopbackV4, + + /// ::1/128, enabled by default. + LoopbackV6, + + /// By interface index, IPv4 only. + IndexV4(u32), + + /// By interface index, IPv6 only. + IndexV6(u32), + + /// By a user-supplied predicate function. + Predicate(IfPredicate), +} + +impl IfKind { + /// Checks if `intf` matches with this interface kind. + pub(crate) fn matches(&self, intf: &Interface) -> bool { + match self { + Self::All => true, + Self::IPv4 => intf.ip().is_ipv4(), + Self::IPv6 => intf.ip().is_ipv6(), + Self::Name(ifname) => ifname == &intf.name, + Self::Addr(addr) => addr == &intf.ip(), + Self::LoopbackV4 => intf.is_loopback() && intf.ip().is_ipv4(), + Self::LoopbackV6 => intf.is_loopback() && intf.ip().is_ipv6(), + Self::IndexV4(idx) => intf.index == Some(*idx) && intf.ip().is_ipv4(), + Self::IndexV6(idx) => intf.index == Some(*idx) && intf.ip().is_ipv6(), + Self::Predicate(p) => p.matches(intf), + } + } +} + +/// 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) -> Self { + Self::Name(val.to_string()) + } +} + +impl From<&String> for IfKind { + fn from(val: &String) -> Self { + Self::Name(val.to_string()) + } +} + +/// Still for ergonomic reasons. +impl From for IfKind { + fn from(val: IpAddr) -> Self { + Self::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 } + } +} + +/// A predicate function for matching against interfaces. +#[derive(Clone)] +pub struct IfPredicate(std::sync::Arc bool + Send + Sync>); + +impl IfPredicate { + /// Creates a predicate from a closure that decides whether an interface + /// matches. + /// + /// # Example + /// + /// ```no_run + /// # use mdns_sd::IfPredicate; + /// // Match any interface that doesn't look like a virtual bridge + /// IfPredicate::new(|intf| !intf.name.starts_with("virbr")); + /// ``` + pub fn new(predicate: impl Fn(&Interface) -> bool + Send + Sync + 'static) -> Self { + Self(std::sync::Arc::new(predicate)) + } + + pub(crate) fn matches(&self, intf: &Interface) -> bool { + self.0(intf) + } +} + +impl std::fmt::Debug for IfPredicate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "IfPredicate(...)") + } +} + +/// Selection of interfaces. +struct IfSelection { + /// The interfaces to be selected. + if_kind: IfKind, + + /// Whether the `if_kind` should be enabled or not. + selected: bool, +} + +/// Selection of the max packet size of interfaces. +struct MaxPacketSizeSelection { + /// The interfaces to be selected. + if_kind: IfKind, + + /// Max byte size of a packet generated for the selected interfaces. + max_packet_size: usize, +} + +/// A struct holding the state. It was inspired by `zeroconf` package in Python. +struct Zeroconf { + /// The mDNS port number to use for socket binding. + /// Typically MDNS_PORT (5353), but can be customized for development/testing. + port: u16, + + /// Local interfaces keyed by interface index. + my_intfs: HashMap, + + /// A common socket for IPv4 interfaces. It's None if IPv4 is disabled in OS kernel. + ipv4_sock: Option, + + /// A common socket for IPv6 interfaces. It's None if IPv6 is disabled in OS kernel. + ipv6_sock: Option, + + /// Local registered services, keyed by service full names. + my_services: HashMap, + + /// Received DNS records. + cache: DnsCache, + + /// Registered service records, keyed by interface index. + dns_registry_map: HashMap, + + /// Active "Browse" commands. + service_queriers: HashMap>, // + + /// Active "ResolveHostname" commands. + /// + /// The timestamps are set at the future timestamp when the command should timeout. + /// `hostname` is case-insensitive and stored in lowercase. + hostname_resolvers: HashMap, Option)>, // + + /// All repeating transmissions. + retransmissions: Vec, + + /// Query responses deferred per RFC 6762 §6. + delayed_responses: Vec, + + counters: Metrics, + + /// Waits for incoming packets. + poller: Poll, + + /// Channels to notify events. + monitors: Vec>, + + /// Options + service_name_len_max: u8, + + /// Interval in millis to check IP address changes. + ip_check_interval: u64, + + /// All max packet size selections called to the daemon, in call order. + /// For an interface matched by more than one, the last one wins. + max_packet_sizes: Vec, + + /// All interface selections called to the daemon. + if_selections: Vec, + + /// Socket for signaling. + signal_sock: MioUdpSocket, + + /// Timestamps marking where we need another iteration of the run loop, + /// to react to events like retransmissions, cache refreshes, interface IP address changes, etc. + /// + /// When the run loop goes through a single iteration, it will + /// set its timeout to the earliest timer in this list. + timers: BinaryHeap>, + + status: DaemonStatus, + + /// Service instances that are pending for resolving SRV and TXT. + pending_resolves: HashSet, + + /// Service instances that are already resolved. + resolved: HashSet, + + multicast_loop_v4: bool, + + multicast_loop_v6: bool, + + accept_unsolicited: bool, + + include_apple_p2p: bool, + + cmd_sender: Sender, + + signal_addr: SocketAddr, + + #[cfg(test)] + test_down_interfaces: HashSet, +} + +/// Join the multicast group for the given interface. +fn join_multicast_group(my_sock: &PktInfoUdpSocket, intf: &Interface) -> Result<()> { + let intf_ip = &intf.ip(); + match intf_ip { + IpAddr::V4(ip) => { + // Join mDNS group to receive packets. + debug!("join multicast group V4 on {} addr {ip}", intf.name); + my_sock + .join_multicast_v4(&GROUP_ADDR_V4, ip) + .map_err(|e| e_fmt!("PKT join multicast group on addr {}: {}", intf_ip, e))?; + } + IpAddr::V6(ip) => { + let if_index = intf.index.unwrap_or(0); + // Join mDNS group to receive packets. + debug!( + "join multicast group V6 on {} addr {ip} with index {if_index}", + intf.name + ); + my_sock + .join_multicast_v6(&GROUP_ADDR_V6, if_index) + .map_err(|e| e_fmt!("PKT join multicast group on addr {}: {}", ip, e))?; + } + } + Ok(()) +} + +impl Zeroconf { + fn new( + signal_sock: MioUdpSocket, + poller: Poll, + port: u16, + cmd_sender: Sender, + signal_addr: SocketAddr, + ) -> Self { + // Get interfaces. + let my_ifaddrs = my_ip_interfaces(true); + + // Create a socket for every IP addr. + // Note: it is possible that `my_ifaddrs` contains the same IP addr with different interface names, + // or the same interface name with different IP addrs. + let mut my_intfs = HashMap::new(); + let mut dns_registry_map = HashMap::new(); + + // Use the same socket for receiving and sending multicast packets. + // Such socket has to bind to INADDR_ANY or IN6ADDR_ANY. + let mut ipv4_sock = None; + let addr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port); + match new_socket(addr.into(), true) { + Ok(sock) => { + // Per RFC 6762 section 11: + // "All Multicast DNS responses (including responses sent via unicast) SHOULD + // be sent with IP TTL set to 255." + // Here we set the TTL to 255 for multicast as we don't support unicast yet. + sock.set_multicast_ttl_v4(255) + .map_err(|e| e_fmt!("set set_multicast_ttl_v4 on addr: {}", e)) + .ok(); + + // This clones a socket. + ipv4_sock = match MyUdpSocket::new(sock) { + Ok(s) => Some(s), + Err(e) => { + debug!("failed to create IPv4 MyUdpSocket: {e}"); + None + } + }; + } + // Per RFC 6762 section 11:} + Err(e) => debug!("failed to create IPv4 socket: {e}"), + } + + let mut ipv6_sock = None; + let addr = SocketAddrV6::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), port, 0, 0); + match new_socket(addr.into(), true) { + Ok(sock) => { + // Per RFC 6762 section 11: + // "All Multicast DNS responses (including responses sent via unicast) SHOULD + // be sent with IP TTL set to 255." + sock.set_multicast_hops_v6(255) + .map_err(|e| e_fmt!("set set_multicast_hops_v6: {}", e)) + .ok(); + + // This clones the ipv6 socket. + ipv6_sock = match MyUdpSocket::new(sock) { + Ok(s) => Some(s), + Err(e) => { + debug!("failed to create IPv6 MyUdpSocket: {e}"); + None + } + }; + } + Err(e) => debug!("failed to create IPv6 socket: {e}"), + } + + // Configure sockets to join multicast groups. + for intf in my_ifaddrs { + let sock_opt = if intf.ip().is_ipv4() { + &ipv4_sock + } else { + &ipv6_sock + }; + let Some(sock) = sock_opt else { + debug!( + "no socket available for interface {} with addr {}. Skipped.", + intf.name, + intf.ip() + ); + continue; + }; + + if let Err(e) = join_multicast_group(&sock.pktinfo, &intf) { + debug!("failed to join multicast: {}: {e}. Skipped.", &intf.ip()); + } + + let if_index = intf.index.unwrap_or(0); + + // Add this interface address if not already present. + dns_registry_map + .entry(if_index) + .or_insert_with(DnsRegistry::new); + + my_intfs + .entry(if_index) + .and_modify(|v: &mut MyIntf| { + v.addrs.insert(intf.addr.clone()); + }) + .or_insert(MyIntf { + name: intf.name.clone(), + index: if_index, + addrs: HashSet::from([intf.addr]), + max_packet_size_v4: MAX_PKT_DEFAULT, + max_packet_size_v6: MAX_PKT_DEFAULT, + }); + } + + let monitors = Vec::new(); + let service_name_len_max = SERVICE_NAME_LEN_MAX_DEFAULT; + let ip_check_interval = IP_CHECK_INTERVAL_IN_SECS_DEFAULT as u64 * 1000; + + let timers = BinaryHeap::new(); + + // Enable everything, including loopback interfaces. + let if_selections = vec![]; + + let status = DaemonStatus::Running; + + Self { + port, + my_intfs, + ipv4_sock, + ipv6_sock, + my_services: HashMap::new(), + cache: DnsCache::new(), + dns_registry_map, + hostname_resolvers: HashMap::new(), + service_queriers: HashMap::new(), + retransmissions: Vec::new(), + delayed_responses: Vec::new(), + counters: HashMap::new(), + poller, + monitors, + service_name_len_max, + ip_check_interval, + max_packet_sizes: Vec::new(), + if_selections, + signal_sock, + timers, + status, + pending_resolves: HashSet::new(), + resolved: HashSet::new(), + multicast_loop_v4: true, + multicast_loop_v6: true, + accept_unsolicited: false, + include_apple_p2p: false, + cmd_sender, + signal_addr, + + #[cfg(test)] + test_down_interfaces: HashSet::new(), + } + } + + /// Send a Command into the daemon channel and poke the signal socket to wake the poll loop. + fn send_cmd_to_self(&self, cmd: Command) -> Result<()> { + let cmd_name = cmd.to_string(); + + self.cmd_sender.try_send(cmd).map_err(|e| match e { + TrySendError::Full(_) => Error::Again, + TrySendError::Disconnected(_) => Error::DaemonShutdown, + })?; + + let addr = SocketAddrV4::new(LOOPBACK_V4, 0); + let socket = UdpSocket::bind(addr) + .map_err(|e| e_fmt!("Failed to create socket to send signal: {}", e))?; + socket + .send_to(cmd_name.as_bytes(), self.signal_addr) + .map_err(|e| { + e_fmt!( + "signal socket send_to {} ({}) failed: {}", + self.signal_addr, + cmd_name, + e + ) + })?; + + Ok(()) + } + + /// Clean up all resources before shutdown. + /// + /// This method: + /// 1. Unregisters all registered services (sends goodbye packets) + /// 2. Stops all active browse operations + /// 3. Stops all active hostname resolution operations + /// 4. Clears all retransmissions + /// 5. Drops all pending delayed responses + fn cleanup(&mut self) { + debug!("Starting cleanup for shutdown"); + + // 1. Unregister all services - send goodbye packets + let service_names: Vec = self.my_services.keys().cloned().collect(); + for fullname in service_names { + if let Some(info) = self.my_services.get(&fullname) { + debug!("Unregistering service during shutdown: {}", &fullname); + + for intf in self.my_intfs.values() { + if let Some(sock) = self.ipv4_sock.as_ref() { + self.unregister_service(info, intf, &sock.pktinfo); + } + + if let Some(sock) = self.ipv6_sock.as_ref() { + self.unregister_service(info, intf, &sock.pktinfo); + } + } + } + } + self.my_services.clear(); + + // 2. Stop all browse operations + let browse_types: Vec = self.service_queriers.keys().cloned().collect(); + for ty_domain in browse_types { + debug!("Stopping browse during shutdown: {}", &ty_domain); + if let Some(sender) = self.service_queriers.remove(&ty_domain) { + // Notify the client + if let Err(e) = sender.send(ServiceEvent::SearchStopped(ty_domain.clone())) { + debug!("Failed to send SearchStopped during shutdown: {}", e); + } + } + } + + // 3. Stop all hostname resolution operations + let hostnames: Vec = self.hostname_resolvers.keys().cloned().collect(); + for hostname in hostnames { + debug!( + "Stopping hostname resolution during shutdown: {}", + &hostname + ); + if let Some((sender, _timeout)) = self.hostname_resolvers.remove(&hostname) { + // Notify the client + if let Err(e) = + sender.send(HostnameResolutionEvent::SearchStopped(hostname.clone())) + { + debug!( + "Failed to send HostnameResolutionEvent::SearchStopped during shutdown: {}", + e + ); + } + } + } + + // 4. Clear all retransmissions + self.retransmissions.clear(); + + // 5. Drop any pending delayed responses + self.delayed_responses.clear(); + + debug!("Cleanup completed"); + } + + /// The main event loop of the daemon thread + /// + /// In each round, it will: + /// 1. select the listening sockets with a timeout. + /// 2. process the incoming packets if any. + /// 3. try_recv on its channel and execute commands. + /// 4. announce its registered services. + /// 5. process retransmissions if any. + fn run(&mut self, receiver: Receiver) -> Option { + // Add the daemon's signal socket to the poller. + if let Err(e) = self.poller.registry().register( + &mut self.signal_sock, + mio::Token(SIGNAL_SOCK_EVENT_KEY), + mio::Interest::READABLE, + ) { + debug!("failed to add signal socket to the poller: {}", e); + return None; + } + + if let Some(sock) = self.ipv4_sock.as_mut() { + if let Err(e) = self.poller.registry().register( + sock, + mio::Token(IPV4_SOCK_EVENT_KEY), + mio::Interest::READABLE, + ) { + debug!("failed to register ipv4 socket: {}", e); + return None; + } + } + + if let Some(sock) = self.ipv6_sock.as_mut() { + if let Err(e) = self.poller.registry().register( + sock, + mio::Token(IPV6_SOCK_EVENT_KEY), + mio::Interest::READABLE, + ) { + debug!("failed to register ipv6 socket: {}", e); + return None; + } + } + + // Setup timer for IP checks. + let mut next_ip_check = if self.ip_check_interval > 0 { + current_time_millis() + self.ip_check_interval + } else { + 0 + }; + + if next_ip_check > 0 { + self.add_timer(next_ip_check); + } + + // Start the run loop. + + let mut events = mio::Events::with_capacity(1024); + loop { + let now = current_time_millis(); + + let earliest_timer = self.peek_earliest_timer(); + let timeout = earliest_timer.map_or(MAX_POLL_SLEEP, |timer| { + // If `timer` already passed, set `timeout` to be 1ms. + let millis = if timer > now { timer - now } else { 1 }; + Duration::from_millis(millis).min(MAX_POLL_SLEEP) + }); + + // Process incoming packets, command events and optional timeout. + events.clear(); + match self.poller.poll(&mut events, Some(timeout)) { + Ok(_) => self.handle_poller_events(&events), + Err(e) => debug!("failed to select from sockets: {}", e), + } + + let now = current_time_millis(); + + // Remove the timers if already passed. + self.pop_timers_till(now); + + // Remove hostname resolvers with expired timeouts. + for hostname in self + .hostname_resolvers + .clone() + .into_iter() + .filter(|(_, (_, timeout))| timeout.map(|t| now >= t).unwrap_or(false)) + .map(|(hostname, _)| hostname) + { + trace!("hostname resolver timeout for {}", &hostname); + call_hostname_resolution_listener( + &self.hostname_resolvers, + &hostname, + HostnameResolutionEvent::SearchTimeout(hostname.to_owned()), + ); + call_hostname_resolution_listener( + &self.hostname_resolvers, + &hostname, + HostnameResolutionEvent::SearchStopped(hostname.to_owned()), + ); + self.hostname_resolvers.remove(&hostname); + } + + // process commands from the command channel + while let Ok(command) = receiver.try_recv() { + if matches!(command, Command::Exit(_)) { + debug!("Exit command received, performing cleanup"); + self.cleanup(); + self.status = DaemonStatus::Shutdown; + return Some(command); + } + self.exec_command(command, false); + } + + // check for repeated commands and run them if their time is up. + let mut i = 0; + while i < self.retransmissions.len() { + if now >= self.retransmissions[i].next_time { + let rerun = self.retransmissions.remove(i); + self.exec_command(rerun.command, true); + } else { + i += 1; + } + } + + // Send delayed responses whose time is up (RFC 6762 §6). + let mut i = 0; + while i < self.delayed_responses.len() { + if now >= self.delayed_responses[i].next_time { + let resp = self.delayed_responses.remove(i); + self.send_delayed_response(resp); + } else { + i += 1; + } + } + + // Refresh cached service records with active queriers + self.refresh_active_services(); + + // Refresh cached A/AAAA records with active queriers + let mut query_count = 0; + for (hostname, _sender) in self.hostname_resolvers.iter() { + for (hostname, ip_addr) in + self.cache.refresh_due_hostname_resolutions(hostname).iter() + { + self.send_query(hostname, ip_address_rr_type(&ip_addr.to_ip_addr())); + query_count += 1; + } + } + + self.increase_counter(Counter::CacheRefreshAddr, query_count); + + // check and evict expired records in our cache + let now = current_time_millis(); + + // Notify service listeners about the expired records. + let expired_services = self.cache.evict_expired_services(now); + if !expired_services.is_empty() { + debug!( + "run: send {} service removal to listeners", + expired_services.len() + ); + self.notify_service_removal(expired_services); + } + + // Notify hostname listeners about the expired records. + let expired_addrs = self.cache.evict_expired_addr(now); + for (hostname, addrs) in expired_addrs { + call_hostname_resolution_listener( + &self.hostname_resolvers, + &hostname, + HostnameResolutionEvent::AddressesRemoved(hostname.clone(), addrs), + ); + let instances = self.cache.get_instances_on_host(&hostname); + let instance_set: HashSet = instances.into_iter().collect(); + self.resolve_updated_instances(&instance_set); + } + + // Send out probing queries. + self.probing_handler(); + + // check IP changes if next_ip_check is reached. + if now >= next_ip_check && next_ip_check > 0 { + next_ip_check = now + self.ip_check_interval; + self.add_timer(next_ip_check); + + self.check_ip_changes(); + } + } + } + + fn process_set_option(&mut self, daemon_opt: DaemonOption) { + match daemon_opt { + DaemonOption::ServiceNameLenMax(length) => self.service_name_len_max = length, + DaemonOption::IpCheckInterval(interval) => self.ip_check_interval = interval, + DaemonOption::MaxPacketSize(if_kind, size) => self.set_max_packet_size(if_kind, size), + DaemonOption::EnableInterface(if_kind) => self.enable_interface(if_kind), + DaemonOption::DisableInterface(if_kind) => self.disable_interface(if_kind), + DaemonOption::MulticastLoopV4(on) => self.set_multicast_loop_v4(on), + DaemonOption::MulticastLoopV6(on) => self.set_multicast_loop_v6(on), + DaemonOption::AcceptUnsolicited(accept) => self.set_accept_unsolicited(accept), + DaemonOption::IncludeAppleP2P(enable) => self.set_apple_p2p(enable), + #[cfg(test)] + DaemonOption::TestDownInterface(ifname) => { + self.test_down_interfaces.insert(ifname); + } + #[cfg(test)] + DaemonOption::TestUpInterface(ifname) => { + self.test_down_interfaces.remove(&ifname); + } + } + } + + fn enable_interface(&mut self, kinds: Vec) { + debug!("enable_interface: {:?}", kinds); + let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); + + for if_kind in kinds { + self.if_selections.push(IfSelection { + if_kind: resolve_addr_to_index(if_kind, &interfaces), + selected: true, + }); + } + + self.apply_intf_selections(interfaces); + } + + fn disable_interface(&mut self, kinds: Vec) { + debug!("disable_interface: {:?}", kinds); + let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); + + for if_kind in kinds { + self.if_selections.push(IfSelection { + if_kind: resolve_addr_to_index(if_kind, &interfaces), + selected: false, + }); + } + + self.apply_intf_selections(interfaces); + } + + fn set_max_packet_size(&mut self, kinds: Vec, size: usize) { + debug!("set_max_packet_size: {:?} {}", kinds, size); + let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); + + for if_kind in kinds { + self.max_packet_sizes.push(MaxPacketSizeSelection { + if_kind: resolve_addr_to_index(if_kind, &interfaces), + max_packet_size: size, + }); + } + + self.apply_max_packet_sizes(&interfaces); + } + + /// Resolve all max packet size selections against `interfaces` and store the + /// outcome in every interface in `my_intfs`. + fn apply_max_packet_sizes(&mut self, interfaces: &[Interface]) { + for (if_index, my_intf) in self.my_intfs.iter_mut() { + let v4 = resolve_max_packet_size(&self.max_packet_sizes, interfaces, *if_index, true); + let v6 = resolve_max_packet_size(&self.max_packet_sizes, interfaces, *if_index, false); + + if my_intf.max_packet_size_v4 != v4 || my_intf.max_packet_size_v6 != v6 { + debug!( + "interface {}: max packet size v4 {} -> {v4}, v6 {} -> {v6}", + my_intf.name, my_intf.max_packet_size_v4, my_intf.max_packet_size_v6 + ); + my_intf.max_packet_size_v4 = v4; + my_intf.max_packet_size_v6 = v6; + } + } + } + + fn set_multicast_loop_v4(&mut self, on: bool) { + let Some(sock) = self.ipv4_sock.as_mut() else { + return; + }; + self.multicast_loop_v4 = on; + sock.pktinfo + .set_multicast_loop_v4(on) + .map_err(|e| e_fmt!("failed to set multicast loop v4: {}", e)) + .unwrap(); + } + + fn set_multicast_loop_v6(&mut self, on: bool) { + let Some(sock) = self.ipv6_sock.as_mut() else { + return; + }; + self.multicast_loop_v6 = on; + sock.pktinfo + .set_multicast_loop_v6(on) + .map_err(|e| e_fmt!("failed to set multicast loop v6: {}", e)) + .unwrap(); + } + + fn set_accept_unsolicited(&mut self, accept: bool) { + self.accept_unsolicited = accept; + } + + fn set_apple_p2p(&mut self, include: bool) { + if self.include_apple_p2p != include { + self.include_apple_p2p = include; + self.apply_intf_selections(my_ip_interfaces_inner(true, self.include_apple_p2p)); + } + } + + fn notify_monitors(&mut self, event: DaemonEvent) { + // Only retain the monitors that are still connected. + self.monitors.retain(|sender| { + if let Err(e) = sender.try_send(event.clone()) { + debug!("notify_monitors: try_send: {}", &e); + if matches!(e, TrySendError::Disconnected(_)) { + return false; // This monitor is dropped. + } + } + true + }); + } + + /// Remove `addr` in my services that enabled `addr_auto`. + fn del_addr_in_my_services(&mut self, addr: &IpAddr) { + for (_, service_info) in self.my_services.iter_mut() { + if service_info.is_addr_auto() { + service_info.remove_ipaddr(addr); + } + } + } + + fn add_timer(&mut self, next_time: u64) { + add_bounded_timer(&mut self.timers, next_time); + } + + fn peek_earliest_timer(&self) -> Option { + self.timers.peek().map(|Reverse(v)| *v) + } + + fn _pop_earliest_timer(&mut self) -> Option { + self.timers.pop().map(|Reverse(v)| v) + } + + /// Pop all timers that are already passed till `now`. + fn pop_timers_till(&mut self, now: u64) { + while let Some(Reverse(v)) = self.timers.peek() { + if *v > now { + break; + } + self.timers.pop(); + } + } + + /// Apply all selections to `interfaces` and return the selected addresses. + fn selected_intfs(&self, interfaces: Vec) -> HashSet { + 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; + } + } + } + + let mut selected_addrs = HashSet::new(); + for i in 0..intf_count { + if intf_selections[i] { + selected_addrs.insert(interfaces[i].clone()); + } + } + + selected_addrs + } + + /// Apply all selections to `interfaces`. + /// + /// For any interface, add it if selected but not bound yet, + /// delete it if not selected but still bound. + fn apply_intf_selections(&mut self, interfaces: Vec) { + // By default, we enable all 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 `my_intfs` based on the selections. + for (idx, intf) in interfaces.iter().enumerate() { + if intf_selections[idx] { + // Add the interface + self.add_interface(intf, &interfaces); + } else { + // Remove the interface + self.del_interface_addr(intf); + } + } + + // An interface that lost an address may now match a different selection. + // (`add_interface` already resolved the ones that gained one.) + self.apply_max_packet_sizes(&interfaces); + } + + fn del_ip(&mut self, ip: IpAddr) { + self.del_addr_in_my_services(&ip); + self.notify_monitors(DaemonEvent::IpDel(ip)); + } + + /// Check for IP changes and update [my_intfs] as needed. + fn check_ip_changes(&mut self) { + // Get the current interfaces. + let my_ifaddrs = my_ip_interfaces_inner(true, self.include_apple_p2p); + + #[cfg(test)] + let my_ifaddrs: Vec<_> = my_ifaddrs + .into_iter() + .filter(|intf| !self.test_down_interfaces.contains(&intf.name)) + .collect(); + + let ifaddrs_map: HashMap> = + my_ifaddrs.iter().fold(HashMap::new(), |mut acc, intf| { + let if_index = intf.index.unwrap_or(0); + acc.entry(if_index).or_default().push(&intf.addr); + acc + }); + + let mut deleted_intfs = Vec::new(); + let mut deleted_ips = Vec::new(); + + for (if_index, my_intf) in self.my_intfs.iter_mut() { + let mut last_ipv4 = None; + let mut last_ipv6 = None; + + if let Some(current_addrs) = ifaddrs_map.get(if_index) { + my_intf.addrs.retain(|addr| { + if current_addrs.contains(&addr) { + true + } else { + match addr.ip() { + IpAddr::V4(ipv4) => last_ipv4 = Some(ipv4), + IpAddr::V6(ipv6) => last_ipv6 = Some(ipv6), + } + deleted_ips.push(addr.ip()); + false + } + }); + if my_intf.addrs.is_empty() { + deleted_intfs.push((*if_index, last_ipv4, last_ipv6)) + } + } else { + // If it does not exist, remove the interface. + debug!( + "check_ip_changes: interface {} ({}) no longer exists, removing", + my_intf.name, if_index + ); + for addr in my_intf.addrs.iter() { + match addr.ip() { + IpAddr::V4(ipv4) => last_ipv4 = Some(ipv4), + IpAddr::V6(ipv6) => last_ipv6 = Some(ipv6), + } + deleted_ips.push(addr.ip()) + } + deleted_intfs.push((*if_index, last_ipv4, last_ipv6)); + } + } + + if !deleted_ips.is_empty() || !deleted_intfs.is_empty() { + debug!( + "check_ip_changes: {} deleted ips {} deleted intfs", + deleted_ips.len(), + deleted_intfs.len() + ); + } + + for ip in deleted_ips { + self.del_ip(ip); + } + + for (if_index, last_ipv4, last_ipv6) in deleted_intfs { + let Some(my_intf) = self.my_intfs.remove(&if_index) else { + continue; + }; + + if let Some(ipv4) = last_ipv4 { + debug!("leave multicast for {ipv4}"); + if let Some(sock) = self.ipv4_sock.as_mut() { + if let Err(e) = sock.pktinfo.leave_multicast_v4(&GROUP_ADDR_V4, &ipv4) { + debug!("leave multicast group for addr {ipv4}: {e}"); + } + } + } + + if let Some(ipv6) = last_ipv6 { + debug!("leave multicast for {ipv6}"); + if let Some(sock) = self.ipv6_sock.as_mut() { + if let Err(e) = sock + .pktinfo + .leave_multicast_v6(&GROUP_ADDR_V6, my_intf.index) + { + debug!("leave multicast group for IPv6: {ipv6}: {e}"); + } + } + } + + // Remove cache records for this interface. + let intf_id = InterfaceId { + name: my_intf.name.to_string(), + index: my_intf.index, + }; + let result = self.cache.remove_records_on_intf(intf_id); + self.notify_service_removal(result.removed_instances); + self.resolve_updated_instances(&result.modified_instances); + } + + // Add newly found interfaces only if in our selections. + self.apply_intf_selections(my_ifaddrs); + } + + /// Remove an interface address when it was down, disabled or removed from the system. + /// If no more addresses on the interface, remove the interface as well. + fn del_interface_addr(&mut self, intf: &Interface) { + let if_index = intf.index.unwrap_or(0); + debug!( + "del_interface_addr: {} ({if_index}) addr {}", + intf.name, + intf.ip() + ); + + let Some(my_intf) = self.my_intfs.get_mut(&if_index) else { + debug!("del_interface_addr: interface {} not found", intf.name); + return; + }; + + let mut ip_removed = false; + + if my_intf.addrs.remove(&intf.addr) { + ip_removed = true; + + match intf.addr.ip() { + IpAddr::V4(ipv4) => { + if my_intf.next_ifaddr_v4().is_none() { + if let Some(sock) = self.ipv4_sock.as_mut() { + if let Err(e) = sock.pktinfo.leave_multicast_v4(&GROUP_ADDR_V4, &ipv4) { + debug!("leave multicast group for addr {ipv4}: {e}"); + } else { + debug!("leave multicast for {ipv4}"); + } + } + } + } + + IpAddr::V6(ipv6) => { + if my_intf.next_ifaddr_v6().is_none() { + if let Some(sock) = self.ipv6_sock.as_mut() { + if let Err(e) = + sock.pktinfo.leave_multicast_v6(&GROUP_ADDR_V6, if_index) + { + debug!("leave multicast group for addr {ipv6}: {e}"); + } + } + } + } + } + + if my_intf.addrs.is_empty() { + // If no more addresses, remove the interface. + debug!("del_interface_addr: removing interface {}", intf.name); + self.my_intfs.remove(&if_index); + self.dns_registry_map.remove(&if_index); + self.cache + .remove_addrs_on_disabled_intf(if_index, IpType::BOTH); + } else { + // Interface still has addresses of the other IP version. + // Remove cached address records for the disabled IP version + // only if no more addresses of that version remain. + let is_v4 = intf.addr.ip().is_ipv4(); + let version_gone = if is_v4 { + my_intf.next_ifaddr_v4().is_none() + } else { + my_intf.next_ifaddr_v6().is_none() + }; + if version_gone { + let ip_type = if is_v4 { IpType::V4 } else { IpType::V6 }; + self.cache.remove_addrs_on_disabled_intf(if_index, ip_type); + } + } + } + + if ip_removed { + // Notify the monitors. + self.notify_monitors(DaemonEvent::IpDel(intf.ip())); + // Remove the interface from my services that enabled `addr_auto`. + self.del_addr_in_my_services(&intf.ip()); + } + } + + /// Add the address of `intf` to `my_intfs`, and announce our services on it. + /// + /// `interfaces` is the full list the caller is applying, needed to resolve the + /// max packet size of the interface before we send anything on it. + fn add_interface(&mut self, intf: &Interface, interfaces: &[Interface]) { + let sock_opt = if intf.ip().is_ipv4() { + &self.ipv4_sock + } else { + &self.ipv6_sock + }; + + let Some(sock) = sock_opt else { + debug!( + "add_interface: no socket available for interface {} with addr {}. Skipped.", + intf.name, + intf.ip() + ); + return; + }; + + let if_index = intf.index.unwrap_or(0); + let mut new_addr = false; + + match self.my_intfs.entry(if_index) { + Entry::Occupied(mut entry) => { + // If intf has a new address, add it to the existing interface. + let my_intf = entry.get_mut(); + if !my_intf.addrs.contains(&intf.addr) { + if let Err(e) = join_multicast_group(&sock.pktinfo, intf) { + debug!("add_interface: socket_config {}: {e}", &intf.name); + } + my_intf.addrs.insert(intf.addr.clone()); + new_addr = true; + } + } + Entry::Vacant(entry) => { + if let Err(e) = join_multicast_group(&sock.pktinfo, intf) { + debug!("add_interface: socket_config {}: {e}. Skipped.", &intf.name); + return; + } + + new_addr = true; + let new_intf = MyIntf { + name: intf.name.clone(), + index: if_index, + addrs: HashSet::from([intf.addr.clone()]), + max_packet_size_v4: MAX_PKT_DEFAULT, + max_packet_size_v6: MAX_PKT_DEFAULT, + }; + entry.insert(new_intf); + } + } + + if !new_addr { + trace!("add_interface: interface {} already exists", &intf.name); + return; + } + + debug!("add new interface {}: {}", intf.name, intf.ip()); + + // Resolve before announcing, so the first packet out already honors it. + let v4 = resolve_max_packet_size(&self.max_packet_sizes, interfaces, if_index, true); + let v6 = resolve_max_packet_size(&self.max_packet_sizes, interfaces, if_index, false); + if let Some(my_intf) = self.my_intfs.get_mut(&if_index) { + my_intf.max_packet_size_v4 = v4; + my_intf.max_packet_size_v6 = v6; + } + + let Some(my_intf) = self.my_intfs.get(&if_index) else { + debug!("add_interface: cannot find if_index {if_index}"); + return; + }; + + let dns_registry = match self.dns_registry_map.get_mut(&if_index) { + Some(registry) => registry, + None => self + .dns_registry_map + .entry(if_index) + .or_insert_with(DnsRegistry::new), + }; + + for (_, service_info) in self.my_services.iter_mut() { + if service_info.is_addr_auto() { + service_info.insert_ipaddr(intf); + + if let Ok(true) = announce_service_on_intf( + dns_registry, + service_info, + my_intf, + &sock.pktinfo, + self.port, + ) { + debug!( + "Announce service {} on {}", + service_info.get_fullname(), + intf.ip() + ); + service_info.set_status(if_index, ServiceStatus::Announced); + } else { + for timer in dns_registry.new_timers.drain(..) { + add_bounded_timer(&mut self.timers, timer); + } + service_info.set_status(if_index, ServiceStatus::Probing); + } + } + } + + // Send browse queries on the new interface without known answers. + // This avoids known-answer suppression (RFC 6762 Section 7.1) that + // would cause the responder to suppress its response, preventing + // address records from being attributed to the new interface. + if let Some(my_intf) = self.my_intfs.get(&if_index) { + for ty in self.service_queriers.keys() { + self.send_query_on_intf(ty, RRType::PTR, my_intf); + } + } + + // Notify the monitors. + self.notify_monitors(DaemonEvent::IpAdd(intf.ip())); + } + + /// Registers a service. + /// + /// RFC 6762 section 8.3. + /// ...the Multicast DNS responder MUST send + /// an unsolicited Multicast DNS response containing, in the Answer + /// Section, all of its newly registered resource records + /// + /// Zeroconf will then respond to requests for information about this service. + fn register_service(&mut self, mut 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; + } + + if info.is_addr_auto() { + let selected_intfs = + self.selected_intfs(my_ip_interfaces_inner(true, self.include_apple_p2p)); + for intf in selected_intfs { + info.insert_ipaddr(&intf); + } + } + + debug!("register service {:?}", &info); + + let outgoing_addrs = self.send_unsolicited_response(&mut info); + if !outgoing_addrs.is_empty() { + self.notify_monitors(DaemonEvent::Announce( + info.get_fullname().to_string(), + format!("{:?}", &outgoing_addrs), + )); + } + + // The key has to be lower case letter as DNS record name is case insensitive. + // The info will have the original name. + let service_fullname = info.get_fullname().to_lowercase(); + self.my_services.insert(service_fullname, info); + } + + /// Sends out announcement of `info` on every valid interface. + /// Returns the list of interface IPs that sent out the announcement. + fn send_unsolicited_response(&mut self, info: &mut ServiceInfo) -> Vec { + let mut outgoing_addrs = Vec::new(); + let mut outgoing_intfs = HashSet::new(); + + let mut invalid_intf_addrs = HashSet::new(); + + for (if_index, intf) in self.my_intfs.iter() { + let dns_registry = match self.dns_registry_map.get_mut(if_index) { + Some(registry) => registry, + None => self + .dns_registry_map + .entry(*if_index) + .or_insert_with(DnsRegistry::new), + }; + + let mut announced = false; + + // IPv4 + if let Some(sock) = self.ipv4_sock.as_mut() { + match announce_service_on_intf(dns_registry, info, intf, &sock.pktinfo, self.port) { + Ok(true) => { + for addr in intf.addrs.iter().filter(|a| a.ip().is_ipv4()) { + outgoing_addrs.push(addr.ip()); + } + outgoing_intfs.insert(intf.index); + + debug!( + "Announce service IPv4 {} on {}", + info.get_fullname(), + intf.name + ); + announced = true; + } + Ok(false) => {} + Err(InternalError::IntfAddrInvalid(intf_addr)) => { + invalid_intf_addrs.insert(intf_addr); + } + } + } + + if let Some(sock) = self.ipv6_sock.as_mut() { + match announce_service_on_intf(dns_registry, info, intf, &sock.pktinfo, self.port) { + Ok(true) => { + for addr in intf.addrs.iter().filter(|a| a.ip().is_ipv6()) { + outgoing_addrs.push(addr.ip()); + } + outgoing_intfs.insert(intf.index); + + debug!( + "Announce service IPv6 {} on {}", + info.get_fullname(), + intf.name + ); + announced = true; + } + Ok(false) => {} + Err(InternalError::IntfAddrInvalid(intf_addr)) => { + invalid_intf_addrs.insert(intf_addr); + } + } + } + + if announced { + info.set_status(intf.index, ServiceStatus::Announced); + } else { + for timer in dns_registry.new_timers.drain(..) { + add_bounded_timer(&mut self.timers, timer); + } + info.set_status(*if_index, ServiceStatus::Probing); + } + } + + if !invalid_intf_addrs.is_empty() { + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addrs)); + } + + // RFC 6762 section 8.3. + // ..The Multicast DNS responder MUST send at least two unsolicited + // responses, one second apart. + let next_time = current_time_millis() + + ANNOUNCE_SECOND_DELAY_MILLIS + + fastrand::u64(0..ANNOUNCE_SECOND_JITTER_MILLIS); + for if_index in outgoing_intfs { + self.add_retransmission( + next_time, + Command::RegisterResend(info.get_fullname().to_string(), if_index), + ); + } + + outgoing_addrs + } + + /// Send probings or finish them if expired. Notify waiting services. + fn probing_handler(&mut self) { + let now = current_time_millis(); + let mut invalid_intf_addrs = HashSet::new(); + + for (if_index, intf) in self.my_intfs.iter() { + let Some(dns_registry) = self.dns_registry_map.get_mut(if_index) else { + continue; + }; + + let (out, expired_probes) = check_probing(dns_registry, &mut self.timers, now); + + // send probing. + if !out.questions().is_empty() { + trace!("sending out probing of questions: {:?}", out.questions()); + if let Some(sock) = self.ipv4_sock.as_mut() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + if let Some(sock) = self.ipv6_sock.as_mut() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + } + + // For finished probes, wake up services that are waiting for the probes. + let waiting_services = + handle_expired_probes(expired_probes, &intf.name, dns_registry, &mut self.monitors); + + for service_name in waiting_services { + // service names are lowercase + if let Some(info) = self.my_services.get_mut(&service_name.to_lowercase()) { + if info.get_status(*if_index) == ServiceStatus::Announced { + debug!("service {} already announced", info.get_fullname()); + continue; + } + + let announced_v4 = if let Some(sock) = self.ipv4_sock.as_mut() { + match announce_service_on_intf( + dns_registry, + info, + intf, + &sock.pktinfo, + self.port, + ) { + Ok(announced) => announced, + Err(InternalError::IntfAddrInvalid(intf_addr)) => { + invalid_intf_addrs.insert(intf_addr); + false + } + } + } else { + false + }; + let announced_v6 = if let Some(sock) = self.ipv6_sock.as_mut() { + match announce_service_on_intf( + dns_registry, + info, + intf, + &sock.pktinfo, + self.port, + ) { + Ok(announced) => announced, + Err(InternalError::IntfAddrInvalid(intf_addr)) => { + invalid_intf_addrs.insert(intf_addr); + false + } + } + } else { + false + }; + + if announced_v4 || announced_v6 { + let next_time = now + + ANNOUNCE_SECOND_DELAY_MILLIS + + fastrand::u64(0..ANNOUNCE_SECOND_JITTER_MILLIS); + let command = + Command::RegisterResend(info.get_fullname().to_string(), *if_index); + self.retransmissions.push(ReRun { next_time, command }); + add_bounded_timer(&mut self.timers, next_time); + + let fullname = dns_registry.resolve_name(&service_name).to_string(); + + let hostname = dns_registry.resolve_name(info.get_hostname()); + + debug!("wake up: announce service {} on {}", fullname, intf.name); + notify_monitors( + &mut self.monitors, + DaemonEvent::Announce(fullname, format!("{}:{}", hostname, &intf.name)), + ); + + info.set_status(*if_index, ServiceStatus::Announced); + } + } + } + } + + if !invalid_intf_addrs.is_empty() { + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addrs)); + } + } + + fn unregister_service( + &self, + info: &ServiceInfo, + intf: &MyIntf, + sock: &PktInfoUdpSocket, + ) -> Vec { + let is_ipv4 = sock.domain() == Domain::IPV4; + + // Goodbye records must carry the names peers actually cached: if + // probing renamed a record on this interface, withdraw the renamed + // name, not the original one from `ServiceInfo`. + let (fullname, hostname) = match self.dns_registry_map.get(&intf.index) { + Some(dns_registry) => ( + dns_registry.resolve_name(info.get_fullname()), + dns_registry.resolve_name(info.get_hostname()), + ), + None => (info.get_fullname(), info.get_hostname()), + }; + + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); + out.add_answer_at_time( + DnsPointer::new( + info.get_type(), + RRType::PTR, + CLASS_IN, + 0, + fullname.to_string(), + ), + 0, + ); + + if let Some(sub) = info.get_subtype() { + trace!("Adding subdomain {}", sub); + out.add_answer_at_time( + DnsPointer::new(sub, RRType::PTR, CLASS_IN, 0, fullname.to_string()), + 0, + ); + } + + out.add_answer_at_time( + DnsSrv::new( + fullname, + CLASS_IN | CLASS_CACHE_FLUSH, + 0, + info.get_priority(), + info.get_weight(), + info.get_port(), + hostname.to_string(), + ), + 0, + ); + out.add_answer_at_time( + DnsTxt::new( + fullname, + CLASS_IN | CLASS_CACHE_FLUSH, + 0, + info.generate_txt(), + ), + 0, + ); + + let if_addrs = if is_ipv4 { + info.get_addrs_on_my_intf_v4(intf) + } else { + info.get_addrs_on_my_intf_v6(intf) + }; + + if if_addrs.is_empty() { + return vec![]; + } + + for address in if_addrs { + out.add_answer_at_time( + DnsAddress::new( + hostname, + ip_address_rr_type(&address), + CLASS_IN | CLASS_CACHE_FLUSH, + 0, + address, + intf.into(), + ), + 0, + ); + } + + // Only (at most) one packet is expected to be sent out. + let sent_vec = match send_dns_outgoing(&out, intf, sock, self.port, None, None) { + Ok(sent_vec) => sent_vec, + Err(InternalError::IntfAddrInvalid(intf_addr)) => { + let invalid_intf_addrs = HashSet::from([intf_addr]); + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addrs)); + vec![] + } + }; + sent_vec.into_iter().next().unwrap_or_default() + } + + /// Binds a channel `listener` to querying mDNS hostnames. + /// + /// If there is already a `listener`, it will be updated, i.e. overwritten. + fn add_hostname_resolver( + &mut self, + hostname: String, + listener: Sender, + timeout: Option, + ) { + let real_timeout = timeout.map(|t| current_time_millis() + t); + self.hostname_resolvers + .insert(hostname.to_lowercase(), (listener, real_timeout)); + if let Some(t) = real_timeout { + self.add_timer(t); + } + } + + /// Sends a multicast query for `name` with `qtype`. + fn send_query(&self, name: &str, qtype: RRType) { + self.send_query_vec(&[(name, qtype)]); + } + + /// Sends a query on a specific interface without known answers. + /// + /// Used when a new interface is added so the responder won't suppress + /// its response due to known-answer suppression (RFC 6762 Section 7.1). + fn send_query_on_intf(&self, name: &str, qtype: RRType, intf: &MyIntf) { + let mut out = DnsOutgoing::new(FLAGS_QR_QUERY); + out.add_question(name, qtype); + + let mut invalid_intf_addrs = HashSet::new(); + if let Some(sock) = self.ipv4_sock.as_ref() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + if let Some(sock) = self.ipv6_sock.as_ref() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + if !invalid_intf_addrs.is_empty() { + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addrs)); + } + } + + /// Sends out a list of `questions` (i.e. DNS questions) via multicast. + fn send_query_vec(&self, questions: &[(&str, RRType)]) { + let mut out = DnsOutgoing::new(FLAGS_QR_QUERY); + let now = current_time_millis(); + + for (name, qtype) in questions { + out.add_question(name, *qtype); + + for record in self.cache.get_known_answers(name, *qtype, now) { + /* + RFC 6762 section 7.1: https://datatracker.ietf.org/doc/html/rfc6762#section-7.1 + ... + When a Multicast DNS querier sends a query to which it already knows + some answers, it populates the Answer Section of the DNS query + message with those answers. + */ + trace!("add known answer: {:?}", record.record); + let mut new_record = record.record.clone(); + new_record.get_record_mut().update_ttl(now); + out.add_answer_box(new_record); + } + } + + let mut invalid_intf_addrs = HashSet::new(); + for (_, intf) in self.my_intfs.iter() { + if let Some(sock) = self.ipv4_sock.as_ref() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + if let Some(sock) = self.ipv6_sock.as_ref() { + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None) + { + invalid_intf_addrs.insert(intf_addr); + } + } + } + + if !invalid_intf_addrs.is_empty() { + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addrs)); + } + } + + /// Reads one UDP datagram from the socket of `intf`. + /// + /// Returns false if failed to receive a packet, + /// otherwise returns true. + fn handle_read(&mut self, event_key: usize) -> bool { + let is_ipv4 = event_key == IPV4_SOCK_EVENT_KEY; + let sock_opt = match event_key { + IPV4_SOCK_EVENT_KEY => &mut self.ipv4_sock, + IPV6_SOCK_EVENT_KEY => &mut self.ipv6_sock, + _ => { + debug!("handle_read: unknown token {}", event_key); + return false; + } + }; + let Some(sock) = sock_opt.as_mut() else { + debug!("handle_read: socket not available for token {}", event_key); + return false; + }; + // The buffer is one byte bigger than the biggest legal message, so that an + // over-sized datagram can be told apart from a legal one that happens to be + // exactly at the limit. + let max_size = max_pkt_absolute(is_ipv4); + let mut buf = vec![0u8; max_size + 1]; + + // Read the next mDNS UDP datagram. + let (sz, pktinfo) = match sock.pktinfo.recv(&mut buf) { + Ok(sz) => sz, + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + debug!("listening socket read failed: {}", e); + } + return false; + } + }; + + // RFC 6762 section 17 caps an mDNS packet at 9000 bytes including the IP and + // UDP headers. A datagram over that arrives truncated, and decoding a + // truncated message does not fail cleanly: names run into whatever bytes + // follow, yielding bogus records or confusing parse errors. Drop it instead. + // + // On Windows, `recv` fails with WSAEMSGSIZE for such a datagram instead of + // truncating it, so it is dropped by the error branch above. Either way it + // is never decoded. + if sz > max_size { + debug!( + "handle_read: dropping over-sized datagram of at least {} bytes (max {})", + sz, max_size + ); + return true; // We still read something. + } + + // Find the interface that received the packet. + let pkt_if_index = pktinfo.if_index as u32; + let Some(my_intf) = self.my_intfs.get(&pkt_if_index) else { + debug!( + "handle_read: no interface found for pktinfo if_index: {}", + pktinfo.if_index + ); + return true; // We still return true to indicate that we read something. + }; + + // Drop packets for an IP version that has been disabled on this interface. + // This is needed because some times the socket layer may still receive packets + // for an IP version even after we left the multicast group for that IP version. + // We want to drop such packets to avoid unnecessary processing. + let is_ipv4 = event_key == IPV4_SOCK_EVENT_KEY; + if (is_ipv4 && my_intf.next_ifaddr_v4().is_none()) + || (!is_ipv4 && my_intf.next_ifaddr_v6().is_none()) + { + debug!( + "handle_read: dropping {} packet on intf {} (disabled)", + if is_ipv4 { "IPv4" } else { "IPv6" }, + my_intf.name + ); + return true; + } + + buf.truncate(sz); // reduce potential processing errors + + match DnsIncoming::new(buf, my_intf.into()) { + Ok(msg) => { + if msg.is_query() { + let querier_addr = pktinfo.addr_src; + self.handle_query(msg, pkt_if_index, querier_addr); + } else if msg.is_response() { + self.handle_response(msg, pkt_if_index, pktinfo.addr_src.ip()); + } else { + debug!("Invalid message: not query and not response"); + } + } + Err(e) => debug!("Invalid incoming DNS message: {}", e), + } + + true + } + + /// Returns true, if sent query. Returns false if SRV already exists. + fn query_unresolved(&mut self, instance: &str) -> bool { + if !valid_instance_name(instance) { + trace!("instance name {} not valid", instance); + return false; + } + + if let Some(records) = self.cache.get_srv(instance) { + for record in records { + if let Some(srv) = record.record.any().downcast_ref::() { + if self.cache.get_addr(srv.host()).is_none() { + self.send_query_vec(&[(srv.host(), RRType::A), (srv.host(), RRType::AAAA)]); + return true; + } + } + } + } else { + self.send_query(instance, RRType::ANY); + return true; + } + + false + } + + /// Checks if `ty_domain` has records in the cache. If yes, sends the + /// cached records via `sender`. + fn query_cache_for_service( + &mut self, + ty_domain: &str, + sender: &Sender, + now: u64, + ) { + let mut resolved: HashSet = HashSet::new(); + let mut unresolved: HashSet = HashSet::new(); + + if let Some(records) = self.cache.get_ptr(ty_domain) { + for record in records.iter().filter(|r| !r.record.expires_soon(now)) { + if let Some(ptr) = record.record.any().downcast_ref::() { + let mut new_event = None; + match self.resolve_service_from_cache(ty_domain, ptr.alias()) { + Ok(resolved_service) => { + if resolved_service.is_valid() { + debug!("Resolved service from cache: {}", ptr.alias()); + new_event = + Some(ServiceEvent::ServiceResolved(Box::new(resolved_service))); + } else { + debug!("Resolved service is not valid: {}", ptr.alias()); + } + } + Err(err) => { + debug!("Error while resolving service from cache: {}", err); + continue; + } + } + + match sender.send(ServiceEvent::ServiceFound( + ty_domain.to_string(), + ptr.alias().to_string(), + )) { + Ok(()) => debug!("sent service found {}", ptr.alias()), + Err(e) => { + debug!("failed to send service found: {}", e); + continue; + } + } + + if let Some(event) = new_event { + resolved.insert(ptr.alias().to_string()); + match sender.send(event) { + Ok(()) => debug!("sent service resolved: {}", ptr.alias()), + Err(e) => debug!("failed to send service resolved: {}", e), + } + } else { + unresolved.insert(ptr.alias().to_string()); + } + } + } + } + + for instance in resolved.drain() { + self.pending_resolves.remove(&instance); + self.resolved.insert(instance); + } + + for instance in unresolved.drain() { + self.add_pending_resolve(instance); + } + } + + /// Checks if `hostname` has records in the cache. If yes, sends the + /// cached records via `sender`. + fn query_cache_for_hostname( + &mut self, + hostname: &str, + sender: Sender, + ) { + let addresses_map = self.cache.get_addresses_for_host(hostname); + for (name, addresses) in addresses_map { + match sender.send(HostnameResolutionEvent::AddressesFound(name, addresses)) { + Ok(()) => trace!("sent hostname addresses found"), + Err(e) => debug!("failed to send hostname addresses found: {}", e), + } + } + } + + fn add_pending_resolve(&mut self, instance: String) { + if !self.pending_resolves.contains(&instance) { + let next_time = current_time_millis() + RESOLVE_WAIT_IN_MILLIS; + self.add_retransmission(next_time, Command::Resolve(instance.clone(), 1)); + self.pending_resolves.insert(instance); + } + } + + /// Creates a `ResolvedService` from the cache. + fn resolve_service_from_cache( + &self, + ty_domain: &str, + fullname: &str, + ) -> Result { + let now = current_time_millis(); + let mut resolved_service = ResolvedService { + ty_domain: ty_domain.to_string(), + sub_ty_domain: None, + fullname: fullname.to_string(), + host: String::new(), + port: 0, + addresses: HashSet::new(), + txt_properties: TxtProperties::new(), + observed_source: None, + }; + + // The service PTR establishes the discovery origin. SRV/TXT/address + // records may arrive separately, but rotating advertised targets must + // not create new source identities for admission accounting. + if let Some(records) = self.cache.get_ptr(ty_domain) { + resolved_service.observed_source = records + .iter() + .filter(|record| !record.record.expires_soon(now)) + .find_map(|record| { + record + .record + .any() + .downcast_ref::() + .filter(|ptr| ptr.alias() == fullname) + .map(|_| record.source_ip) + }); + } + + // Be sure setting `subtype` if available even when querying for the parent domain. + if let Some(subtype) = self.cache.get_subtype(fullname) { + trace!( + "ty_domain: {} found subtype {} for instance: {}", + ty_domain, + subtype, + fullname + ); + if resolved_service.sub_ty_domain.is_none() { + resolved_service.sub_ty_domain = Some(subtype.to_string()); + } + } + + // resolve SRV record + if let Some(records) = self.cache.get_srv(fullname) { + if let Some(answer) = records.iter().find(|r| !r.record.expires_soon(now)) { + if let Some(dns_srv) = answer.record.any().downcast_ref::() { + resolved_service.host = dns_srv.host().to_string(); + resolved_service.port = dns_srv.port(); + } + } + } + + // resolve TXT record + if let Some(records) = self.cache.get_txt(fullname) { + if let Some(record) = records.iter().find(|r| !r.record.expires_soon(now)) { + if let Some(dns_txt) = record.record.any().downcast_ref::() { + resolved_service.txt_properties = dns_txt.text().into(); + } + } + } + + // resolve A and AAAA records + if let Some(records) = self.cache.get_addr(&resolved_service.host) { + for answer in records.iter() { + if let Some(dns_a) = answer.record.any().downcast_ref::() { + if dns_a.expires_soon(now) { + trace!( + "Addr expired or expires soon: {}", + dns_a.address().to_ip_addr() + ); + } else { + let scoped = dns_a.address(); + if let ScopedIp::V4(v4) = &scoped { + // Merge interface_ids if this V4 addr already exists. + // Linear scan by IP since Eq/Hash include interface_ids. + let existing = resolved_service + .addresses + .iter() + .find(|a| a.to_ip_addr() == IpAddr::V4(*v4.addr())) + .cloned(); + if let Some(mut existing) = existing { + resolved_service.addresses.remove(&existing); + if let ScopedIp::V4(existing_v4) = &mut existing { + for id in v4.interface_ids() { + existing_v4.add_interface_id(id.clone()); + } + } + resolved_service.addresses.insert(existing); + } else { + resolved_service.addresses.insert(scoped); + } + } else { + resolved_service.addresses.insert(scoped); + } + } + } + } + } + + Ok(resolved_service) + } + + fn handle_poller_events(&mut self, events: &mio::Events) { + for ev in events.iter() { + trace!("event received with key {:?}", ev.token()); + if ev.token().0 == SIGNAL_SOCK_EVENT_KEY { + // Drain signals as we will drain commands as well. + self.signal_sock_drain(); + + if let Err(e) = self.poller.registry().reregister( + &mut self.signal_sock, + ev.token(), + mio::Interest::READABLE, + ) { + debug!("failed to modify poller for signal socket: {}", e); + } + continue; // Next event. + } + + // Read until no more packets available. + while self.handle_read(ev.token().0) {} + + // we continue to monitor this socket. + if ev.token().0 == IPV4_SOCK_EVENT_KEY { + // Re-register the IPv4 socket for reading. + if let Some(sock) = self.ipv4_sock.as_mut() { + if let Err(e) = + self.poller + .registry() + .reregister(sock, ev.token(), mio::Interest::READABLE) + { + debug!("modify poller for IPv4 socket: {}", e); + } + } + } else if ev.token().0 == IPV6_SOCK_EVENT_KEY { + // Re-register the IPv6 socket for reading. + if let Some(sock) = self.ipv6_sock.as_mut() { + if let Err(e) = + self.poller + .registry() + .reregister(sock, ev.token(), mio::Interest::READABLE) + { + debug!("modify poller for IPv6 socket: {}", e); + } + } + } + } + } + + /// Deal with incoming response packets. All answers + /// are held in the cache, and listeners are notified. + fn handle_response(&mut self, mut msg: DnsIncoming, if_index: u32, source_ip: IpAddr) { + let now = current_time_millis(); + + // remove records that are expired. + let mut record_predicate = |record: &DnsRecordBox| { + if !record.get_record().is_expired(now) { + return true; + } + + debug!("record is expired, removing it from cache."); + if self.cache.remove(record) { + // for PTR records, send event to listeners + if let Some(dns_ptr) = record.any().downcast_ref::() { + call_service_listener( + &self.service_queriers, + dns_ptr.get_name(), + ServiceEvent::ServiceRemoved( + dns_ptr.get_name().to_string(), + dns_ptr.alias().to_string(), + ), + ); + } + } + false + }; + msg.answers_mut().retain(&mut record_predicate); + msg.authorities_mut().retain(&mut record_predicate); + msg.additionals_mut().retain(&mut record_predicate); + + // check possible conflicts and handle them. + self.conflict_handler(&msg, if_index); + + // check if the message is for us. + let mut is_for_us = true; // assume it is for us. + + // If there are any PTR records in the answers, there should be + // at least one PTR for us. Otherwise, the message is not for us. + // If there are no PTR records at all, assume this message is for us. + for answer in msg.answers() { + if answer.get_type() == RRType::PTR { + if self.service_queriers.contains_key(answer.get_name()) { + is_for_us = true; + break; // OK to break: at least one PTR for us. + } else { + is_for_us = false; + } + } else if answer.get_type() == RRType::A || answer.get_type() == RRType::AAAA { + // If there is a hostname querier for this address, then it is for us. + let answer_lowercase = answer.get_name().to_lowercase(); + if self.hostname_resolvers.contains_key(&answer_lowercase) { + is_for_us = true; + break; // OK to break: at least one hostname for us. + } + } + } + + // if we explicitily want to accept unsolicited responses, we should consider all messages as for us. + if self.accept_unsolicited { + is_for_us = true; + } + + /// Represents a DNS record change that involves one service instance. + struct InstanceChange { + ty: RRType, // The type of DNS record for the instance. + name: String, // The name of the record. + } + + // Go through all answers to get the new and updated records. + // For new PTR records, send out ServiceFound immediately. For others, + // collect them into `changes`. + // + // Note: we don't try to identify the update instances based on + // each record immediately as the answers are likely related to each + // other. + let mut changes = Vec::new(); + let mut timers = Vec::new(); + let Some(my_intf) = self.my_intfs.get(&if_index) else { + return; + }; + for record in msg.all_records() { + match self + .cache + .add_or_update(my_intf, source_ip, record, &mut timers, is_for_us) + { + Some((dns_record, true)) => { + timers.push(dns_record.record.get_record().get_expire_time()); + timers.push(dns_record.record.get_record().get_refresh_time()); + + let ty = dns_record.record.get_type(); + let name = dns_record.record.get_name(); + + // Positive dump: a new record was accepted into the cache. + // Counterpart to the "skipping record" debug line in the parser. + debug!("cache: new record: {:?}", &dns_record.record); + + // Only process PTR that does not expire soon (i.e. TTL > 1). + if ty == RRType::PTR && dns_record.record.get_record().get_ttl() > 1 { + if self.service_queriers.contains_key(name) { + timers.push(dns_record.record.get_record().get_refresh_time()); + } + + // send ServiceFound + if let Some(dns_ptr) = dns_record.record.any().downcast_ref::() + { + debug!("calling listener with service found: {name}"); + call_service_listener( + &self.service_queriers, + name, + ServiceEvent::ServiceFound( + name.to_string(), + dns_ptr.alias().to_string(), + ), + ); + changes.push(InstanceChange { + ty, + name: dns_ptr.alias().to_string(), + }); + } + } else { + changes.push(InstanceChange { + ty, + name: name.to_string(), + }); + } + } + Some((dns_record, false)) => { + timers.push(dns_record.record.get_record().get_expire_time()); + timers.push(dns_record.record.get_record().get_refresh_time()); + } + _ => {} + } + } + + // Add timers for the new records. + for t in timers { + self.add_timer(t); + } + + // Go through remaining changes to see if any hostname resolutions were found or updated. + for change in changes + .iter() + .filter(|change| change.ty == RRType::A || change.ty == RRType::AAAA) + { + let addr_map = self.cache.get_addresses_for_host(&change.name); + for (name, addresses) in addr_map { + call_hostname_resolution_listener( + &self.hostname_resolvers, + &change.name, + HostnameResolutionEvent::AddressesFound(name, addresses), + ) + } + } + + // Identify the instances that need to be "resolved". + let mut updated_instances = HashSet::new(); + for update in changes { + match update.ty { + RRType::PTR | RRType::SRV | RRType::TXT => { + updated_instances.insert(update.name); + } + RRType::A | RRType::AAAA => { + let instances = self.cache.get_instances_on_host(&update.name); + updated_instances.extend(instances); + } + _ => {} + } + } + + self.resolve_updated_instances(&updated_instances); + } + + fn conflict_handler(&mut self, msg: &DnsIncoming, if_index: u32) { + let Some(my_intf) = self.my_intfs.get(&if_index) else { + debug!("handle_response: no intf found for index {if_index}"); + return; + }; + + let Some(dns_registry) = self.dns_registry_map.get_mut(&if_index) else { + return; + }; + + for answer in msg.answers().iter() { + let mut new_records = Vec::new(); + + let name = answer.get_name(); + let Some(probe) = dns_registry.probing.get_mut(name) else { + continue; + }; + + // check against possible multicast forwarding + if answer.get_type() == RRType::A || answer.get_type() == RRType::AAAA { + if let Some(answer_addr) = answer.any().downcast_ref::() { + if answer_addr.interface_id.index != if_index { + debug!( + "conflict handler: answer addr {:?} not in the subnet of intf {}", + answer_addr, my_intf.name + ); + continue; + } + } + + // double check if any other address record matches rrdata, + // as there could be multiple addresses for the same name. + let any_match = probe.records.iter().any(|r| { + r.get_type() == answer.get_type() + && r.get_class() == answer.get_class() + && r.rrdata_match(answer.as_ref()) + }); + if any_match { + continue; // no conflict for this answer. + } + } + + probe.records.retain(|record| { + if record.get_type() == answer.get_type() + && record.get_class() == answer.get_class() + && !record.rrdata_match(answer.as_ref()) + { + debug!( + "found conflict name: '{name}' record: {}: {} PEER: {}", + record.get_type(), + record.rdata_print(), + answer.rdata_print() + ); + + // create a new name for this record + // then remove the old record in probing. + let mut new_record = record.clone(); + let new_name = match record.get_type() { + RRType::A => hostname_change(name), + RRType::AAAA => hostname_change(name), + _ => name_change(name), + }; + new_record.get_record_mut().set_new_name(new_name); + new_records.push(new_record); + return false; // old record is dropped from the probe. + } + + true + }); + + // ????? + // if probe.records.is_empty() { + // dns_registry.probing.remove(name); + // } + + // Probing again with the new names. + let create_time = current_time_millis() + fastrand::u64(0..250); + + let waiting_services = probe.waiting_services.clone(); + + for record in new_records { + if dns_registry.update_hostname(name, record.get_name(), create_time) { + add_bounded_timer(&mut self.timers, create_time); + } + + // remember the name changes (note: `name` might not be the original, it could be already changed once.) + dns_registry.name_changes.insert( + record.get_record().get_original_name().to_string(), + record.get_name().to_string(), + ); + + let new_probe = match dns_registry.probing.get_mut(record.get_name()) { + Some(p) => p, + None => { + let new_probe = dns_registry + .probing + .entry(record.get_name().to_string()) + .or_insert_with(|| { + debug!("conflict handler: new probe of {}", record.get_name()); + Probe::new(create_time) + }); + add_bounded_timer(&mut self.timers, new_probe.next_send); + new_probe + } + }; + + debug!( + "insert record with new name '{}' {} into probe", + record.get_name(), + record.get_type() + ); + new_probe.insert_record(record); + + new_probe.waiting_services.extend(waiting_services.clone()); + } + } + } + + /// Resolve the updated (including new) instances. + /// + /// Note: it is possible that more than 1 PTR pointing to the same + /// instance. For example, a regular service type PTR and a sub-type + /// service type PTR can both point to the same service instance. + /// This loop automatically handles the sub-type PTRs. + fn resolve_updated_instances(&mut self, updated_instances: &HashSet) { + if updated_instances.is_empty() { + return; + } + + let mut resolved: HashSet = HashSet::new(); + let mut unresolved: HashSet = HashSet::new(); + let mut removed_instances = HashMap::new(); + + let now = current_time_millis(); + + for (ty_domain, records) in self.cache.all_ptr().iter() { + if !self.service_queriers.contains_key(ty_domain) { + // No need to resolve if not in our queries. + continue; + } + + for ptr in records.iter().filter(|r| !r.record.expires_soon(now)) { + let Some(dns_ptr) = ptr.record.any().downcast_ref::() else { + continue; + }; + + let instance = dns_ptr.alias(); + if !updated_instances.contains(instance) { + continue; + } + + let Ok(resolved_service) = self.resolve_service_from_cache(ty_domain, instance) + else { + continue; + }; + + debug!("resolve_updated_instances: from cache: {instance}"); + if resolved_service.is_valid() { + debug!( + "resolved '{}' -> host '{}' port {} addrs {:?}", + instance, + resolved_service.host, + resolved_service.port, + resolved_service.addresses, + ); + resolved.insert(instance.to_string()); + let event = ServiceEvent::ServiceResolved(Box::new(resolved_service)); + call_service_listener(&self.service_queriers, ty_domain, event); + } else { + debug!("Resolved service is not valid: {instance}"); + if self.resolved.remove(dns_ptr.alias()) { + removed_instances + .entry(ty_domain.to_string()) + .or_insert_with(HashSet::new) + .insert(instance.to_string()); + } + unresolved.insert(instance.to_string()); + } + } + } + + for instance in resolved.drain() { + self.pending_resolves.remove(&instance); + self.resolved.insert(instance); + } + + for instance in unresolved.drain() { + self.add_pending_resolve(instance); + } + + if !removed_instances.is_empty() { + debug!( + "resolve_updated_instances: removed {}", + &removed_instances.len() + ); + self.notify_service_removal(removed_instances); + } + } + + /// Handle incoming query packets, figure out whether and what to respond. + fn handle_query(&mut self, msg: DnsIncoming, if_index: u32, querier_addr: SocketAddr) { + let querier_ip = querier_addr.ip(); + let is_ipv4 = querier_ip.is_ipv4(); + let sock_opt = if is_ipv4 { + &self.ipv4_sock + } else { + &self.ipv6_sock + }; + let Some(sock) = sock_opt.as_ref() else { + debug!("handle_query: socket not available for intf {}", if_index); + return; + }; + + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); + let mut delayed = false; + + // Special meta-query "_services._dns-sd._udp.". + // See https://datatracker.ietf.org/doc/html/rfc6763#section-9 + const META_QUERY: &str = "_services._dns-sd._udp.local."; + + let Some(dns_registry) = self.dns_registry_map.get_mut(&if_index) else { + debug!("missing dns registry for intf {}", if_index); + return; + }; + + let Some(intf) = self.my_intfs.get(&if_index) else { + debug!("handle_query: no intf found for index {if_index}"); + return; + }; + + for question in msg.questions().iter() { + let qtype = question.entry_type(); + let q_name = question.entry_name(); + + if qtype == RRType::PTR { + // PTR answers are shared records: defer the response unless this + // is a legacy-unicast (source port != 5353) or probe-defense + // (records in the Authority Section) query. + if querier_addr.port() == MDNS_PORT && msg.num_authorities() == 0 { + delayed = true; + } + for service in self.my_services.values() { + if service.get_status(if_index) != ServiceStatus::Announced { + continue; + } + + if service.matches_type_or_subtype(q_name) { + out.add_answer_with_additionals(&msg, service, intf, dns_registry, is_ipv4); + } else if q_name == META_QUERY { + let ttl = service.get_other_ttl(); + let alias = service.get_type().to_string(); + let ptr = DnsPointer::new(q_name, RRType::PTR, CLASS_IN, ttl, alias); + if !out.add_answer(&msg, ptr) { + trace!("answer was not added for meta-query {:?}", &question); + } + } + } + } else { + // Simultaneous Probe Tiebreaking (RFC 6762 section 8.2) + if qtype == RRType::ANY && msg.num_authorities() > 0 { + if let Some(probe) = dns_registry.probing.get_mut(q_name) { + probe.tiebreaking(&msg, q_name); + } + } + + if qtype == RRType::A || qtype == RRType::AAAA || qtype == RRType::ANY { + for service in self.my_services.values() { + if service.get_status(if_index) != ServiceStatus::Announced { + continue; + } + + let service_hostname = dns_registry.resolve_name(service.get_hostname()); + + if service_hostname.to_lowercase() == question.entry_name().to_lowercase() { + // Pick addresses based on the question type, not the + // socket family. RFC 6762 doesn't require A queries + // to come over IPv4 transport — Android's getaddrinfo + // routinely sends both A and AAAA queries over its + // preferred IPv6 mDNS socket and expects A records + // to be answered with v4 addresses. + let mut intf_addrs: Vec = Vec::new(); + if qtype == RRType::A || qtype == RRType::ANY { + intf_addrs.extend(service.get_addrs_on_my_intf_v4(intf)); + } + if qtype == RRType::AAAA || qtype == RRType::ANY { + intf_addrs.extend(service.get_addrs_on_my_intf_v6(intf)); + } + if intf_addrs.is_empty() + && (qtype == RRType::A || qtype == RRType::AAAA) + { + let t = match qtype { + RRType::A => "TYPE_A", + RRType::AAAA => "TYPE_AAAA", + _ => "invalid_type", + }; + trace!( + "Cannot find valid addrs for {} response on intf {:?}", + t, + &intf + ); + continue; + } + for address in intf_addrs { + out.add_answer( + &msg, + DnsAddress::new( + service_hostname, + ip_address_rr_type(&address), + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_host_ttl(), + address, + intf.into(), + ), + ); + } + } + } + } + + let query_name = q_name.to_lowercase(); + let service_opt = self + .my_services + .iter() + .find(|(k, _v)| dns_registry.resolve_name(k.as_str()) == query_name) + .map(|(_, v)| v); + + let Some(service) = service_opt else { + continue; + }; + + if service.get_status(if_index) != ServiceStatus::Announced { + continue; + } + + let intf_addrs = if is_ipv4 { + service.get_addrs_on_my_intf_v4(intf) + } else { + service.get_addrs_on_my_intf_v6(intf) + }; + if intf_addrs.is_empty() { + debug!( + "Cannot find valid addrs for TYPE_SRV response on intf {:?}", + &intf + ); + continue; + } + + add_answer_of_service( + &mut out, + &msg, + question.entry_name(), + service, + qtype, + intf_addrs, + ); + } + } + + // Defer PTR responses (RFC 6762 §6). + if delayed && out.answers_count() > 0 { + out.set_id(msg.id()); + self.increase_counter(Counter::KnownAnswerSuppression, out.known_answer_count()); + let delay = + fastrand::u64(SHARED_RESPONSE_DELAY_MIN_MILLIS..SHARED_RESPONSE_DELAY_MAX_MILLIS); + let next_time = current_time_millis() + delay; + self.delayed_responses.push(DelayedResponse { + next_time, + out, + if_index, + is_ipv4, + }); + self.add_timer(next_time); + return; + } + + if out.answers_count() > 0 { + out.set_id(msg.id()); + + // Pick a source IfAddr on `intf` whose subnet contains the querier's IP. + // It's OK if it's None, `send_dns_outgoing` will then pick one address. + let matched_source = intf + .addrs + .iter() + .find(|if_addr| valid_ip_on_intf(&querier_ip, if_addr)); + + // RFC 6762 §6.7 (Legacy Unicast Responses): if the querier's source + // port is not 5353, it's a one-shot legacy querier (e.g. Android's + // getaddrinfo, iOS resolver fallback). The response MUST be unicast + // back to the querier's source IP and port; multicast replies will + // never reach the querier's ephemeral socket. Legacy unicast + // responses must also echo the question section and clear the + // cache-flush bit, since legacy resolvers don't understand it. + let unicast_dest = if querier_addr.port() != MDNS_PORT { + Some(querier_addr) + } else { + None + }; + + if unicast_dest.is_some() { + for q in msg.questions() { + out.add_question(q.entry_name(), q.entry_type()); + } + out.clear_cache_flush_bits(); + } else if msg.num_authorities() == 0 { + // RFC 6762 §6: a record MUST NOT be multicast on an interface + // more than once per second. Two exceptions skip the limit here: + // - Unicast responses (handled above). + // - Answering probe queries: a probe carries the proposed + // records in its Authority Section, and we MUST defend our + // records immediately so the prober detects the conflict. + dns_registry.apply_multicast_rate_limit(&mut out, current_time_millis(), is_ipv4); + } + + if out.answers_count() > 0 { + debug!("sending response on intf {}", &intf.name); + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = send_dns_outgoing( + &out, + intf, + &sock.pktinfo, + self.port, + matched_source, + unicast_dest, + ) { + let invalid_intf_addr = HashSet::from([intf_addr]); + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addr)); + } + + let if_name = intf.name.clone(); + + self.increase_counter(Counter::Respond, 1); + self.notify_monitors(DaemonEvent::Respond(if_name)); + } + } + + self.increase_counter(Counter::KnownAnswerSuppression, out.known_answer_count()); + } + + /// Multicasts a PTR query response that was deferred per RFC 6762 §6. + /// + /// Re-resolves the socket and interface from `if_index`, so it is safe to + /// call from the timer loop after the borrows taken while building the + /// response are gone. The original querier is no longer known, so the + /// response is always a plain multicast (no unicast destination, no + /// source-address preference); the §6 once-per-second multicast rate limit + /// still applies. + fn send_delayed_response(&mut self, resp: DelayedResponse) { + let DelayedResponse { + mut out, + if_index, + is_ipv4, + .. + } = resp; + + let sock_opt = if is_ipv4 { + &self.ipv4_sock + } else { + &self.ipv6_sock + }; + let Some(sock) = sock_opt.as_ref() else { + debug!("send_delayed_response: socket not available for intf {if_index}"); + return; + }; + + if let Some(dns_registry) = self.dns_registry_map.get_mut(&if_index) { + dns_registry.apply_multicast_rate_limit(&mut out, current_time_millis(), is_ipv4); + } + if out.answers_count() == 0 { + return; + } + + let Some(intf) = self.my_intfs.get(&if_index) else { + debug!("send_delayed_response: no intf found for index {if_index}"); + return; + }; + + let if_name = intf.name.clone(); + debug!("sending delayed response on intf {}", &if_name); + let send_result = send_dns_outgoing(&out, intf, &sock.pktinfo, self.port, None, None); + + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = send_result { + let invalid_intf_addr = HashSet::from([intf_addr]); + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addr)); + } + + self.increase_counter(Counter::Respond, 1); + self.notify_monitors(DaemonEvent::Respond(if_name)); + } + + /// Increases the value of `counter` by `count`. + fn increase_counter(&mut self, counter: Counter, count: i64) { + let key = counter.to_string(); + match self.counters.get_mut(&key) { + Some(v) => *v += count, + None => { + self.counters.insert(key, count); + } + } + } + + /// Sets the value of `counter` to `count`. + fn set_counter(&mut self, counter: Counter, count: i64) { + let key = counter.to_string(); + self.counters.insert(key, count); + } + + fn signal_sock_drain(&self) { + let mut signal_buf = [0; 1024]; + + // This recv is non-blocking as the socket is non-blocking. + while let Ok(sz) = self.signal_sock.recv(&mut signal_buf) { + trace!( + "signal socket recvd: {}", + String::from_utf8_lossy(&signal_buf[0..sz]) + ); + } + } + + fn add_retransmission(&mut self, next_time: u64, command: Command) { + self.retransmissions.push(ReRun { next_time, command }); + self.add_timer(next_time); + } + + /// Sends service removal event to listeners for expired service records. + /// `expired`: map of service type domain to set of instance names. + fn notify_service_removal(&self, expired: HashMap>) { + for (ty_domain, sender) in self.service_queriers.iter() { + if let Some(instances) = expired.get(ty_domain) { + for instance_name in instances { + let event = ServiceEvent::ServiceRemoved( + ty_domain.to_string(), + instance_name.to_string(), + ); + match sender.send(event) { + Ok(()) => debug!("notify_service_removal: sent ServiceRemoved to listener of {ty_domain}: {instance_name}"), + Err(e) => debug!("Failed to send event: {}", e), + } + } + } + } + } + + /// The entry point that executes all commands received by the daemon. + /// + /// `repeating`: whether this is a retransmission. + fn exec_command(&mut self, command: Command, repeating: bool) { + trace!("exec_command: {:?} repeating: {}", &command, repeating); + match command { + Command::Browse(ty, next_delay, cache_only, listener) => { + self.exec_command_browse(repeating, ty, next_delay, cache_only, listener); + } + + Command::ResolveHostname(hostname, next_delay, listener, timeout) => { + self.exec_command_resolve_hostname( + repeating, hostname, next_delay, listener, timeout, + ); + } + + Command::Register(service_info) => { + self.register_service(*service_info); + self.increase_counter(Counter::Register, 1); + } + + Command::RegisterResend(fullname, intf) => { + trace!("register-resend service: {fullname} on {}", &intf); + if let Err(InternalError::IntfAddrInvalid(intf_addr)) = + self.exec_command_register_resend(fullname, intf) + { + let invalid_intf_addr = HashSet::from([intf_addr]); + let _ = self.send_cmd_to_self(Command::InvalidIntfAddrs(invalid_intf_addr)); + } + } + + Command::Unregister(fullname, resp_s) => { + trace!("unregister service {} repeat {}", &fullname, &repeating); + self.exec_command_unregister(repeating, fullname, resp_s); + } + + Command::UnregisterResend(packet, if_index, is_ipv4) => { + self.exec_command_unregister_resend(packet, if_index, is_ipv4); + } + + Command::StopBrowse(ty_domain) => self.exec_command_stop_browse(ty_domain), + + Command::StopResolveHostname(hostname) => { + self.exec_command_stop_resolve_hostname(hostname.to_lowercase()) + } + + Command::Resolve(instance, try_count) => self.exec_command_resolve(instance, try_count), + + Command::GetMetrics(resp_s) => self.exec_command_get_metrics(resp_s), + + Command::GetStatus(resp_s) => match resp_s.send(self.status.clone()) { + Ok(()) => trace!("Sent status to the client"), + Err(e) => debug!("Failed to send status: {}", e), + }, + + Command::Monitor(resp_s) => { + self.monitors.push(resp_s); + } + + Command::SetOption(daemon_opt) => { + self.process_set_option(daemon_opt); + } + + Command::GetOption(resp_s) => { + let val = DaemonOptionVal { + _service_name_len_max: self.service_name_len_max, + ip_check_interval: self.ip_check_interval, + }; + if let Err(e) = resp_s.send(val) { + debug!("Failed to send options: {}", e); + } + } + + Command::Verify(instance_fullname, timeout) => { + self.exec_command_verify(instance_fullname, timeout, repeating); + } + + Command::InvalidIntfAddrs(invalid_intf_addrs) => { + for intf_addr in invalid_intf_addrs { + self.del_interface_addr(&intf_addr); + } + + self.check_ip_changes(); + } + + _ => { + debug!("unexpected command: {:?}", &command); + } + } + } + + fn exec_command_get_metrics(&mut self, resp_s: Sender>) { + self.set_counter(Counter::CachedPTR, self.cache.ptr_count() as i64); + self.set_counter(Counter::CachedSRV, self.cache.srv_count() as i64); + self.set_counter(Counter::CachedAddr, self.cache.addr_count() as i64); + self.set_counter(Counter::CachedTxt, self.cache.txt_count() as i64); + self.set_counter(Counter::CachedNSec, self.cache.nsec_count() as i64); + self.set_counter(Counter::CachedSubtype, self.cache.subtype_count() as i64); + self.set_counter(Counter::Timer, self.timers.len() as i64); + + let dns_registry_probe_count: usize = self + .dns_registry_map + .values() + .map(|r| r.probing.len()) + .sum(); + self.set_counter(Counter::DnsRegistryProbe, dns_registry_probe_count as i64); + + let dns_registry_active_count: usize = self + .dns_registry_map + .values() + .map(|r| r.active.values().map(|a| a.len()).sum::()) + .sum(); + self.set_counter(Counter::DnsRegistryActive, dns_registry_active_count as i64); + + let dns_registry_timer_count: usize = self + .dns_registry_map + .values() + .map(|r| r.new_timers.len()) + .sum(); + self.set_counter(Counter::DnsRegistryTimer, dns_registry_timer_count as i64); + + let dns_registry_name_change_count: usize = self + .dns_registry_map + .values() + .map(|r| r.name_changes.len()) + .sum(); + self.set_counter( + Counter::DnsRegistryNameChange, + dns_registry_name_change_count as i64, + ); + + // Send the metrics to the client. + if let Err(e) = resp_s.send(self.counters.clone()) { + debug!("Failed to send metrics: {}", e); + } + } + + fn exec_command_browse( + &mut self, + repeating: bool, + ty: String, + next_delay: u32, + cache_only: bool, + listener: Sender, + ) { + let pretty_addrs: Vec = self + .my_intfs + .iter() + .map(|(if_index, itf)| format!("{} ({if_index})", itf.name)) + .collect(); + + if let Err(e) = listener.send(ServiceEvent::SearchStarted(format!( + "{ty} on {} interfaces [{}]", + pretty_addrs.len(), + pretty_addrs.join(", ") + ))) { + debug!( + "Failed to send SearchStarted({})(repeating:{}): {}", + &ty, repeating, e + ); + return; + } + + let now = current_time_millis(); + if !repeating { + // Binds a `listener` to querying mDNS domain type `ty`. + // + // If there is already a `listener`, it will be updated, i.e. overwritten. + self.service_queriers.insert(ty.clone(), listener.clone()); + + // if we already have the records in our cache, just send them + self.query_cache_for_service(&ty, &listener, now); + } + + if cache_only { + // If cache_only is true, we do not send a query. + match listener.send(ServiceEvent::SearchStopped(ty.clone())) { + Ok(()) => debug!("SearchStopped sent for {}", &ty), + Err(e) => debug!("Failed to send SearchStopped: {}", e), + } + return; + } + + if !repeating { + // RFC 6762 §5.2: delay the first query by a random jitter. + let jitter = + fastrand::u64(INITIAL_QUERY_DELAY_MIN_MILLIS..INITIAL_QUERY_DELAY_MAX_MILLIS); + self.add_retransmission(now + jitter, Command::Browse(ty, 1, cache_only, listener)); + return; + } + + self.send_query(&ty, RRType::PTR); + self.increase_counter(Counter::Browse, 1); + + let next_time = now + (next_delay * 1000) as u64; + let max_delay = 60 * 60; + let delay = cmp::min(next_delay * 2, max_delay); + self.add_retransmission(next_time, Command::Browse(ty, delay, cache_only, listener)); + } + + fn exec_command_resolve_hostname( + &mut self, + repeating: bool, + hostname: String, + next_delay: u32, + listener: Sender, + timeout: Option, + ) { + let addr_list: Vec<_> = self.my_intfs.iter().collect(); + if let Err(e) = listener.send(HostnameResolutionEvent::SearchStarted(format!( + "{} on addrs {:?}", + &hostname, &addr_list + ))) { + debug!( + "Failed to send ResolveStarted({})(repeating:{}): {}", + &hostname, repeating, e + ); + return; + } + let now = current_time_millis(); + if !repeating { + self.add_hostname_resolver(hostname.to_owned(), listener.clone(), timeout); + // if we already have the records in our cache, just send them + self.query_cache_for_hostname(&hostname, listener.clone()); + + // RFC 6762 §5.2: delay the first query by a random jitter. + let jitter = + fastrand::u64(INITIAL_QUERY_DELAY_MIN_MILLIS..INITIAL_QUERY_DELAY_MAX_MILLIS); + self.add_retransmission( + now + jitter, + Command::ResolveHostname(hostname, 1, listener, None), + ); + return; + } + + self.send_query_vec(&[(&hostname, RRType::A), (&hostname, RRType::AAAA)]); + self.increase_counter(Counter::ResolveHostname, 1); + + let next_time = now + u64::from(next_delay) * 1000; + let max_delay = 60 * 60; + let delay = cmp::min(next_delay * 2, max_delay); + + // Only add retransmission if it does not exceed the hostname resolver timeout, if any. + if self + .hostname_resolvers + .get(&hostname) + .and_then(|(_sender, timeout)| *timeout) + .map(|timeout| next_time < timeout) + .unwrap_or(true) + { + self.add_retransmission( + next_time, + Command::ResolveHostname(hostname, delay, listener, None), + ); + } + } + + fn exec_command_resolve(&mut self, instance: String, try_count: u16) { + let pending_query = self.query_unresolved(&instance); + let max_try = 3; + if pending_query && try_count < max_try { + // Note that if the current try already succeeds, the next retransmission + // will be no-op as the cache has been updated. + let next_time = current_time_millis() + RESOLVE_WAIT_IN_MILLIS; + self.add_retransmission(next_time, Command::Resolve(instance, try_count + 1)); + } + } + + fn exec_command_unregister( + &mut self, + repeating: bool, + fullname: String, + resp_s: Sender, + ) { + let response = match self.my_services.remove_entry(&fullname) { + None => { + debug!("unregister: cannot find such service {}", &fullname); + UnregisterStatus::NotFound + } + Some((_k, info)) => { + let mut timers = Vec::new(); + + for (if_index, intf) in self.my_intfs.iter() { + if let Some(sock) = self.ipv4_sock.as_ref() { + let packet = self.unregister_service(&info, intf, &sock.pktinfo); + // repeat for one time just in case some peers miss the message + if !repeating && !packet.is_empty() { + let next_time = current_time_millis() + 120; + self.retransmissions.push(ReRun { + next_time, + command: Command::UnregisterResend(packet, *if_index, true), + }); + timers.push(next_time); + } + } + + // ipv6 + if let Some(sock) = self.ipv6_sock.as_ref() { + let packet = self.unregister_service(&info, intf, &sock.pktinfo); + if !repeating && !packet.is_empty() { + let next_time = current_time_millis() + 120; + self.retransmissions.push(ReRun { + next_time, + command: Command::UnregisterResend(packet, *if_index, false), + }); + timers.push(next_time); + } + } + } + + for t in timers { + self.add_timer(t); + } + + self.increase_counter(Counter::Unregister, 1); + UnregisterStatus::OK + } + }; + if let Err(e) = resp_s.send(response) { + debug!("unregister: failed to send response: {}", e); + } + } + + fn exec_command_unregister_resend(&mut self, packet: Vec, if_index: u32, is_ipv4: bool) { + let Some(intf) = self.my_intfs.get(&if_index) else { + return; + }; + let sock_opt = if is_ipv4 { + &self.ipv4_sock + } else { + &self.ipv6_sock + }; + let Some(sock) = sock_opt else { + return; + }; + + let if_addr = if is_ipv4 { + match intf.next_ifaddr_v4() { + Some(addr) => addr, + None => return, + } + } else { + match intf.next_ifaddr_v6() { + Some(addr) => addr, + None => return, + } + }; + + debug!("UnregisterResend from {:?}", if_addr); + multicast_on_intf( + &packet[..], + &intf.name, + intf.index, + if_addr, + &sock.pktinfo, + self.port, + ); + + self.increase_counter(Counter::UnregisterResend, 1); + } + + fn exec_command_stop_browse(&mut self, ty_domain: String) { + match self.service_queriers.remove_entry(&ty_domain) { + None => debug!("StopBrowse: cannot find querier for {}", &ty_domain), + Some((ty, sender)) => { + // Remove pending browse commands in the reruns. + trace!("StopBrowse: removed queryer for {}", &ty); + let mut i = 0; + while i < self.retransmissions.len() { + if let Command::Browse(t, _, _, _) = &self.retransmissions[i].command { + if t == &ty { + self.retransmissions.remove(i); + trace!("StopBrowse: removed retransmission for {}", &ty); + continue; + } + } + i += 1; + } + + // Remove cache entries. + self.cache.remove_service_type(&ty_domain); + + // Notify the client. + match sender.send(ServiceEvent::SearchStopped(ty_domain)) { + Ok(()) => trace!("Sent SearchStopped to the listener"), + Err(e) => debug!("Failed to send SearchStopped: {}", e), + } + } + } + } + + fn exec_command_stop_resolve_hostname(&mut self, hostname: String) { + if let Some((host, (sender, _timeout))) = self.hostname_resolvers.remove_entry(&hostname) { + // Remove pending resolve commands in the reruns. + trace!("StopResolve: removed queryer for {}", &host); + let mut i = 0; + while i < self.retransmissions.len() { + if let Command::Resolve(t, _) = &self.retransmissions[i].command { + if t == &host { + self.retransmissions.remove(i); + trace!("StopResolve: removed retransmission for {}", &host); + continue; + } + } + i += 1; + } + + // Notify the client. + match sender.send(HostnameResolutionEvent::SearchStopped(hostname)) { + Ok(()) => trace!("Sent SearchStopped to the listener"), + Err(e) => debug!("Failed to send SearchStopped: {}", e), + } + } + } + + fn exec_command_register_resend(&mut self, fullname: String, if_index: u32) -> MyResult<()> { + let Some(info) = self.my_services.get_mut(&fullname) else { + trace!("announce: cannot find such service {}", &fullname); + return Ok(()); + }; + + let Some(dns_registry) = self.dns_registry_map.get_mut(&if_index) else { + return Ok(()); + }; + + let Some(intf) = self.my_intfs.get(&if_index) else { + return Ok(()); + }; + + let announced_v4 = if let Some(sock) = self.ipv4_sock.as_ref() { + announce_service_on_intf(dns_registry, info, intf, &sock.pktinfo, self.port)? + } else { + false + }; + let announced_v6 = if let Some(sock) = self.ipv6_sock.as_ref() { + announce_service_on_intf(dns_registry, info, intf, &sock.pktinfo, self.port)? + } else { + false + }; + + if announced_v4 || announced_v6 { + let hostname = dns_registry.resolve_name(info.get_hostname()); + let service_name = dns_registry.resolve_name(&fullname).to_string(); + + debug!("resend: announce service {service_name} on {}", intf.name); + + notify_monitors( + &mut self.monitors, + DaemonEvent::Announce(service_name, format!("{}:{}", hostname, &intf.name)), + ); + info.set_status(if_index, ServiceStatus::Announced); + } else { + debug!("register-resend should not fail"); + } + + self.increase_counter(Counter::RegisterResend, 1); + Ok(()) + } + + fn exec_command_verify(&mut self, instance: String, timeout: Duration, repeating: bool) { + /* + RFC 6762 section 10.4: + ... + When the cache receives this hint that it should reconfirm some + record, it MUST issue two or more queries for the resource record in + dispute. If no response is received within ten seconds, then, even + though its TTL may indicate that it is not yet due to expire, that + record SHOULD be promptly flushed from the cache. + */ + let now = current_time_millis(); + let expire_at = if repeating { + None + } else { + Some(now + timeout.as_millis() as u64) + }; + + // send query for the resource records. + let record_vec = self.cache.service_verify_queries(&instance, expire_at); + + if !record_vec.is_empty() { + let query_vec: Vec<(&str, RRType)> = record_vec + .iter() + .map(|(record, rr_type)| (record.as_str(), *rr_type)) + .collect(); + self.send_query_vec(&query_vec); + + if let Some(new_expire) = expire_at { + self.add_timer(new_expire); // ensure a check for the new expire time. + + // schedule a resend 1 second later + self.add_retransmission(now + 1000, Command::Verify(instance, timeout)); + } + } + } + + /// Refresh cached service records with active queriers + fn refresh_active_services(&mut self) { + let mut query_ptr_count = 0; + let mut query_srv_count = 0; + let mut new_timers = HashSet::new(); + let mut query_addr_count = 0; + + for (ty_domain, _sender) in self.service_queriers.iter() { + let refreshed_timers = self.cache.refresh_due_ptr(ty_domain); + if !refreshed_timers.is_empty() { + trace!("sending refresh query for PTR: {}", ty_domain); + self.send_query(ty_domain, RRType::PTR); + query_ptr_count += 1; + new_timers.extend(refreshed_timers); + } + + let (instances, timers) = self.cache.refresh_due_srv_txt(ty_domain); + for (instance, types) in instances { + trace!("sending refresh query for: {}", &instance); + let query_vec = types + .into_iter() + .map(|ty| (instance.as_str(), ty)) + .collect::>(); + self.send_query_vec(&query_vec); + query_srv_count += 1; + } + new_timers.extend(timers); + let (hostnames, timers) = self.cache.refresh_due_hosts(ty_domain); + for hostname in hostnames.iter() { + trace!("sending refresh queries for A and AAAA: {}", hostname); + self.send_query_vec(&[(hostname, RRType::A), (hostname, RRType::AAAA)]); + query_addr_count += 2; + } + new_timers.extend(timers); + } + + for timer in new_timers { + self.add_timer(timer); + } + + self.increase_counter(Counter::CacheRefreshPTR, query_ptr_count); + self.increase_counter(Counter::CacheRefreshSrvTxt, query_srv_count); + self.increase_counter(Counter::CacheRefreshAddr, query_addr_count); + } +} + +/// Adds one or more answers of a service for incoming msg and RR entry name. +fn add_answer_of_service( + out: &mut DnsOutgoing, + msg: &DnsIncoming, + entry_name: &str, + service: &ServiceInfo, + qtype: RRType, + intf_addrs: Vec, +) { + if qtype == RRType::SRV || qtype == RRType::ANY { + out.add_answer( + msg, + DnsSrv::new( + entry_name, + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_host_ttl(), + service.get_priority(), + service.get_weight(), + service.get_port(), + service.get_hostname().to_string(), + ), + ); + } + + if qtype == RRType::TXT || qtype == RRType::ANY { + out.add_answer( + msg, + DnsTxt::new( + entry_name, + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_other_ttl(), + service.generate_txt(), + ), + ); + } + + if qtype == RRType::SRV { + for address in intf_addrs { + out.add_additional_answer(DnsAddress::new( + service.get_hostname(), + ip_address_rr_type(&address), + CLASS_IN | CLASS_CACHE_FLUSH, + service.get_host_ttl(), + address, + InterfaceId::default(), + )); + } + } +} + +/// All possible events sent to the client from the daemon +/// regarding service discovery. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ServiceEvent { + /// Started searching for a service type. + SearchStarted(String), + + /// Found a specific (service_type, fullname). + ServiceFound(String, String), + + /// Resolved a service instance in a ResolvedService struct. + ServiceResolved(Box), + + /// A service instance (service_type, fullname) was removed. + ServiceRemoved(String, String), + + /// Stopped searching for a service type. + SearchStopped(String), +} + +/// All possible events sent to the client from the daemon +/// regarding host resolution. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum HostnameResolutionEvent { + /// Started searching for the ip address of a hostname. + SearchStarted(String), + /// One or more addresses for a hostname has been found. + AddressesFound(String, HashSet), + /// One or more addresses for a hostname has been removed. + AddressesRemoved(String, HashSet), + /// The search for the ip address of a hostname has timed out. + SearchTimeout(String), + /// Stopped searching for the ip address of a hostname. + SearchStopped(String), +} + +/// Some notable events from the daemon besides [`ServiceEvent`]. +/// These events are expected to happen infrequently. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum DaemonEvent { + /// Daemon unsolicitly announced a service from an interface. + Announce(String, String), + + /// Daemon encountered an error. + Error(Error), + + /// Daemon detected a new IP address from the host. + IpAdd(IpAddr), + + /// Daemon detected a IP address removed from the host. + IpDel(IpAddr), + + /// Daemon resolved a name conflict by changing one of its names. + /// see [DnsNameChange] for more details. + NameChange(DnsNameChange), + + /// Send out a multicast response via an interface. + Respond(String), +} + +/// Represents a name change due to a name conflict resolution. +/// See [RFC 6762 section 9](https://datatracker.ietf.org/doc/html/rfc6762#section-9) +#[derive(Clone, Debug)] +pub struct DnsNameChange { + /// The original name set in `ServiceInfo` by the user. + pub original: String, + + /// A new name is created by appending a suffix after the original name. + /// + /// - for a service instance name, the suffix is `(N)`, where N starts at 2. + /// - for a host name, the suffix is `-N`, where N starts at 2. + /// + /// For example: + /// + /// - Service name `foo._service-type._udp` becomes `foo (2)._service-type._udp` + /// - Host name `foo.local.` becomes `foo-2.local.` + pub new_name: String, + + /// The resource record type + pub rr_type: RRType, + + /// The interface where the name conflict and its change happened. + pub intf_name: String, +} + +/// Commands supported by the daemon +#[derive(Debug)] +enum Command { + /// Browsing for a service type (ty_domain, next_time_delay_in_seconds, channel::sender) + Browse(String, u32, bool, Sender), + + /// Resolve a hostname to IP addresses. + ResolveHostname(String, u32, Sender, Option), // (hostname, next_time_delay_in_seconds, sender, timeout_in_milliseconds) + + /// Register a service + Register(Box), + + /// Unregister a service + Unregister(String, Sender), // (fullname) + + /// Announce again a service to local network + RegisterResend(String, u32), // (fullname) + + /// Resend unregister packet. + UnregisterResend(Vec, u32, bool), // (packet content, if_index, is_ipv4) + + /// Stop browsing a service type + StopBrowse(String), // (ty_domain) + + /// Stop resolving a hostname + StopResolveHostname(String), // (hostname) + + /// Send query to resolve a service instance. + /// This is used when a PTR record exists but SRV & TXT records are missing. + Resolve(String, u16), // (service_instance_fullname, try_count) + + /// Read the current values of the counters + GetMetrics(Sender), + + /// Get the current status of the daemon. + GetStatus(Sender), + + /// Monitor noticeable events in the daemon. + Monitor(Sender), + + SetOption(DaemonOption), + + GetOption(Sender), + + /// Proactively confirm a DNS resource record. + /// + /// The intention is to check if a service name or IP address still valid + /// before its TTL expires. + Verify(String, Duration), + + /// Invalidate some interface addresses. + InvalidIntfAddrs(HashSet), + + Exit(Sender), +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Browse(_, _, _, _) => write!(f, "Command Browse"), + Self::ResolveHostname(_, _, _, _) => write!(f, "Command ResolveHostname"), + Self::Exit(_) => write!(f, "Command Exit"), + Self::GetStatus(_) => write!(f, "Command GetStatus"), + Self::GetMetrics(_) => write!(f, "Command GetMetrics"), + Self::Monitor(_) => write!(f, "Command Monitor"), + Self::Register(_) => write!(f, "Command Register"), + Self::RegisterResend(_, _) => write!(f, "Command RegisterResend"), + Self::SetOption(_) => write!(f, "Command SetOption"), + Self::GetOption(_) => write!(f, "Command GetOption"), + Self::StopBrowse(_) => write!(f, "Command StopBrowse"), + Self::StopResolveHostname(_) => write!(f, "Command StopResolveHostname"), + Self::Unregister(_, _) => write!(f, "Command Unregister"), + Self::UnregisterResend(_, _, _) => write!(f, "Command UnregisterResend"), + Self::Resolve(_, _) => write!(f, "Command Resolve"), + Self::Verify(_, _) => write!(f, "Command VerifyResource"), + Self::InvalidIntfAddrs(_) => write!(f, "Command InvalidIntfAddrs"), + } + } +} + +struct DaemonOptionVal { + _service_name_len_max: u8, + ip_check_interval: u64, +} + +#[derive(Debug)] +enum DaemonOption { + ServiceNameLenMax(u8), + IpCheckInterval(u64), + MaxPacketSize(Vec, usize), + EnableInterface(Vec), + DisableInterface(Vec), + MulticastLoopV4(bool), + MulticastLoopV6(bool), + AcceptUnsolicited(bool), + IncludeAppleP2P(bool), + #[cfg(test)] + TestDownInterface(String), + #[cfg(test)] + TestUpInterface(String), +} + +/// 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 "_.." string. +fn check_service_name_length(ty_domain: &str, limit: u8) -> Result<()> { + if ty_domain.len() <= DOMAIN_LEN + 1 { + // service name cannot be empty or only '_'. + return Err(e_fmt!("Service type name cannot be empty: {}", ty_domain)); + } + + 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(()) +} + +/// Checks if `name` ends with a valid domain: '._tcp.local.' or '._udp.local.' +fn check_domain_suffix(name: &str) -> Result<()> { + if !(name.ends_with("._tcp.local.") || name.ends_with("._udp.local.")) { + return Err(e_fmt!( + "mDNS service {} must end with '._tcp.local.' or '._udp.local.'", + name + )); + } + + Ok(()) +} + +/// Validate the service name in a fully qualified name. +/// +/// A Full Name = .. +/// The only `` 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<()> { + check_domain_suffix(fullname)?; + + 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] != "_" { + return Err(e_fmt!("Service name must start with '_'")); + } + + let name = &name[1..]; + + if name.contains("--") { + return Err(e_fmt!("Service name must not contain '--'")); + } + + if name.starts_with('-') || name.ends_with('-') { + return Err(e_fmt!("Service name (%s) may not start or end with '-'")); + } + + let ascii_count = name.chars().filter(|c| c.is_ascii_alphabetic()).count(); + if ascii_count < 1 { + return Err(e_fmt!( + "Service name must contain at least one letter (eg: 'A-Za-z')" + )); + } + + Ok(()) +} + +/// Validate a hostname. +fn check_hostname(hostname: &str) -> Result<()> { + if !hostname.ends_with(".local.") { + return Err(e_fmt!("Hostname must end with '.local.': {hostname}")); + } + + if hostname == ".local." { + return Err(e_fmt!( + "The part of the hostname before '.local.' cannot be empty" + )); + } + + if hostname.len() > 255 { + return Err(e_fmt!("Hostname length must be <= 255 bytes")); + } + + Ok(()) +} + +fn call_service_listener( + listeners_map: &HashMap>, + ty_domain: &str, + event: ServiceEvent, +) { + if let Some(listener) = listeners_map.get(ty_domain) { + match listener.send(event) { + Ok(()) => trace!("Sent event to listener successfully"), + Err(e) => debug!("Failed to send event: {}", e), + } + } +} + +fn call_hostname_resolution_listener( + listeners_map: &HashMap, Option)>, + hostname: &str, + event: HostnameResolutionEvent, +) { + let hostname_lower = hostname.to_lowercase(); + if let Some(listener) = listeners_map.get(&hostname_lower).map(|(l, _)| l) { + match listener.send(event) { + Ok(()) => trace!("Sent event to listener successfully"), + Err(e) => debug!("Failed to send event: {}", e), + } + } +} + +/// Returns valid network interfaces in the host system. +/// Operational down interfaces are excluded. +/// Loopback interfaces are excluded if `with_loopback` is false. +fn my_ip_interfaces(with_loopback: bool) -> Vec { + my_ip_interfaces_inner(with_loopback, false) +} + +fn my_ip_interfaces_inner(with_loopback: bool, with_apple_p2p: bool) -> Vec { + if_addrs::get_if_addrs() + .unwrap_or_default() + .into_iter() + .filter(|i| { + i.is_oper_up() + && !i.is_p2p() + && (!i.is_loopback() || with_loopback) + && (with_apple_p2p || !is_apple_p2p_by_name(&i.name)) + }) + .collect() +} + +/// Checks if the interface name indicates it's an Apple peer-to-peer interface, +/// which should be ignored by default. +fn is_apple_p2p_by_name(name: &str) -> bool { + let p2p_prefixes = ["awdl", "llw"]; + p2p_prefixes.iter().any(|prefix| name.starts_with(prefix)) +} + +/// How to encode and where to send outgoing messages on one interface. +#[derive(Clone, Copy, Debug)] +struct SendConfig { + /// The mDNS port to send to. + port: u16, + + /// Max byte size of a generated packet. + /// See [`ServiceDaemon::set_max_packet_size`]. + max_packet_size: usize, + + /// Whether the packets go out over IPv4, which decides their absolute + /// ceiling: see [`max_pkt_absolute`]. + is_ipv4: bool, +} + +/// Send an outgoing mDNS query or response, and returns the packet bytes. +/// Returns empty vec if no valid interface address is found. +fn send_dns_outgoing( + out: &DnsOutgoing, + my_intf: &MyIntf, + sock: &PktInfoUdpSocket, + port: u16, + source: Option<&IfAddr>, + unicast_dest: Option, +) -> MyResult>> { + let if_name = &my_intf.name; + + let if_addr = match source { + Some(addr) => addr, + None => { + if sock.domain() == Domain::IPV4 { + match my_intf.next_ifaddr_v4() { + Some(addr) => addr, + None => return Ok(vec![]), + } + } else { + match my_intf.next_ifaddr_v6() { + Some(addr) => addr, + None => return Ok(vec![]), + } + } + } + }; + + // The limits are per address family, so read them off the address we send from. + let is_ipv4 = if_addr.ip().is_ipv4(); + let config = SendConfig { + port, + max_packet_size: my_intf.max_packet_size(is_ipv4), + is_ipv4, + }; + + send_dns_outgoing_impl( + out, + if_name, + my_intf.index, + if_addr, + sock, + config, + unicast_dest, + ) +} + +/// Send an outgoing mDNS query or response, and returns the packet bytes. +fn send_dns_outgoing_impl( + out: &DnsOutgoing, + if_name: &str, + if_index: u32, + if_addr: &IfAddr, + sock: &PktInfoUdpSocket, + config: SendConfig, + unicast_dest: Option, +) -> MyResult>> { + let qtype = if out.is_query() { + "query" + } else { + if out.answers_count() == 0 && out.additionals().is_empty() { + return Ok(vec![]); // no need to send empty response + } + "response" + }; + trace!( + "send {}: {} questions {} answers {} authorities {} additional", + qtype, + out.questions().len(), + out.answers_count(), + out.authorities().len(), + out.additionals().len() + ); + + match if_addr.ip() { + IpAddr::V4(ipv4) => { + if let Err(e) = sock.set_multicast_if_v4(&ipv4) { + debug!( + "send_dns_outgoing: failed to set multicast interface for IPv4 {}: {}", + ipv4, e + ); + // cannot send without a valid interface + if e.kind() == std::io::ErrorKind::AddrNotAvailable { + let intf_addr = Interface { + name: if_name.to_string(), + addr: if_addr.clone(), + index: Some(if_index), + oper_status: if_addrs::IfOperStatus::Down, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + return Err(InternalError::IntfAddrInvalid(intf_addr)); + } + return Ok(vec![]); // non-fatal other failure + } + } + IpAddr::V6(ipv6) => { + if let Err(e) = sock.set_multicast_if_v6(if_index) { + debug!( + "send_dns_outgoing: failed to set multicast interface for IPv6 {}: {}", + ipv6, e + ); + // cannot send without a valid interface + if e.kind() == std::io::ErrorKind::AddrNotAvailable { + let intf_addr = Interface { + name: if_name.to_string(), + addr: if_addr.clone(), + index: Some(if_index), + oper_status: if_addrs::IfOperStatus::Down, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + return Err(InternalError::IntfAddrInvalid(intf_addr)); + } + return Ok(vec![]); // non-fatal other failure + } + } + } + + let packet_list = out.to_data_on_wire(config.max_packet_size, config.is_ipv4); + for packet in packet_list.iter() { + match unicast_dest { + Some(dest) => unicast_on_intf(packet, if_name, dest, sock), + None => multicast_on_intf(packet, if_name, if_index, if_addr, sock, config.port), + } + } + Ok(packet_list) +} + +/// Sends a unicast packet directly to `dest` (used for RFC 6762 §6.7 +/// legacy unicast responses). +fn unicast_on_intf(packet: &[u8], if_name: &str, dest: SocketAddr, socket: &PktInfoUdpSocket) { + let max_size = max_pkt_absolute(dest.is_ipv4()); + if packet.len() > max_size { + debug!("Drop over-sized packet ({} > {max_size})", packet.len()); + return; + } + + let sock_addr = dest.into(); + match socket.send_to(packet, &sock_addr) { + Ok(sz) => trace!( + "sent unicast {} bytes on interface {} to {}", + sz, + if_name, + dest + ), + Err(e) => trace!( + "Failed to send unicast to {} via {:?}: {}", + dest, + &if_name, + e + ), + } +} + +/// Sends a multicast packet, and returns the packet bytes. +fn multicast_on_intf( + packet: &[u8], + if_name: &str, + if_index: u32, + if_addr: &IfAddr, + socket: &PktInfoUdpSocket, + port: u16, +) { + let max_size = max_pkt_absolute(if_addr.ip().is_ipv4()); + if packet.len() > max_size { + debug!("Drop over-sized packet ({} > {max_size})", packet.len()); + return; + } + + let addr: SocketAddr = match if_addr { + if_addrs::IfAddr::V4(_) => SocketAddrV4::new(GROUP_ADDR_V4, port).into(), + if_addrs::IfAddr::V6(_) => { + let mut sock = SocketAddrV6::new(GROUP_ADDR_V6, port, 0, 0); + sock.set_scope_id(if_index); // Choose iface for multicast + sock.into() + } + }; + + // Sends out `packet` to `addr` on the socket. + let sock_addr = addr.into(); + match socket.send_to(packet, &sock_addr) { + Ok(sz) => trace!( + "sent out {} bytes on interface {} (idx {}) addr {}", + sz, + if_name, + if_index, + if_addr.ip() + ), + Err(e) => trace!("Failed to send to {} via {:?}: {}", addr, &if_name, e), + } +} + +/// Returns true if `name` is a valid instance name of format: +/// ..<_udp|_tcp>.local. +/// Note: could contain '.' as well. +fn valid_instance_name(name: &str) -> bool { + name.split('.').count() >= 5 +} + +fn notify_monitors(monitors: &mut Vec>, event: DaemonEvent) { + monitors.retain(|sender| { + if let Err(e) = sender.try_send(event.clone()) { + debug!("notify_monitors: try_send: {}", &e); + if matches!(e, TrySendError::Disconnected(_)) { + return false; // This monitor is dropped. + } + } + true + }); +} + +/// Check if all unique records passed "probing", and if yes, create a packet +/// to announce the service. +fn prepare_announce( + info: &ServiceInfo, + intf: &MyIntf, + dns_registry: &mut DnsRegistry, + is_ipv4: bool, +) -> Option { + let intf_addrs = if is_ipv4 { + info.get_addrs_on_my_intf_v4(intf) + } else { + info.get_addrs_on_my_intf_v6(intf) + }; + + if intf_addrs.is_empty() { + debug!( + "prepare_announce (ipv4: {is_ipv4}): no valid addrs on interface {}", + &intf.name + ); + return None; + } + + // check if we changed our name due to conflicts. + let service_fullname = dns_registry.resolve_name(info.get_fullname()); + + debug!( + "prepare to announce service {service_fullname} on {:?}", + &intf_addrs + ); + + let mut probing_count = 0; + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); + let create_time = current_time_millis() + fastrand::u64(0..250); + + out.add_answer_at_time( + DnsPointer::new( + info.get_type(), + RRType::PTR, + CLASS_IN, + info.get_other_ttl(), + service_fullname.to_string(), + ), + 0, + ); + + if let Some(sub) = info.get_subtype() { + trace!("Adding subdomain {}", sub); + out.add_answer_at_time( + DnsPointer::new( + sub, + RRType::PTR, + CLASS_IN, + info.get_other_ttl(), + service_fullname.to_string(), + ), + 0, + ); + } + + // SRV records. + let hostname = dns_registry.resolve_name(info.get_hostname()).to_string(); + + let mut srv = DnsSrv::new( + info.get_fullname(), + CLASS_IN | CLASS_CACHE_FLUSH, + info.get_host_ttl(), + info.get_priority(), + info.get_weight(), + info.get_port(), + hostname, + ); + + if let Some(new_name) = dns_registry.name_changes.get(info.get_fullname()) { + srv.get_record_mut().set_new_name(new_name.to_string()); + } + + if !info.requires_probe() + || dns_registry.is_probing_done(&srv, info.get_fullname(), create_time) + { + out.add_answer_at_time(srv, 0); + } else { + probing_count += 1; + } + + // TXT records. + + let mut txt = DnsTxt::new( + info.get_fullname(), + CLASS_IN | CLASS_CACHE_FLUSH, + info.get_other_ttl(), + info.generate_txt(), + ); + + if let Some(new_name) = dns_registry.name_changes.get(info.get_fullname()) { + txt.get_record_mut().set_new_name(new_name.to_string()); + } + + if !info.requires_probe() + || dns_registry.is_probing_done(&txt, info.get_fullname(), create_time) + { + out.add_answer_at_time(txt, 0); + } else { + probing_count += 1; + } + + // Address records. (A and AAAA) + + let hostname = info.get_hostname(); + for address in intf_addrs { + let mut dns_addr = DnsAddress::new( + hostname, + ip_address_rr_type(&address), + CLASS_IN | CLASS_CACHE_FLUSH, + info.get_host_ttl(), + address, + intf.into(), + ); + + if let Some(new_name) = dns_registry.name_changes.get(hostname) { + dns_addr.get_record_mut().set_new_name(new_name.to_string()); + } + + if !info.requires_probe() + || dns_registry.is_probing_done(&dns_addr, info.get_fullname(), create_time) + { + out.add_answer_at_time(dns_addr, 0); + } else { + probing_count += 1; + } + } + + if probing_count > 0 { + return None; + } + + Some(out) +} + +/// Send an unsolicited response for owned service via `intf` and `sock`. +/// Returns true if sent out successfully for IPv4 or IPv6. +fn announce_service_on_intf( + dns_registry: &mut DnsRegistry, + info: &ServiceInfo, + intf: &MyIntf, + sock: &PktInfoUdpSocket, + port: u16, +) -> MyResult { + let is_ipv4 = sock.domain() == Domain::IPV4; + if let Some(mut out) = prepare_announce(info, intf, dns_registry, is_ipv4) { + // RFC 6762 §6: a record MUST NOT be multicast on an interface more than + // once per second. Announcements are unsolicited multicast responses. + dns_registry.apply_multicast_rate_limit(&mut out, current_time_millis(), is_ipv4); + if out.answers_count() > 0 { + let _ = send_dns_outgoing(&out, intf, sock, port, None, None)?; + } + return Ok(true); + } + + Ok(false) +} + +/// Returns a new name based on the `original` to avoid conflicts. +/// If the name already contains a number in parentheses, increments that number. +/// +/// Examples: +/// - `foo.local.` becomes `foo (2).local.` +/// - `foo (2).local.` becomes `foo (3).local.` +/// - `foo (9)` becomes `foo (10)` +fn name_change(original: &str) -> String { + let mut parts: Vec<_> = original.split('.').collect(); + let Some(first_part) = parts.get_mut(0) else { + return format!("{original} (2)"); + }; + + let mut new_name = format!("{first_part} (2)"); + + // check if there is already has `()` suffix. + if let Some(paren_pos) = first_part.rfind(" (") { + // Check if there's a closing parenthesis + if let Some(end_paren) = first_part[paren_pos..].find(')') { + let absolute_end_pos = paren_pos + end_paren; + // Only process if the closing parenthesis is the last character + if absolute_end_pos == first_part.len() - 1 { + let num_start = paren_pos + 2; // Skip " (" + // Try to parse the number between parentheses + if let Ok(number) = first_part[num_start..absolute_end_pos].parse::() { + let base_name = &first_part[..paren_pos]; + new_name = format!("{} ({})", base_name, number + 1) + } + } + } + } + + *first_part = &new_name; + parts.join(".") +} + +/// Returns a new name based on the `original` to avoid conflicts. +/// If the name already contains a hyphenated number, increments that number. +/// +/// Examples: +/// - `foo.local.` becomes `foo-2.local.` +/// - `foo-2.local.` becomes `foo-3.local.` +/// - `foo` becomes `foo-2` +fn hostname_change(original: &str) -> String { + let mut parts: Vec<_> = original.split('.').collect(); + let Some(first_part) = parts.get_mut(0) else { + return format!("{original}-2"); + }; + + let mut new_name = format!("{first_part}-2"); + + // check if there is already a `-` suffix + if let Some(hyphen_pos) = first_part.rfind('-') { + // Try to parse everything after the hyphen as a number + if let Ok(number) = first_part[hyphen_pos + 1..].parse::() { + let base_name = &first_part[..hyphen_pos]; + new_name = format!("{}-{}", base_name, number + 1); + } + } + + *first_part = &new_name; + parts.join(".") +} + +/// Check probes in a registry and returns: a probing packet to send out, and a list of probe names +/// that are finished. +fn check_probing( + dns_registry: &mut DnsRegistry, + timers: &mut BinaryHeap>, + now: u64, +) -> (DnsOutgoing, Vec) { + let mut expired_probes = Vec::new(); + let mut out = DnsOutgoing::new(FLAGS_QR_QUERY); + + for (name, probe) in dns_registry.probing.iter_mut() { + if now >= probe.next_send { + if probe.expired(now) { + // move the record to active + expired_probes.push(name.clone()); + } else { + out.add_question(name, RRType::ANY); + + /* + RFC 6762 section 8.2: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 + ... + for tiebreaking to work correctly in all + cases, the Authority Section must contain *all* the records and + proposed rdata being probed for uniqueness. + */ + for record in probe.records.iter() { + out.add_authority(record.clone()); + } + + probe.update_next_send(now); + + // add timer + add_bounded_timer(timers, probe.next_send); + } + } + } + + (out, expired_probes) +} + +fn add_bounded_timer(timers: &mut BinaryHeap>, next_time: u64) { + if timers.len() >= MAX_TIMERS || timers.iter().any(|timer| timer.0 == next_time) { + return; + } + timers.push(Reverse(next_time)); +} + +#[cfg(test)] +mod timer_admission_tests { + use super::{add_bounded_timer, BinaryHeap, Reverse, MAX_TIMERS}; + + #[test] + fn timer_admission_deduplicates_and_stops_at_capacity() { + let mut timers = BinaryHeap::new(); + for timer in 0..(MAX_TIMERS * 2) as u64 { + add_bounded_timer(&mut timers, timer); + } + assert_eq!(timers.len(), MAX_TIMERS); + add_bounded_timer(&mut timers, 0); + assert_eq!(timers.len(), MAX_TIMERS); + assert_eq!(timers.peek(), Some(&Reverse(0))); + } +} + +/// Process expired probes on an interface and return a list of services +/// that are waiting for the probe to finish. +/// +/// `DnsNameChange` events are sent to the monitors. +fn handle_expired_probes( + expired_probes: Vec, + intf_name: &str, + dns_registry: &mut DnsRegistry, + monitors: &mut Vec>, +) -> HashSet { + let mut waiting_services = HashSet::new(); + + for name in expired_probes { + let Some(probe) = dns_registry.probing.remove(&name) else { + continue; + }; + + // send notifications about name changes + for record in probe.records.iter() { + if let Some(new_name) = record.get_record().get_new_name() { + dns_registry + .name_changes + .insert(name.clone(), new_name.to_string()); + + let event = DnsNameChange { + original: record.get_record().get_original_name().to_string(), + new_name: new_name.to_string(), + rr_type: record.get_type(), + intf_name: intf_name.to_string(), + }; + debug!("Name change event: {:?}", &event); + notify_monitors(monitors, DaemonEvent::NameChange(event)); + } + } + + // move RR from probe to active. + debug!( + "probe of '{name}' finished: move {} records to active. ({} waiting services)", + probe.records.len(), + probe.waiting_services.len(), + ); + + // Move records to active and plan to wake up services if records are not empty. + if !probe.records.is_empty() { + match dns_registry.active.get_mut(&name) { + Some(records) => { + records.extend(probe.records); + } + None => { + dns_registry.active.insert(name, probe.records); + } + } + + waiting_services.extend(probe.waiting_services); + } + } + + waiting_services +} + +/// Returns the max packet size to use on the interface `if_index` for the given +/// address family, i.e. the size of the last selection matching it, or +/// [`MAX_PKT_DEFAULT`] if none does. +/// +/// A selection matches an address, so it applies as soon as any address of the +/// interface in that family matches. That keeps the two families independent: +/// e.g. [`IfKind::IPv4`] leaves the IPv6 side of the interface alone. +fn resolve_max_packet_size( + selections: &[MaxPacketSizeSelection], + interfaces: &[Interface], + if_index: u32, + is_ipv4: bool, +) -> usize { + let mut size = MAX_PKT_DEFAULT; + + for selection in selections { + let matched = interfaces.iter().any(|intf| { + intf.index.unwrap_or(0) == if_index + && intf.ip().is_ipv4() == is_ipv4 + && selection.if_kind.matches(intf) + }); + if matched { + size = selection.max_packet_size; + } + } + + size +} + +/// Resolves `IfKind::Addr(ip)` to `IndexV4(if_index)` or `IndexV6(if_index)`. +fn resolve_addr_to_index(if_kind: IfKind, interfaces: &[Interface]) -> IfKind { + if let IfKind::Addr(addr) = &if_kind { + if let Some(intf) = interfaces.iter().find(|intf| &intf.ip() == addr) { + let if_index = intf.index.unwrap_or(0); + return if addr.is_ipv4() { + IfKind::IndexV4(if_index) + } else { + IfKind::IndexV6(if_index) + }; + } + } + if_kind +} + +#[cfg(test)] +mod tests { + use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr, UdpSocket}, + time::{Duration, Instant, SystemTime}, + }; + + use if_addrs::{IfAddr, Ifv4Addr, Ifv6Addr, Interface}; + use test_log::test; + + use super::{ + _new_socket_bind, + check_domain_suffix, + check_service_name_length, + hostname_change, + my_ip_interfaces, + name_change, + resolve_max_packet_size, + send_dns_outgoing_impl, + valid_instance_name, + valid_ip_on_intf, + DaemonEvent, + HostnameResolutionEvent, + IfKind, + MaxPacketSizeSelection, + MyIntf, + SendConfig, + ServiceDaemon, + ServiceEvent, + ServiceInfo, + GROUP_ADDR_V4, + INITIAL_QUERY_DELAY_MAX_MILLIS, + INITIAL_QUERY_DELAY_MIN_MILLIS, + MAX_PKT_ABSOLUTE_IPV6, + MAX_PKT_DEFAULT, + MDNS_PORT, + MIN_MAX_PACKET_SIZE, + SHARED_RESPONSE_DELAY_MAX_MILLIS, + SHARED_RESPONSE_DELAY_MIN_MILLIS, + }; + use crate::{ + dns_parser::{ + DnsEntryExt, + DnsIncoming, + DnsOutgoing, + DnsPointer, + InterfaceId, + RRType, + ScopedIp, + CLASS_IN, + FLAGS_AA, + FLAGS_QR_QUERY, + FLAGS_QR_RESPONSE, + }, + service_daemon::{add_answer_of_service, check_hostname}, + }; + + /// Builds an interface address for the max packet size tests below. + fn test_interface(name: &str, index: u32, addr: IfAddr) -> Interface { + Interface { + name: name.to_string(), + addr, + index: Some(index), + oper_status: if_addrs::IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + } + } + + fn test_ifaddr_v4(ip: Ipv4Addr) -> IfAddr { + IfAddr::V4(Ifv4Addr { + ip, + netmask: Ipv4Addr::new(255, 255, 255, 0), + broadcast: None, + prefixlen: 24, + }) + } + + fn test_ifaddr_v6(ip: Ipv6Addr) -> IfAddr { + IfAddr::V6(Ifv6Addr { + ip, + netmask: Ipv6Addr::from(u128::MAX << 64), + broadcast: None, + prefixlen: 64, + }) + } + + #[test] + fn test_resolve_max_packet_size() { + // en0 is dual-stack, en1 is IPv4 only. + let interfaces = vec![ + test_interface("en0", 1, test_ifaddr_v4(Ipv4Addr::new(192, 168, 1, 2))), + test_interface( + "en0", + 1, + test_ifaddr_v6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)), + ), + test_interface("en1", 2, test_ifaddr_v4(Ipv4Addr::new(10, 0, 0, 2))), + ]; + + let resolve = |selections: &[MaxPacketSizeSelection], if_index, is_ipv4| { + resolve_max_packet_size(selections, &interfaces, if_index, is_ipv4) + }; + + // No selection: every interface keeps the default. + assert_eq!(resolve(&[], 1, true), MAX_PKT_DEFAULT); + assert_eq!(resolve(&[], 1, false), MAX_PKT_DEFAULT); + + // A selection by name applies to the interface it matches, both families. + let by_name = vec![MaxPacketSizeSelection { + if_kind: IfKind::Name("en0".to_string()), + max_packet_size: 8000, + }]; + assert_eq!(resolve(&by_name, 1, true), 8000); + assert_eq!(resolve(&by_name, 1, false), 8000); + assert_eq!(resolve(&by_name, 2, true), MAX_PKT_DEFAULT); + + // For an interface matched more than once, the last selection wins. + let overlapping = vec![ + MaxPacketSizeSelection { + if_kind: IfKind::All, + max_packet_size: 8000, + }, + MaxPacketSizeSelection { + if_kind: IfKind::Name("en1".to_string()), + max_packet_size: 4000, + }, + ]; + assert_eq!(resolve(&overlapping, 1, true), 8000); + assert_eq!(resolve(&overlapping, 1, false), 8000); + assert_eq!(resolve(&overlapping, 2, true), 4000); + + // A selection of one address family leaves the other one alone. + let v4_only = vec![MaxPacketSizeSelection { + if_kind: IfKind::IPv4, + max_packet_size: 8000, + }]; + assert_eq!(resolve(&v4_only, 1, true), 8000); + assert_eq!(resolve(&v4_only, 1, false), MAX_PKT_DEFAULT); + + let v6_only = vec![MaxPacketSizeSelection { + if_kind: IfKind::IPv6, + max_packet_size: 8000, + }]; + assert_eq!(resolve(&v6_only, 1, false), 8000); + assert_eq!(resolve(&v6_only, 1, true), MAX_PKT_DEFAULT); + // en1 has no IPv6 address, so the IPv6 selection cannot reach it. + assert_eq!(resolve(&v6_only, 2, true), MAX_PKT_DEFAULT); + assert_eq!(resolve(&v6_only, 2, false), MAX_PKT_DEFAULT); + + // Same for an index selection, which names a family too. + let by_index_v4 = vec![MaxPacketSizeSelection { + if_kind: IfKind::IndexV4(1), + max_packet_size: 8000, + }]; + assert_eq!(resolve(&by_index_v4, 1, true), 8000); + assert_eq!(resolve(&by_index_v4, 1, false), MAX_PKT_DEFAULT); + } + + /// A size outside [`MIN_MAX_PACKET_SIZE`]..=[`MAX_PKT_ABSOLUTE_IPV6`] is rejected + /// rather than clamped, so what reaches the encoder is always legal. + #[test] + fn test_set_max_packet_size_range() { + let daemon = ServiceDaemon::new().unwrap(); + + assert!(daemon + .set_max_packet_size(IfKind::All, MIN_MAX_PACKET_SIZE - 1) + .is_err()); + assert!(daemon + .set_max_packet_size(IfKind::All, MAX_PKT_ABSOLUTE_IPV6 + 1) + .is_err()); + + // Both ends of the range are accepted. + assert!(daemon + .set_max_packet_size(IfKind::All, MIN_MAX_PACKET_SIZE) + .is_ok()); + assert!(daemon + .set_max_packet_size(IfKind::All, MAX_PKT_ABSOLUTE_IPV6) + .is_ok()); + + daemon.shutdown().unwrap(); + } + + #[test] + fn test_response_source_ifaddr_match() { + // When an interface has multiple IPs on unrelated subnets, + // handle_query should pick the IfAddr whose subnet contains the querier, + // and fall back to None if none match. + let ifaddr_a = IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(192, 168, 1, 148), + netmask: Ipv4Addr::new(255, 255, 255, 0), + broadcast: None, + prefixlen: 24, + }); + let ifaddr_b = IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(10, 238, 0, 51), + netmask: Ipv4Addr::new(255, 255, 255, 0), + broadcast: None, + prefixlen: 24, + }); + + let intf = MyIntf { + name: "dummy0".to_string(), + index: 1, + addrs: HashSet::from([ifaddr_a.clone(), ifaddr_b.clone()]), + max_packet_size_v4: MAX_PKT_DEFAULT, + max_packet_size_v6: MAX_PKT_DEFAULT, + }; + + let pick = |querier: IpAddr| -> Option { + intf.addrs + .iter() + .find(|a| valid_ip_on_intf(&querier, a)) + .cloned() + }; + + assert_eq!( + pick(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))), + Some(ifaddr_a) + ); + assert_eq!( + pick(IpAddr::V4(Ipv4Addr::new(10, 238, 0, 99))), + Some(ifaddr_b) + ); + // Querier not on any local subnet: fall back to None. + assert_eq!(pick(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1))), None); + } + + #[test] + fn test_instance_name() { + assert!(valid_instance_name("my-laser._printer._tcp.local.")); + assert!(valid_instance_name("my-laser.._printer._tcp.local.")); + assert!(!valid_instance_name("_printer._tcp.local.")); + } + + #[test] + fn test_legacy_unicast_response() { + // RFC 6762 §6.7: a query whose UDP source port is not 5353 (a + // "legacy" / "one-shot" querier, e.g. Android's getaddrinfo) must + // get its response via unicast, sent back to the querier's source + // address, with the question echoed and the cache-flush bit cleared. + // + // This test sends such a query from an ephemeral port and asserts + // the response arrives on that same socket. The socket is not joined + // to the mDNS multicast group, so a multicast-only reply would never + // reach it — simply receiving the response proves it was unicast. + + let intf_ip = match my_ip_interfaces(false) + .into_iter() + .find_map(|intf| match intf.ip() { + IpAddr::V4(ip) => Some(ip), + IpAddr::V6(_) => None, + }) { + Some(ip) => ip, + None => { + println!("No IPv4 interface available; skipping test."); + return; + } + }; + + // Register a service with a unique hostname on this host. + let daemon = ServiceDaemon::new().expect("Failed to create daemon"); + let unique = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros(); + let hostname = format!("legacy-unicast-test-{unique}.local."); + let service_info = ServiceInfo::new( + "_legacy-uni._udp.local.", + "test_instance", + &hostname, + &[IpAddr::V4(intf_ip)] as &[IpAddr], + 5353, // arbitrary; the test only resolves the hostname + None, + ) + .expect("invalid service info"); + daemon.register(service_info).expect("register service"); + + // A one-shot querier: ephemeral source port, not 5353. Binding to + // `intf_ip` directs the multicast query out that interface, which is + // one the daemon is listening on. + let querier = UdpSocket::bind((intf_ip, 0)).expect("bind querier socket"); + querier + .set_multicast_loop_v4(true) + .expect("enable multicast loopback"); + querier + .set_read_timeout(Some(Duration::from_millis(500))) + .expect("set read timeout"); + assert_ne!( + querier.local_addr().unwrap().port(), + MDNS_PORT, + "querier must use an ephemeral (non-5353) source port" + ); + + // Build a one-question A-record query for our hostname. + let mut query = DnsOutgoing::new(FLAGS_QR_QUERY); + query.add_question(&hostname, RRType::A); + let query_packet = query + .to_data_on_wire(MAX_PKT_DEFAULT, true) + .pop() + .expect("query serialized to one packet"); + + let if_id = InterfaceId { + name: "test".to_string(), + index: 0, + }; + + // The service is announced asynchronously after register(), so retry + // the query until our answer comes back or the deadline passes. + let deadline = Instant::now() + Duration::from_secs(8); + let mut response = None; + 'outer: while Instant::now() < deadline { + querier + .send_to(&query_packet, (GROUP_ADDR_V4, MDNS_PORT)) + .expect("send query"); + + // Drain whatever has arrived; on read timeout the loop ends and + // we re-send the query. + let mut buf = [0u8; 1500]; + while let Ok((len, from)) = querier.recv_from(&mut buf) { + let Ok(msg) = DnsIncoming::new(buf[..len].to_vec(), if_id.clone()) else { + continue; + }; + if msg.is_response() + && msg + .answers() + .iter() + .any(|a| a.get_name().eq_ignore_ascii_case(&hostname)) + { + response = Some((msg, from)); + break 'outer; + } + } + } + + let (msg, from) = response.expect( + "expected a unicast response to the legacy query; \ + a multicast-only reply would never reach this un-joined socket", + ); + + // The reply came back to our ephemeral socket, from the mDNS port. + assert_eq!( + from.port(), + MDNS_PORT, + "response should originate from the mDNS port" + ); + + // RFC 6762 §6.7: the original question must be echoed. + assert!( + msg.questions() + .iter() + .any(|q| q.entry_name().eq_ignore_ascii_case(&hostname)), + "legacy unicast response must echo the question section" + ); + + // RFC 6762 §6.7 / §10.2: the answer must be the A record we asked + // for, with the cache-flush bit cleared. + let answer = msg + .answers() + .iter() + .find(|a| a.get_name().eq_ignore_ascii_case(&hostname)) + .expect("response contains an answer for our hostname"); + assert_eq!( + answer.get_type(), + RRType::A, + "an A query should be answered with an A record" + ); + assert!( + !answer.get_cache_flush(), + "legacy unicast responses must clear the cache-flush bit" + ); + + daemon.shutdown().unwrap(); + } + + #[test] + fn test_shared_response_delay_bounds() { + // A shared-record (PTR) response is delayed by a uniform-random amount. + // We deviate from the RFC 6762 §6 suggested 20-120 ms window and use a + // shorter 10-50 ms delay (`MAX` is the exclusive upper bound, so the + // actual delay is 10..=49 ms). + assert_eq!(SHARED_RESPONSE_DELAY_MIN_MILLIS, 10); + assert_eq!(SHARED_RESPONSE_DELAY_MAX_MILLIS, 50); + for _ in 0..10_000 { + let d = + fastrand::u64(SHARED_RESPONSE_DELAY_MIN_MILLIS..SHARED_RESPONSE_DELAY_MAX_MILLIS); + assert!( + (SHARED_RESPONSE_DELAY_MIN_MILLIS..SHARED_RESPONSE_DELAY_MAX_MILLIS).contains(&d), + "delay {} ms is outside the configured {}-{} ms range", + d, + SHARED_RESPONSE_DELAY_MIN_MILLIS, + SHARED_RESPONSE_DELAY_MAX_MILLIS + ); + } + } + + #[test] + fn test_initial_query_delayed() { + // RFC 6762 §5.2: a querier delays the first query of a continuous + // monitoring series by a random amount (we use a 10-50 ms window). + // Start a browse and observe, on a socket joined to the mDNS group, the + // daemon's first PTR query for our (unique) service type. Assert it + // arrives no sooner than ~10 ms after `browse()` — i.e. it is not sent + // immediately. + use socket2::{Domain, Protocol, Socket, Type}; + + let (intf, intf_ip) = match my_ip_interfaces(false) + .into_iter() + .find_map(|intf| match intf.ip() { + IpAddr::V4(ip) if !ip.is_loopback() => Some((intf, ip)), + _ => None, + }) { + Some(pair) => pair, + None => { + println!("No IPv4 interface available; skipping test."); + return; + } + }; + let interface_id = InterfaceId::from(&intf); + + // A receiver socket joined to the mDNS group on this interface. The + // daemon loops back its multicast by default, so its outgoing query is + // delivered here on the same host. + let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); + sock.set_reuse_address(true).unwrap(); + #[cfg(unix)] + sock.set_reuse_port(true).unwrap(); + sock.bind(&std::net::SocketAddr::from((Ipv4Addr::UNSPECIFIED, MDNS_PORT)).into()) + .unwrap(); + sock.join_multicast_v4(&GROUP_ADDR_V4, &intf_ip).unwrap(); + sock.set_read_timeout(Some(Duration::from_millis(200))) + .unwrap(); + let sock: UdpSocket = sock.into(); + + // Unique service type, kept within the RFC 6763 §7.2 15-byte label limit. + let unique = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros() + % 1_000_000_000; + let service_type = format!("_qd{unique}._udp.local."); + + let daemon = ServiceDaemon::new().expect("Failed to create daemon"); + + let sent_at = Instant::now(); + let _browse = daemon.browse(&service_type).expect("browse"); + + // Read packets until we see our own PTR query or time out. The 10-50 ms + // jitter plus command/scheduling latency comfortably fits in 2 s. + let deadline = Instant::now() + Duration::from_secs(2); + let mut buf = [0u8; 2048]; + let mut measured = None; + while Instant::now() < deadline { + let n = match sock.recv_from(&mut buf) { + Ok((n, _)) => n, + Err(_) => continue, // read timeout; keep polling until the deadline + }; + let Ok(msg) = DnsIncoming::new(buf[..n].to_vec(), interface_id.clone()) else { + continue; + }; + if msg.is_query() + && msg + .questions() + .iter() + .any(|q| q.entry_name() == service_type) + { + measured = Some(sent_at.elapsed()); + break; + } + } + + daemon.shutdown().unwrap(); + + let elapsed = measured.expect("expected the daemon to send a PTR query for our browse"); + let tolerance = Duration::from_millis(2); + assert!( + elapsed + tolerance >= Duration::from_millis(INITIAL_QUERY_DELAY_MIN_MILLIS), + "first browse query was sent after only {:?}; the first query of a series must be \ + delayed (10-50 ms window), not sent immediately", + elapsed + ); + + // Upper bound: the query must fall within the jitter window. Allow + // generous slack above INITIAL_QUERY_DELAY_MAX_MILLIS for command + // handoff, event-loop wakeup, and loopback latency, while still catching + // a regression to a much larger delay (e.g. the RFC's 120 ms window). + let scheduling_slack = Duration::from_millis(50); + assert!( + elapsed <= Duration::from_millis(INITIAL_QUERY_DELAY_MAX_MILLIS) + scheduling_slack, + "first browse query was sent after {:?}, beyond the {}-{} ms jitter window (plus slack)", + elapsed, + INITIAL_QUERY_DELAY_MIN_MILLIS, + INITIAL_QUERY_DELAY_MAX_MILLIS + ); + } + + #[test] + fn test_shared_ptr_response_delayed() { + // RFC 6762 §6: a PTR (shared record set) response sent by multicast is + // delayed by a uniform-random amount (we use a 10-50 ms window). Register + // a service, then as a proper multicast querier (source port 5353) send a + // PTR query and assert the daemon emits its response no sooner than ~10 ms + // after the query. (A legacy unicast querier gets an *immediate* response + // instead; see `test_legacy_unicast_response`.) + use socket2::{Domain, Protocol, Socket, Type}; + + let intf_ip = match my_ip_interfaces(false) + .into_iter() + .find_map(|intf| match intf.ip() { + IpAddr::V4(ip) if !ip.is_loopback() => Some(ip), + _ => None, + }) { + Some(ip) => ip, + None => { + println!("No IPv4 interface available; skipping test."); + return; + } + }; + + let daemon = ServiceDaemon::new().expect("Failed to create daemon"); + let monitor = daemon.monitor().expect("monitor daemon events"); + + // Keep the service name (the `_sd…` label) within the 15-byte limit + // that RFC 6763 §7.2 imposes, while staying unique per run. + let unique = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros() + % 1_000_000_000; + let service_type = format!("_sd{unique}._udp.local."); + let hostname = format!("sd{unique}.local."); + let service_info = ServiceInfo::new( + &service_type, + "test_instance", + &hostname, + &[IpAddr::V4(intf_ip)] as &[IpAddr], + 5353, + None, + ) + .expect("invalid service info"); + daemon.register(service_info).expect("register service"); + + // A proper multicast querier: source port 5353 so the daemon takes the + // shared-record (delayed) path rather than the legacy-unicast one. We only + // *send* on this socket; the response is observed through the monitor. + let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap(); + sock.set_reuse_address(true).unwrap(); + #[cfg(unix)] + sock.set_reuse_port(true).unwrap(); + sock.bind(&std::net::SocketAddr::from((Ipv4Addr::UNSPECIFIED, MDNS_PORT)).into()) + .unwrap(); + sock.set_multicast_if_v4(&intf_ip).unwrap(); + // Loop the query back to the daemon's socket on this same host. + sock.set_multicast_loop_v4(true).unwrap(); + let sock: UdpSocket = sock.into(); + + // Build the PTR query for our service type. + let mut query = DnsOutgoing::new(FLAGS_QR_QUERY); + query.add_question(&service_type, RRType::PTR); + let query_packet = query + .to_data_on_wire(MAX_PKT_DEFAULT, true) + .pop() + .expect("one packet"); + + // Wait for the initial announcements and the §6 rate-limit window (1s) to + // pass, so our query elicits a fresh (delayed) response instead of being + // suppressed by the rate limiter. + std::thread::sleep(Duration::from_secs(3)); + + // Retry until the daemon emits a Respond for our query. A query landing + // inside the 1 s multicast rate-limit window is rate-limited to an empty + // response (no send, no event), so we simply re-query on the next pass. + let deadline = Instant::now() + Duration::from_secs(8); + let mut measured = None; + while Instant::now() < deadline { + // Drop any Respond events queued earlier so we time only the response + // to the query we are about to send. + while monitor.try_recv().is_ok() {} + + let sent_at = Instant::now(); + sock.send_to(&query_packet, (GROUP_ADDR_V4, MDNS_PORT)) + .expect("send query"); + + // The delay window is 10-50 ms; 700 ms comfortably covers it plus any + // scheduling slack. Ignore unrelated events; on timeout, re-query. + let attempt_deadline = sent_at + Duration::from_millis(700); + loop { + let remaining = attempt_deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + break; + } + match monitor.recv_timeout(remaining) { + Ok(DaemonEvent::Respond(_)) => { + measured = Some(sent_at.elapsed()); + break; + } + Ok(_) => continue, // some other daemon event; keep waiting + Err(_) => break, // timed out; re-query + } + } + if measured.is_some() { + break; + } + } + + let elapsed = + measured.expect("expected the daemon to respond to our PTR query within the deadline"); + assert!( + elapsed >= Duration::from_millis(8), + "PTR response was sent after only {:?}; a shared-record response must be \ + delayed (10-50 ms window), not sent immediately", + elapsed + ); + assert!( + elapsed <= Duration::from_millis(600), + "PTR response was sent after {:?}; expected within the 10-50 ms delay window", + elapsed + ); + + daemon.shutdown().unwrap(); + } + + #[test] + fn test_check_service_name_length() { + let result = check_service_name_length("_tcp", 100); + assert!(result.is_err()); + if let Err(e) = result { + println!("{}", e); + } + } + + #[test] + fn test_check_hostname() { + // valid hostnames + for hostname in &[ + "my_host.local.", + &("A".repeat(255 - ".local.".len()) + ".local."), + ] { + let result = check_hostname(hostname); + assert!(result.is_ok()); + } + + // erroneous hostnames + for hostname in &[ + "my_host.local", + ".local.", + &("A".repeat(256 - ".local.".len()) + ".local."), + ] { + let result = check_hostname(hostname); + assert!(result.is_err()); + if let Err(e) = result { + println!("{}", e); + } + } + } + + #[test] + fn test_check_domain_suffix() { + assert!(check_domain_suffix("_missing_dot._tcp.local").is_err()); + assert!(check_domain_suffix("_missing_bar.tcp.local.").is_err()); + assert!(check_domain_suffix("_mis_spell._tpp.local.").is_err()); + assert!(check_domain_suffix("_mis_spell._upp.local.").is_err()); + assert!(check_domain_suffix("_has_dot._tcp.local.").is_ok()); + assert!(check_domain_suffix("_goodname._udp.local.").is_ok()); + } + + #[test] + fn test_service_with_temporarily_invalidated_ptr() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + let service = "_test_inval_ptr._udp.local."; + let host_name = "my_host_tmp_invalidated_ptr.local."; + let intfs: Vec<_> = my_ip_interfaces(false); + let intf_ips: Vec<_> = intfs.iter().map(|intf| intf.ip()).collect(); + let port = 5201; + let my_service = + ServiceInfo::new(service, "my_instance", host_name, &intf_ips[..], port, None) + .expect("invalid service info") + .enable_addr_auto(); + let result = d.register(my_service.clone()); + assert!(result.is_ok()); + + // Browse for a service + let browse_chan = d.browse(service).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + println!("Resolved a service of {}", &info.fullname); + break; + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + + println!("Stopping browse of {}", service); + // Pause browsing so restarting will cause a new immediate query. + // Unregistering will not work here, it will invalidate all the records. + d.stop_browse(service).unwrap(); + + // Ensure the search is stopped. + // Reduces the chance of receiving an answer adding the ptr back to the + // cache causing the later browse to return directly from the cache. + // (which invalidates what this test is trying to test for.) + let mut stopped = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::SearchStopped(_) => { + stopped = true; + println!("Stopped browsing service"); + break; + } + // Other `ServiceResolved` messages may be received + // here as they come from different interfaces. + // That's fine for this test. + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(stopped); + + // Invalidate the ptr from the service to the host. + let invalidate_ptr_packet = DnsPointer::new( + my_service.get_type(), + RRType::PTR, + CLASS_IN, + 0, + my_service.get_fullname().to_string(), + ); + + let mut packet_buffer = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); + packet_buffer.add_additional_answer(invalidate_ptr_packet); + + for intf in intfs { + let sock = _new_socket_bind(&intf, true).unwrap(); + send_dns_outgoing_impl( + &packet_buffer, + &intf.name, + intf.index.unwrap_or(0), + &intf.addr, + &sock.pktinfo, + SendConfig { + port: MDNS_PORT, + max_packet_size: MAX_PKT_DEFAULT, + is_ipv4: intf.addr.ip().is_ipv4(), + }, + None, + ) + .unwrap(); + } + + println!( + "Sent PTR record invalidation. Starting second browse for {}", + service + ); + + // Restart the browse to force the sender to re-send the announcements. + let browse_chan = d.browse(service).unwrap(); + + resolved = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + println!("Resolved a service of {}", &info.fullname); + break; + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + d.shutdown().unwrap(); + } + + #[test] + fn test_expired_srv() { + // construct service info + let service_type = "_expired-srv._udp.local."; + let instance = "test_instance"; + let host_name = "expired_srv_host.local."; + let mut my_service = ServiceInfo::new(service_type, instance, host_name, "", 5023, None) + .unwrap() + .enable_addr_auto(); + // let fullname = my_service.get_fullname().to_string(); + + // set SRV to expire soon. + let new_ttl = 3; // for testing only. + my_service._set_host_ttl(new_ttl); + + // register my service + let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server"); + let result = mdns_server.register(my_service); + assert!(result.is_ok()); + + let mdns_client = ServiceDaemon::new().expect("Failed to create mdns client"); + let browse_chan = mdns_client.browse(service_type).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + resolved = true; + println!("Resolved a service of {}", &info.fullname); + break; + } + } + + assert!(resolved); + + // Exit the server so that no more responses. + mdns_server.shutdown().unwrap(); + + // SRV record in the client cache will expire. + let expire_timeout = Duration::from_secs(new_ttl as u64); + while let Ok(event) = browse_chan.recv_timeout(expire_timeout) { + if let ServiceEvent::ServiceRemoved(service_type, full_name) = event { + println!("Service removed: {}: {}", &service_type, &full_name); + break; + } + } + } + + #[test] + fn test_hostname_resolution_address_removed() { + // Create a mDNS server + let server = ServiceDaemon::new().expect("Failed to create server"); + let hostname = "addr_remove_host._tcp.local."; + let service_ip_addr: ScopedIp = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.into()) + .unwrap(); + + let mut my_service = ServiceInfo::new( + "_host_res_test._tcp.local.", + "my_instance", + hostname, + service_ip_addr.to_ip_addr(), + 1234, + None, + ) + .expect("invalid service info"); + + // Set a short TTL for addresses for testing. + let addr_ttl = 2; + my_service._set_host_ttl(addr_ttl); // Expire soon + + server.register(my_service).unwrap(); + + // Create a mDNS client for resolving the hostname. + let client = ServiceDaemon::new().expect("Failed to create client"); + let event_receiver = client.resolve_hostname(hostname, None).unwrap(); + let resolved = loop { + match event_receiver.recv() { + Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => { + assert!(found_hostname == hostname); + assert!(addresses.contains(&service_ip_addr)); + println!("address found: {:?}", &addresses); + break true; + } + Ok(HostnameResolutionEvent::SearchStopped(_)) => break false, + Ok(_event) => {} + Err(_) => break false, + } + }; + + assert!(resolved); + + // Shutdown the server so no more responses / refreshes for addresses. + server.shutdown().unwrap(); + + // Wait till hostname address record expires, with 1 second grace period. + let timeout = Duration::from_secs(addr_ttl as u64 + 1); + let removed = loop { + match event_receiver.recv_timeout(timeout) { + Ok(HostnameResolutionEvent::AddressesRemoved(removed_host, addresses)) => { + assert!(removed_host == hostname); + assert!(addresses.contains(&service_ip_addr)); + + println!( + "address removed: hostname: {} addresses: {:?}", + &hostname, &addresses + ); + break true; + } + Ok(_event) => {} + Err(_) => { + break false; + } + } + }; + + assert!(removed); + + client.shutdown().unwrap(); + } + + #[test] + fn test_refresh_ptr() { + // construct service info + let service_type = "_refresh-ptr._udp.local."; + let instance = "test_instance"; + let host_name = "refresh_ptr_host.local."; + let service_ip_addr = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let mut my_service = ServiceInfo::new( + service_type, + instance, + host_name, + service_ip_addr, + 5023, + None, + ) + .unwrap(); + + let new_ttl = 3; // for testing only. + my_service._set_other_ttl(new_ttl); + + // register my service + let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server"); + let result = mdns_server.register(my_service); + assert!(result.is_ok()); + + let mdns_client = ServiceDaemon::new().expect("Failed to create mdns client"); + let browse_chan = mdns_client.browse(service_type).unwrap(); + let timeout = Duration::from_millis(1500); // Give at least 1 second for the service probing. + let mut resolved = false; + + // resolve the service first. + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + resolved = true; + println!("Resolved a service of {}", &info.fullname); + break; + } + } + + assert!(resolved); + + // wait over 80% of TTL, and refresh PTR should be sent out. + let timeout = Duration::from_millis(new_ttl as u64 * 1000 * 90 / 100); + while let Ok(event) = browse_chan.recv_timeout(timeout) { + println!("event: {:?}", &event); + } + + // verify refresh counter. + let metrics_chan = mdns_client.get_metrics().unwrap(); + let metrics = metrics_chan.recv_timeout(timeout).unwrap(); + let ptr_refresh_counter = metrics["cache-refresh-ptr"]; + assert_eq!(ptr_refresh_counter, 1); + let srvtxt_refresh_counter = metrics["cache-refresh-srv-txt"]; + assert_eq!(srvtxt_refresh_counter, 1); + + // Exit the server so that no more responses. + mdns_server.shutdown().unwrap(); + mdns_client.shutdown().unwrap(); + } + + #[test] + fn test_name_change() { + assert_eq!(name_change("foo.local."), "foo (2).local."); + assert_eq!(name_change("foo (2).local."), "foo (3).local."); + assert_eq!(name_change("foo (9).local."), "foo (10).local."); + assert_eq!(name_change("foo"), "foo (2)"); + assert_eq!(name_change("foo (2)"), "foo (3)"); + assert_eq!(name_change(""), " (2)"); + + // Additional edge cases + assert_eq!(name_change("foo (abc)"), "foo (abc) (2)"); // Invalid number + assert_eq!(name_change("foo (2"), "foo (2 (2)"); // Missing closing parenthesis + assert_eq!(name_change("foo (2) extra"), "foo (2) extra (2)"); // Extra text after number + } + + #[test] + fn test_hostname_change() { + assert_eq!(hostname_change("foo.local."), "foo-2.local."); + assert_eq!(hostname_change("foo"), "foo-2"); + assert_eq!(hostname_change("foo-2.local."), "foo-3.local."); + assert_eq!(hostname_change("foo-9"), "foo-10"); + assert_eq!(hostname_change("test-42.domain."), "test-43.domain."); + } + + #[test] + fn test_add_answer_txt_ttl() { + // construct a simple service info + let service_type = "_test_add_answer._udp.local."; + let instance = "test_instance"; + let host_name = "add_answer_host.local."; + let service_intf = my_ip_interfaces(false) + .into_iter() + .find(|iface| iface.ip().is_ipv4()) + .unwrap(); + let service_ip_addr = service_intf.ip(); + let my_service = ServiceInfo::new( + service_type, + instance, + host_name, + service_ip_addr, + 5023, + None, + ) + .unwrap(); + + // construct a DnsOutgoing message + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); + + // Construct a dummy DnsIncoming message + let mut dummy_data = out.to_data_on_wire(MAX_PKT_DEFAULT, true); + let interface_id = InterfaceId::from(&service_intf); + let incoming = DnsIncoming::new(dummy_data.pop().unwrap(), interface_id).unwrap(); + + // Add an answer of TXT type for the service. + let if_addrs = vec![service_intf.ip()]; + add_answer_of_service( + &mut out, + &incoming, + instance, + &my_service, + RRType::TXT, + if_addrs, + ); + + // Check if the answer was added correctly + assert!( + out.answers_count() > 0, + "No answers added to the outgoing message" + ); + + // Check if the first answer is of type TXT + let answer = out._answers().first().unwrap(); + assert_eq!(answer.0.get_type(), RRType::TXT); + + // Check TTL is set properly for the TXT record + assert_eq!(answer.0.get_record().get_ttl(), my_service.get_other_ttl()); + } + + #[test] + fn test_interface_flip() { + // start a server + let ty_domain = "_intf-flip._udp.local."; + let host_name = "intf_flip.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let port = 5200; + + // Get a single IPv4 address + let (ip_addr1, intf_name) = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| (iface.ip(), iface.name.clone())) + .unwrap(); + + println!("Using interface {} with IP {}", intf_name, ip_addr1); + + // Register the service. + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + let server1 = ServiceDaemon::new().expect("failed to start server"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + std::thread::sleep(Duration::from_secs(2)); + + // start a client + let client = ServiceDaemon::new().expect("failed to start client"); + + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(3); + let mut got_data = false; + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(_) = event { + println!("Received ServiceResolved event"); + got_data = true; + break; + } + } + + assert!(got_data, "Should receive ServiceResolved event"); + + // Set a short IP check interval to detect interface changes quickly. + client.set_ip_check_interval(1).unwrap(); + + // Now shutdown the interface and expect the client to lose the service. + println!("Shutting down interface {}", &intf_name); + client.test_down_interface(&intf_name).unwrap(); + + let mut got_removed = false; + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceRemoved(ty_domain, instance) = event { + got_removed = true; + println!("removed: {ty_domain} : {instance}"); + break; + } + } + assert!(got_removed, "Should receive ServiceRemoved event"); + + println!("Bringing up interface {}", &intf_name); + client.test_up_interface(&intf_name).unwrap(); + let mut got_data = false; + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(resolved) = event { + got_data = true; + println!("Received ServiceResolved: {:?}", resolved); + break; + } + } + assert!( + got_data, + "Should receive ServiceResolved event after interface is back up" + ); + + server1.shutdown().unwrap(); + client.shutdown().unwrap(); + } + + #[test] + fn test_cache_only() { + // construct service info + let service_type = "_cache_only._udp.local."; + let instance = "test_instance"; + let host_name = "cache_only_host.local."; + let service_ip_addr = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let mut my_service = ServiceInfo::new( + service_type, + instance, + host_name, + service_ip_addr, + 5023, + None, + ) + .unwrap(); + + let new_ttl = 3; // for testing only. + my_service._set_other_ttl(new_ttl); + + let mdns_client = ServiceDaemon::new().expect("Failed to create mdns client"); + + // make a single browse request to record that we are interested in the service. This ensures that + // subsequent announcements are cached. + let browse_chan = mdns_client.browse_cache(service_type).unwrap(); + std::thread::sleep(Duration::from_secs(2)); + + // register my service + let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server"); + let result = mdns_server.register(my_service); + assert!(result.is_ok()); + + let timeout = Duration::from_millis(1500); // Give at least 1 second for the service probing. + let mut resolved = false; + + // resolve the service. + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + resolved = true; + println!("Resolved a service of {}", &info.get_fullname()); + break; + } + } + + assert!(resolved); + + // Exit the server so that no more responses. + mdns_server.shutdown().unwrap(); + mdns_client.shutdown().unwrap(); + } + + #[test] + fn test_cache_only_unsolicited() { + let service_type = "_c_unsolicit._udp.local."; + let instance = "test_instance"; + let host_name = "c_unsolicit_host.local."; + let service_ip_addr = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let my_service = ServiceInfo::new( + service_type, + instance, + host_name, + service_ip_addr, + 5023, + None, + ) + .unwrap(); + + // register my service + let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server"); + let result = mdns_server.register(my_service); + assert!(result.is_ok()); + + let mdns_client = ServiceDaemon::new().expect("Failed to create mdns client"); + mdns_client.accept_unsolicited(true).unwrap(); + + // Wait a bit for the service announcements to go out, before calling browse_cache. This ensures + // that the announcements are treated as unsolicited + std::thread::sleep(Duration::from_secs(2)); + let browse_chan = mdns_client.browse_cache(service_type).unwrap(); + let timeout = Duration::from_millis(1500); // Give at least 1 second for the service probing. + let mut resolved = false; + + // resolve the service. + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + resolved = true; + println!("Resolved a service of {}", &info.get_fullname()); + break; + } + } + + assert!(resolved); + + // Exit the server so that no more responses. + mdns_server.shutdown().unwrap(); + mdns_client.shutdown().unwrap(); + } + + #[test] + fn test_custom_port_isolation() { + // This test verifies: + // 1. Daemons on a custom port can communicate with each other + // 2. Daemons on different ports are isolated (no cross-talk) + + let service_type = "_custom_port._udp.local."; + let instance_custom = "custom_port_instance"; + let instance_default = "default_port_instance"; + let host_name = "custom_port_host.local."; + + let service_ip_addr = my_ip_interfaces(false) + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .expect("Test requires an IPv4 interface"); + + // Create service info for custom port (5454) + let service_custom = ServiceInfo::new( + service_type, + instance_custom, + host_name, + service_ip_addr, + 8080, + None, + ) + .unwrap(); + + // Create service info for default port (5353) + let service_default = ServiceInfo::new( + service_type, + instance_default, + host_name, + service_ip_addr, + 8081, + None, + ) + .unwrap(); + + // Create two daemons on custom port 5454 + let custom_port = 5454u16; + let server_custom = + ServiceDaemon::new_with_port(custom_port).expect("Failed to create custom port server"); + let client_custom = + ServiceDaemon::new_with_port(custom_port).expect("Failed to create custom port client"); + + // Create daemon on default port (5353) + let server_default = ServiceDaemon::new().expect("Failed to create default port server"); + + // Register service on custom port + server_custom + .register(service_custom.clone()) + .expect("Failed to register custom port service"); + + // Register service on default port + server_default + .register(service_default.clone()) + .expect("Failed to register default port service"); + + // Browse from custom port client + let browse_custom = client_custom + .browse(service_type) + .expect("Failed to browse on custom port"); + + let timeout = Duration::from_secs(3); + let mut found_custom = false; + let mut found_default_on_custom = false; + + // Custom port client should find the custom port service + while let Ok(event) = browse_custom.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Custom port client resolved: {} on port {}", + info.get_fullname(), + info.get_port() + ); + if info.get_fullname().starts_with(instance_custom) { + found_custom = true; + assert_eq!(info.get_port(), 8080); + } + if info.get_fullname().starts_with(instance_default) { + found_default_on_custom = true; + } + } + } + + assert!( + found_custom, + "Custom port client should find service on custom port" + ); + assert!( + !found_default_on_custom, + "Custom port client should NOT find service on default port" + ); + + // Now verify the default port daemon can find its own services + // but not the custom port services + let client_default = ServiceDaemon::new().expect("Failed to create default port client"); + let browse_default = client_default + .browse(service_type) + .expect("Failed to browse on default port"); + + let mut found_default = false; + let mut found_custom_on_default = false; + + while let Ok(event) = browse_default.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Default port client resolved: {} on port {}", + info.get_fullname(), + info.get_port() + ); + if info.get_fullname().starts_with(instance_default) { + found_default = true; + assert_eq!(info.get_port(), 8081); + } + if info.get_fullname().starts_with(instance_custom) { + found_custom_on_default = true; + } + } + } + + assert!( + found_default, + "Default port client should find service on default port" + ); + assert!( + !found_custom_on_default, + "Default port client should NOT find service on custom port" + ); + + // Cleanup + server_custom.shutdown().unwrap(); + client_custom.shutdown().unwrap(); + server_default.shutdown().unwrap(); + client_default.shutdown().unwrap(); + } +} diff --git a/vendor/mdns-sd/src/service_info.rs b/vendor/mdns-sd/src/service_info.rs new file mode 100644 index 0000000..9d1f9c0 --- /dev/null +++ b/vendor/mdns-sd/src/service_info.rs @@ -0,0 +1,2112 @@ +//! Define `ServiceInfo` to represent a service and its operations. + +use std::{ + cmp, + collections::{HashMap, HashSet}, + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +use if_addrs::{IfAddr, Interface}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "logging")] +use crate::log::{debug, trace}; +use crate::{ + dns_parser::{DnsIncoming, DnsOutgoing, DnsRecordBox, DnsRecordExt, DnsSrv, RRType, ScopedIp}, + Error, + IfKind, + InterfaceId, + Result, +}; + +/// Default TTL values in seconds +const DNS_HOST_TTL: u32 = 120; // 2 minutes for host records (A, SRV etc) per RFC6762 +const DNS_OTHER_TTL: u32 = 4500; // 75 minutes for non-host records (PTR, TXT etc) per RFC6762 + +/// Represents a network interface. +#[derive(Debug)] +pub(crate) struct MyIntf { + /// The name of the interface. + pub(crate) name: String, + + /// Unique index assigned by the OS. Used by IPv6 for its scope_id. + pub(crate) index: u32, + + /// One interface can have multiple IPv4 addresses and/or multiple IPv6 addresses. + pub(crate) addrs: HashSet, + + /// Max byte size of a packet generated for the IPv4 addresses of this interface. + pub(crate) max_packet_size_v4: usize, + + /// Same as `max_packet_size_v4`, for the IPv6 addresses of this interface. + pub(crate) max_packet_size_v6: usize, +} + +impl MyIntf { + pub(crate) fn next_ifaddr_v4(&self) -> Option<&IfAddr> { + self.addrs.iter().find(|a| a.ip().is_ipv4()) + } + + pub(crate) fn next_ifaddr_v6(&self) -> Option<&IfAddr> { + self.addrs.iter().find(|a| a.ip().is_ipv6()) + } + + /// Max byte size of a packet generated for the given address family. + pub(crate) fn max_packet_size(&self, is_ipv4: bool) -> usize { + if is_ipv4 { + self.max_packet_size_v4 + } else { + self.max_packet_size_v6 + } + } +} + +impl From<&MyIntf> for InterfaceId { + fn from(my_intf: &MyIntf) -> Self { + InterfaceId { + name: my_intf.name.clone(), + index: my_intf.index, + } + } +} + +/// Escapes dots and backslashes in a DNS instance name according to RFC 6763 Section 4.3. +/// - '.' becomes '\.' +/// - '\' becomes '\\' +/// +/// Note: `\` itself needs to be escaped in the source code. +/// +/// This is required when concatenating the three portions of a Service Instance Name +/// to ensure that literal dots in the instance name are not interpreted as label separators. +fn escape_instance_name(name: &str) -> String { + let mut result = String::with_capacity(name.len() + 10); // Extra space for escapes + + for ch in name.chars() { + match ch { + '.' => { + result.push('\\'); + result.push('.'); + } + '\\' => { + result.push('\\'); + result.push('\\'); + } + _ => result.push(ch), + } + } + + result +} + +/// 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) and AAAA (IPv6 Address) records. +#[derive(Debug, Clone)] +pub struct ServiceInfo { + /// Service type and domain: {service-type-name}.{domain} + /// By default the service-type-name length must be <= 15. + /// so "_abcdefghijklmno._udp.local." would be valid but "_abcdefghijklmnop._udp.local." is not + ty_domain: String, + + /// See RFC6763 section 7.1 about "Subtypes": + /// + sub_domain: Option, // ._sub.. + + fullname: String, // .. + server: String, // fully qualified name for service host + addresses: HashSet, + port: u16, + host_ttl: u32, // used for SRV and Address records + other_ttl: u32, // used for PTR and TXT records + priority: u16, + weight: u16, + txt_properties: TxtProperties, + addr_auto: bool, // Let the system update addresses automatically. + + status: HashMap, // keyed by interface index. + + /// Whether we need to probe names before announcing this service. + requires_probe: bool, + + /// If set, the service is only exposed on these interfaces + supported_intfs: Vec, + + /// If true, only link-local addresses are published. + is_link_local_only: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ServiceStatus { + Probing, + Announced, + Unknown, +} + +impl ServiceInfo { + /// Creates a new service info. + /// + /// `ty_domain` is the service type and the domain label, for example "_my-service._udp.local.". + /// By default the service type length must be <= 15 bytes + /// + /// `my_name` is the instance name, without the service type suffix. + /// It allows dots (`.`) and backslashes (`\`). + /// + /// `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` + /// - `Option>` + /// - slice of tuple: `&[(K, V)]` where `K` and `V` are [`std::string::ToString`]. + /// + /// Note: The maximum length of a single property string is `255`, Property that exceed the length are truncated. + /// > `len(key + value) < u8::MAX` + /// + /// `ip` can be one or more IP addresses, in a type that implements + /// [`AsIpAddrs`] trait. It supports: + /// + /// - Single IPv4: `"192.168.0.1"` + /// - Single IPv6: `"2001:0db8::7334"` + /// - Multiple IPv4 separated by comma: `"192.168.0.1,192.168.0.2"` + /// - Multiple IPv6 separated by comma: `"2001:0db8::7334,2001:0db8::7335"` + /// - A slice of IPv4: `&["192.168.0.1", "192.168.0.2"]` + /// - A slice of IPv6: `&["2001:0db8::7334", "2001:0db8::7335"]` + /// - A mix of IPv4 and IPv6: `"192.168.0.1,2001:0db8::7334"` + /// - All the above formats with [IpAddr] or `String` instead of `&str`. + /// + /// The host TTL and other TTL are set to default values. + pub fn new( + ty_domain: &str, + my_name: &str, + host_name: &str, + ip: Ip, + port: u16, + properties: P, + ) -> Result { + let (ty_domain, sub_domain) = split_sub_domain(ty_domain); + + let escaped_name = escape_instance_name(my_name); + let fullname = format!("{escaped_name}.{ty_domain}"); + let ty_domain = ty_domain.to_string(); + let sub_domain = sub_domain.map(str::to_string); + let server = normalize_hostname(host_name.to_string()); + let addresses = ip.as_ip_addrs()?; + let txt_properties = properties.into_txt_properties(); + + // RFC6763 section 6.4: https://www.rfc-editor.org/rfc/rfc6763#section-6.4 + // The characters of a key MUST be printable US-ASCII values (0x20-0x7E) + // [RFC20], excluding '=' (0x3D). + for prop in txt_properties.iter() { + let key = prop.key(); + if !key.is_ascii() { + return Err(Error::Msg(format!( + "TXT property key {} is not ASCII", + prop.key() + ))); + } + if key.contains('=') { + return Err(Error::Msg(format!( + "TXT property key {} contains '='", + prop.key() + ))); + } + + // RFC6763 section 6.1: each TXT record string is prefixed by a + // single length byte, so it cannot exceed 255 bytes. + let prop_len = key.len() + prop.val().map_or(0, |v| v.len() + 1); + if prop_len > u8::MAX as usize { + return Err(Error::Msg(format!( + "TXT property '{}' has length {} bytes, exceeding the 255-byte limit", + key, prop_len + ))); + } + } + + let this = Self { + ty_domain, + sub_domain, + fullname, + server, + addresses, + port, + host_ttl: DNS_HOST_TTL, + other_ttl: DNS_OTHER_TTL, + priority: 0, + weight: 0, + txt_properties, + addr_auto: false, + status: HashMap::new(), + requires_probe: true, + is_link_local_only: false, + supported_intfs: vec![IfKind::All], + }; + + Ok(this) + } + + /// Indicates that the library should automatically + /// update the addresses of this service, when IP + /// address(es) are added or removed on the host. + pub const fn enable_addr_auto(mut self) -> Self { + self.addr_auto = true; + self + } + + /// Returns if the service's addresses will be updated + /// automatically when the host IP addrs change. + pub const fn is_addr_auto(&self) -> bool { + self.addr_auto + } + + /// Set whether this service info requires name probing for potential name conflicts. + /// + /// By default, it is true (i.e. requires probing) for every service info. You + /// set it to `false` only when you are sure there are no conflicts, or for testing purposes. + pub fn set_requires_probe(&mut self, enable: bool) { + self.requires_probe = enable; + } + + /// Set whether the service is restricted to link-local addresses. + /// + /// By default, it is false. + pub fn set_link_local_only(&mut self, is_link_local_only: bool) { + self.is_link_local_only = is_link_local_only; + } + + /// Set the supported interfaces for this service. + /// + /// The service will be advertised on the provided interfaces only. When ips are auto-detected + /// (via 'enable_addr_auto') only addresses on these interfaces will be considered. + pub fn set_interfaces(&mut self, intfs: Vec) { + self.supported_intfs = intfs; + } + + /// Returns whether this service info requires name probing for potential name conflicts. + /// + /// By default, it returns true for every service info. + pub const fn requires_probe(&self) -> bool { + self.requires_probe + } + + /// Returns the service type including the domain label. + /// + /// For example: "_my-service._udp.local.". + #[inline] + pub fn get_type(&self) -> &str { + &self.ty_domain + } + + /// Returns the service subtype including the domain label, + /// if subtype has been defined. + /// + /// For example: "_printer._sub._http._tcp.local.". + #[inline] + pub const fn get_subtype(&self) -> &Option { + &self.sub_domain + } + + /// Returns whether the service type or subtype matches the given name. + pub(crate) fn matches_type_or_subtype(&self, name: &str) -> bool { + name == self.get_type() || self.get_subtype().as_ref().is_some_and(|v| v == name) + } + + /// Returns a reference of the service fullname. + /// + /// This is useful, for example, in unregister. + #[inline] + pub fn get_fullname(&self) -> &str { + &self.fullname + } + + /// Returns the properties from TXT records. + #[inline] + pub const fn get_properties(&self) -> &TxtProperties { + &self.txt_properties + } + + /// Returns a property for a given `key`, where `key` is + /// case insensitive. + /// + /// Returns `None` if `key` does not exist. + pub fn get_property(&self, key: &str) -> Option<&TxtProperty> { + self.txt_properties.get(key) + } + + /// Returns a property value for a given `key`, where `key` is + /// case insensitive. + /// + /// Returns `None` if `key` does not exist. + pub fn get_property_val(&self, key: &str) -> Option> { + self.txt_properties.get_property_val(key) + } + + /// Returns a property value string for a given `key`, where `key` is + /// case insensitive. + /// + /// Returns `None` if `key` does not exist. + pub fn get_property_val_str(&self, key: &str) -> Option<&str> { + self.txt_properties.get_property_val_str(key) + } + + /// Returns the service's hostname. + #[inline] + pub fn get_hostname(&self) -> &str { + &self.server + } + + /// Returns the service's port. + #[inline] + pub const fn get_port(&self) -> u16 { + self.port + } + + /// Returns the service's addresses + #[inline] + pub const fn get_addresses(&self) -> &HashSet { + &self.addresses + } + + /// Returns the service's IPv4 addresses only. + pub fn get_addresses_v4(&self) -> HashSet<&Ipv4Addr> { + let mut ipv4_addresses = HashSet::new(); + + for ip in &self.addresses { + if let IpAddr::V4(ipv4) = ip { + ipv4_addresses.insert(ipv4); + } + } + + ipv4_addresses + } + + /// Returns the service's TTL used for SRV and Address records. + #[inline] + pub const fn get_host_ttl(&self) -> u32 { + self.host_ttl + } + + /// Returns the service's TTL used for PTR and TXT records. + #[inline] + pub const fn get_other_ttl(&self) -> u32 { + self.other_ttl + } + + /// Returns the service's priority used in SRV records. + #[inline] + pub const fn get_priority(&self) -> u16 { + self.priority + } + + /// Returns the service's weight used in SRV records. + #[inline] + pub const fn get_weight(&self) -> u16 { + self.weight + } + + /// Returns all addresses published + pub(crate) fn get_addrs_on_my_intf_v4(&self, my_intf: &MyIntf) -> Vec { + self.addresses + .iter() + .filter(|a| a.is_ipv4() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x))) + .copied() + .collect() + } + + pub(crate) fn get_addrs_on_my_intf_v6(&self, my_intf: &MyIntf) -> Vec { + self.addresses + .iter() + .filter(|a| a.is_ipv6() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x))) + .copied() + .collect() + } + + /// Returns whether the service info is ready to be resolved. + pub(crate) fn _is_ready(&self) -> bool { + let some_missing = self.ty_domain.is_empty() + || self.fullname.is_empty() + || self.server.is_empty() + || self.addresses.is_empty(); + !some_missing + } + + /// Insert `addr` into service info addresses. + pub(crate) fn insert_ipaddr(&mut self, intf: &Interface) { + if self.is_address_supported(intf) { + self.addresses.insert(intf.addr.ip()); + } else { + trace!( + "skipping unsupported address {} for service {}", + intf.addr.ip(), + self.fullname + ); + } + } + + pub(crate) fn remove_ipaddr(&mut self, addr: &IpAddr) { + self.addresses.remove(addr); + } + + pub(crate) fn generate_txt(&self) -> Vec { + encode_txt(self.get_properties().iter()) + } + + pub(crate) fn _set_port(&mut self, port: u16) { + self.port = port; + } + + pub(crate) fn _set_hostname(&mut self, hostname: String) { + self.server = normalize_hostname(hostname); + } + + /// Returns true if properties are updated. + pub(crate) fn _set_properties_from_txt(&mut self, txt: &[u8]) -> bool { + let properties = decode_txt_unique(txt); + if self.txt_properties.properties != properties { + self.txt_properties = TxtProperties { properties }; + true + } else { + false + } + } + + pub(crate) fn _set_subtype(&mut self, subtype: String) { + self.sub_domain = Some(subtype); + } + + /// host_ttl is for SRV and address records + /// currently only used for testing. + pub(crate) fn _set_host_ttl(&mut self, ttl: u32) { + self.host_ttl = ttl; + } + + /// other_ttl is for PTR and TXT records. + pub(crate) fn _set_other_ttl(&mut self, ttl: u32) { + self.other_ttl = ttl; + } + + pub(crate) fn set_status(&mut self, if_index: u32, status: ServiceStatus) { + match self.status.get_mut(&if_index) { + Some(service_status) => { + *service_status = status; + } + None => { + self.status.entry(if_index).or_insert(status); + } + } + } + + pub(crate) fn get_status(&self, intf: u32) -> ServiceStatus { + self.status + .get(&intf) + .cloned() + .unwrap_or(ServiceStatus::Unknown) + } + + /// Consumes self and returns a resolved service, i.e. a lite version of `ServiceInfo`. + pub fn as_resolved_service(self) -> ResolvedService { + let addresses: HashSet = self.addresses.into_iter().map(|a| a.into()).collect(); + ResolvedService { + ty_domain: self.ty_domain, + sub_ty_domain: self.sub_domain, + fullname: self.fullname, + host: self.server, + port: self.port, + addresses, + txt_properties: self.txt_properties, + observed_source: None, + } + } + + fn is_address_supported(&self, intf: &Interface) -> bool { + let interface_supported = self.supported_intfs.iter().any(|i| i.matches(intf)); + let addr = intf.ip(); + let passes_link_local = !self.is_link_local_only + || match &addr { + IpAddr::V4(ipv4) => ipv4.is_link_local(), + IpAddr::V6(ipv6) => is_unicast_link_local(ipv6), + }; + debug!( + "matching inserted address {} on intf {}: passes_link_local={}, interface_supported={}", + addr, addr, passes_link_local, interface_supported + ); + interface_supported && passes_link_local + } +} + +/// Removes potentially duplicated ".local." at the end of "hostname". +fn normalize_hostname(mut hostname: String) -> String { + if hostname.ends_with(".local.local.") { + let new_len = hostname.len() - "local.".len(); + hostname.truncate(new_len); + } + hostname +} + +/// This trait allows for parsing an input into a set of one or multiple [`Ipv4Addr`]. +pub trait AsIpAddrs { + fn as_ip_addrs(&self) -> Result>; +} + +impl AsIpAddrs for &T { + fn as_ip_addrs(&self) -> Result> { + (*self).as_ip_addrs() + } +} + +/// Supports one address or multiple addresses separated by `,`. +/// For example: "127.0.0.1,127.0.0.2". +/// +/// If the string is empty, will return an empty set. +impl AsIpAddrs for &str { + fn as_ip_addrs(&self) -> Result> { + let mut addrs = HashSet::new(); + + if !self.is_empty() { + let iter = self.split(',').map(str::trim).map(IpAddr::from_str); + for addr in iter { + let addr = addr.map_err(|err| Error::ParseIpAddr(err.to_string()))?; + addrs.insert(addr); + } + } + + Ok(addrs) + } +} + +impl AsIpAddrs for String { + fn as_ip_addrs(&self) -> Result> { + self.as_str().as_ip_addrs() + } +} + +/// Support slice. Example: &["127.0.0.1", "127.0.0.2"] +impl AsIpAddrs for &[I] { + fn as_ip_addrs(&self) -> Result> { + let mut addrs = HashSet::new(); + + for result in self.iter().map(I::as_ip_addrs) { + addrs.extend(result?); + } + + Ok(addrs) + } +} + +/// Optimization for zero sized/empty values, as `()` will never take up any space or evaluate to +/// anything, helpful in contexts where we just want an empty value. +impl AsIpAddrs for () { + fn as_ip_addrs(&self) -> Result> { + Ok(HashSet::new()) + } +} + +impl AsIpAddrs for std::net::IpAddr { + fn as_ip_addrs(&self) -> Result> { + let mut ips = HashSet::new(); + ips.insert(*self); + + Ok(ips) + } +} + +impl AsIpAddrs for Box { + fn as_ip_addrs(&self) -> Result> { + self.as_ref().as_ip_addrs() + } +} + +/// Represents properties in a TXT record. +/// +/// The key string of a property is case insensitive, and only +/// one [`TxtProperty`] is stored for the same key. +/// +/// [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763#section-6.4): +/// "A given key SHOULD NOT appear more than once in a TXT record." +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct TxtProperties { + // Use `Vec` instead of `HashMap` to keep the order of insertions. + properties: Vec, +} + +impl Default for TxtProperties { + fn default() -> Self { + TxtProperties::new() + } +} + +impl TxtProperties { + pub fn new() -> Self { + TxtProperties { + properties: Vec::new(), + } + } + + /// Returns an iterator for all properties. + pub fn iter(&self) -> impl Iterator { + self.properties.iter() + } + + /// Returns the number of properties. + pub fn len(&self) -> usize { + self.properties.len() + } + + /// Returns if the properties are empty. + pub fn is_empty(&self) -> bool { + self.properties.is_empty() + } + + /// Returns a property for a given `key`, where `key` is + /// case insensitive. + pub fn get(&self, key: &str) -> Option<&TxtProperty> { + let key = key.to_lowercase(); + self.properties + .iter() + .find(|&prop| prop.key.to_lowercase() == key) + } + + /// Returns a property value for a given `key`, where `key` is + /// case insensitive. + /// + /// Returns `None` if `key` does not exist. + /// Returns `Some(Option<&u8>)` for its value. + pub fn get_property_val(&self, key: &str) -> Option> { + self.get(key).map(|x| x.val()) + } + + /// Returns a property value string for a given `key`, where `key` is + /// case insensitive. + /// + /// Returns `None` if `key` does not exist. + /// Returns `Some("")` if its value is `None` or is empty. + pub fn get_property_val_str(&self, key: &str) -> Option<&str> { + self.get(key).map(|x| x.val_str()) + } + + /// Consumes properties and returns a hashmap, where the keys are the properties keys. + /// + /// If a property value is empty, return an empty string (because RFC 6763 allows empty values). + /// If a property value is non-empty but not valid UTF-8, skip the property and log a message. + pub fn into_property_map_str(self) -> HashMap { + self.properties + .into_iter() + .filter_map(|property| { + let val_string = property.val.map_or(Some(String::new()), |val| { + String::from_utf8(val) + .map_err(|e| { + debug!("Property value contains invalid UTF-8: {e}"); + }) + .ok() + })?; + Some((property.key, val_string)) + }) + .collect() + } +} + +impl fmt::Display for TxtProperties { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let delimiter = ", "; + let props: Vec = self.properties.iter().map(|p| p.to_string()).collect(); + write!(f, "({})", props.join(delimiter)) + } +} + +impl From<&[u8]> for TxtProperties { + fn from(txt: &[u8]) -> Self { + let properties = decode_txt_unique(txt); + TxtProperties { properties } + } +} + +/// Represents a property in a TXT record. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct TxtProperty { + /// The name of the property. The original cases are kept. + key: String, + + /// RFC 6763 says values are bytes, not necessarily UTF-8. + /// It is also possible that there is no value, in which case + /// the key is a boolean key. + #[cfg_attr(feature = "serde", serde(rename = "value"))] + val: Option>, +} + +impl TxtProperty { + /// Returns the key of a property. + pub fn key(&self) -> &str { + &self.key + } + + /// Returns the value of a property, which could be `None`. + /// + /// To obtain a `&str` of the value, use `val_str()` instead. + pub fn val(&self) -> Option<&[u8]> { + self.val.as_deref() + } + + /// Returns the value of a property as str. + pub fn val_str(&self) -> &str { + self.val + .as_ref() + .map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default()) + } +} + +/// Supports constructing from a tuple. +impl From<&(K, V)> for TxtProperty +where + K: ToString, + V: ToString, +{ + fn from(prop: &(K, V)) -> Self { + Self { + key: prop.0.to_string(), + val: Some(prop.1.to_string().into_bytes()), + } + } +} + +impl From<(K, V)> for TxtProperty +where + K: ToString, + V: AsRef<[u8]>, +{ + fn from(prop: (K, V)) -> Self { + Self { + key: prop.0.to_string(), + val: Some(prop.1.as_ref().into()), + } + } +} + +/// Support a property that has no value. +impl From<&str> for TxtProperty { + fn from(key: &str) -> Self { + Self { + key: key.to_string(), + val: None, + } + } +} + +impl fmt::Display for TxtProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}={}", self.key, self.val_str()) + } +} + +/// Mimic the default debug output for a struct, with a twist: +/// - If self.var is UTF-8, will output it as a string in double quotes. +/// - If self.var is not UTF-8, will output its bytes as in hex. +impl fmt::Debug for TxtProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val_string = self.val.as_ref().map_or_else( + || "None".to_string(), + |v| { + std::str::from_utf8(&v[..]).map_or_else( + |_| format!("Some({})", u8_slice_to_hex(&v[..])), + |s| format!("Some(\"{s}\")"), + ) + }, + ); + + write!( + f, + "TxtProperty {{key: \"{}\", val: {}}}", + &self.key, &val_string, + ) + } +} + +const HEX_TABLE: [char; 16] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', +]; + +/// Create a hex string from `slice`, with a "0x" prefix. +/// +/// For example, [1u8, 2u8] -> "0x0102" +fn u8_slice_to_hex(slice: &[u8]) -> String { + let mut hex = String::with_capacity(slice.len() * 2 + 2); + hex.push_str("0x"); + for b in slice { + hex.push(HEX_TABLE[(b >> 4) as usize]); + hex.push(HEX_TABLE[(b & 0x0F) as usize]); + } + hex +} + +/// This trait allows for converting inputs into [`TxtProperties`]. +pub trait IntoTxtProperties { + fn into_txt_properties(self) -> TxtProperties; +} + +impl IntoTxtProperties for HashMap { + fn into_txt_properties(mut self) -> TxtProperties { + let properties = self + .drain() + .map(|(key, val)| TxtProperty { + key, + val: Some(val.into_bytes()), + }) + .collect(); + TxtProperties { properties } + } +} + +/// Mainly for backward compatibility. +impl IntoTxtProperties for Option> { + fn into_txt_properties(self) -> TxtProperties { + self.map_or_else( + || TxtProperties { + properties: Vec::new(), + }, + |h| h.into_txt_properties(), + ) + } +} + +/// Support Vec like `[("k1", "v1"), ("k2", "v2")]`. +impl<'a, T: 'a> IntoTxtProperties for &'a [T] +where + TxtProperty: From<&'a T>, +{ + fn into_txt_properties(self) -> TxtProperties { + let mut properties = Vec::new(); + let mut keys = HashSet::new(); + for t in self.iter() { + let prop = TxtProperty::from(t); + let key = prop.key.to_lowercase(); + if keys.insert(key) { + // Only push a new entry if the key did not exist. + // + // RFC 6763: https://www.rfc-editor.org/rfc/rfc6763#section-6.4 + // + // "If a client receives a TXT record containing the same key more than + // once, then the client MUST silently ignore all but the first + // occurrence of that attribute. " + properties.push(prop); + } + } + TxtProperties { properties } + } +} + +impl IntoTxtProperties for Vec { + fn into_txt_properties(self) -> TxtProperties { + TxtProperties { properties: self } + } +} + +// Convert from properties key/value pairs to DNS TXT record content +fn encode_txt<'a>(properties: impl Iterator) -> Vec { + let mut bytes = Vec::new(); + for prop in properties { + let mut s = prop.key.clone().into_bytes(); + if let Some(v) = &prop.val { + s.extend(b"="); + s.extend(v); + } + + debug_assert!( + s.len() <= u8::MAX as usize, + "TXT property '{}' exceeds 255 bytes; should have been validated in ServiceInfo::new()", + prop.key + ); + s.truncate(u8::MAX as usize); + let sz: u8 = s.len() as u8; + + // TXT uses (Length,Value) format for each property, + // i.e. the first byte is the length. + bytes.push(sz); + bytes.extend(s); + } + if bytes.is_empty() { + bytes.push(0); + } + bytes +} + +// Convert from DNS TXT record content to key/value pairs +pub(crate) fn decode_txt(txt: &[u8]) -> Vec { + let mut properties = Vec::new(); + let mut offset = 0; + while offset < txt.len() { + let length = txt[offset] as usize; + if length == 0 { + break; // reached the end + } + offset += 1; // move over the length byte + + let offset_end = offset + length; + if offset_end > txt.len() { + debug!("DNS TXT record contains invalid data: Size given for property would be out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len()); + break; // Skipping the rest of the record content, as the size for this property would already be out of range. + } + let kv_bytes = &txt[offset..offset_end]; + + // split key and val using the first `=` + let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else( + || (kv_bytes.to_vec(), None), + |idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())), + ); + + // Make sure the key can be stored in UTF-8. + match String::from_utf8(k) { + Ok(k_string) => { + properties.push(TxtProperty { + key: k_string, + val: v, + }); + } + Err(e) => debug!("failed to convert to String from key: {}", e), + } + + offset += length; + } + + properties +} + +fn decode_txt_unique(txt: &[u8]) -> Vec { + let mut properties = decode_txt(txt); + + // Remove duplicated keys and retain only the first appearance + // of each key. + let mut keys = HashSet::new(); + properties.retain(|p| { + let key = p.key().to_lowercase(); + keys.insert(key) // returns True if key is new. + }); + properties +} + +/// Returns true if `addr` is in the same network of `intf`. +pub(crate) fn valid_ip_on_intf(addr: &IpAddr, if_addr: &IfAddr) -> bool { + match (addr, if_addr) { + (IpAddr::V4(addr), IfAddr::V4(if_v4)) => { + let netmask = u32::from(if_v4.netmask); + let intf_net = u32::from(if_v4.ip) & netmask; + let addr_net = u32::from(*addr) & netmask; + addr_net == intf_net + } + (IpAddr::V6(addr), IfAddr::V6(if_v6)) => { + let netmask = u128::from(if_v6.netmask); + let intf_net = u128::from(if_v6.ip) & netmask; + let addr_net = u128::from(*addr) & netmask; + addr_net == intf_net + } + _ => false, + } +} + +/// A probing for a particular name. +#[derive(Debug)] +pub(crate) struct Probe { + /// All records probing for the same name. + pub(crate) records: Vec, + + /// The fullnames of services that are probing these records. + /// These are the original service names, will not change per conflicts. + pub(crate) waiting_services: HashSet, + + /// The time (T) to send the first query . + pub(crate) start_time: u64, + + /// The time to send the next (including the first) query. + pub(crate) next_send: u64, +} + +impl Probe { + pub(crate) fn new(start_time: u64) -> Self { + // RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.1: + // + // "250 ms after the first query, the host should send a second; then, + // 250 ms after that, a third. If, by 250 ms after the third probe, no + // conflicting Multicast DNS responses have been received, the host may + // move to the next step, announcing. " + let next_send = start_time; + + Self { + records: Vec::new(), + waiting_services: HashSet::new(), + start_time, + next_send, + } + } + + /// Add a new record with the same probing name in a sorted order. + pub(crate) fn insert_record(&mut self, record: DnsRecordBox) { + /* + RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2.1 + + " The records are sorted using the same lexicographical order as + described above, that is, if the record classes differ, the record + with the lower class number comes first. If the classes are the same + but the rrtypes differ, the record with the lower rrtype number comes + first." + */ + let insert_position = self + .records + .binary_search_by( + |existing| match existing.get_class().cmp(&record.get_class()) { + std::cmp::Ordering::Equal => existing.get_type().cmp(&record.get_type()), + other => other, + }, + ) + .unwrap_or_else(|pos| pos); + + self.records.insert(insert_position, record); + } + + /// Compares with `incoming` records. Postpone probe and retry if we yield. + pub(crate) fn tiebreaking(&mut self, msg: &DnsIncoming, probe_name: &str) { + let now = crate::current_time_millis(); + + // Only do tiebreaking if probe already started. + // This check also helps avoid redo tiebreaking if start time + // was postponed. + if self.start_time >= now { + return; + } + + let incoming: Vec<_> = msg + .authorities() + .iter() + .filter(|r| r.get_name() == probe_name) + .collect(); + /* + RFC 6762 section 8.2: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 + ... + if the host finds that its own data is lexicographically later, it + simply ignores the other host's probe. If the host finds that its + own data is lexicographically earlier, then it defers to the winning + host by waiting one second, and then begins probing for this record + again. + */ + let min_len = self.records.len().min(incoming.len()); + + // Compare elements up to the length of the shorter vector + let mut cmp_result = cmp::Ordering::Equal; + for (i, incoming_record) in incoming.iter().enumerate().take(min_len) { + match self.records[i].compare(incoming_record.as_ref()) { + cmp::Ordering::Equal => continue, + other => { + cmp_result = other; + break; // exit loop on first difference + } + } + } + + if cmp_result == cmp::Ordering::Equal { + // If all compared records are equal, compare the lengths of the records. + cmp_result = self.records.len().cmp(&incoming.len()); + } + + match cmp_result { + cmp::Ordering::Less => { + debug!("tiebreaking '{probe_name}': LOST, will wait for one second",); + self.start_time = now + 1000; // wait and restart. + self.next_send = now + 1000; + } + ordering => { + debug!("tiebreaking '{probe_name}': {:?}", ordering); + } + } + } + + pub(crate) fn update_next_send(&mut self, now: u64) { + self.next_send = now + 250; + } + + /// Returns whether this probe is finished. + pub(crate) fn expired(&self, now: u64) -> bool { + // The 2nd query is T + 250ms, the 3rd query is T + 500ms, + // The expire time is T + 750ms + now >= self.start_time + 750 + } +} + +/// DNS records of all the registered services. +pub(crate) struct DnsRegistry { + /// keyed by the name of all related DNS records. + /* + When a host is probing for a group of related records with the same + name (e.g., the SRV and TXT record describing a DNS-SD service), only + a single question need be placed in the Question Section, since query + type "ANY" (255) is used, which will elicit answers for all records + with that name. However, for tiebreaking to work correctly in all + cases, the Authority Section must contain *all* the records and + proposed rdata being probed for uniqueness. + */ + pub(crate) probing: HashMap, + + /// Already done probing, or no need to probe. + /// Keyed by DNS record name. + pub(crate) active: HashMap>, + + /// timers of the newly added probes. + pub(crate) new_timers: Vec, + + /// Mapping from original names to new names. + pub(crate) name_changes: HashMap, + + /// RFC 6762 section 6: the last time (in millis) each record was multicast + /// on this interface's IPv4 group, keyed by the record's identity + /// (name + type + rdata). Used to enforce the per-record, per-interface + /// one-second rate limit. + /// + /// IPv4 and IPv6 are tracked separately: a single interface (`if_index`) + /// carries both address families, but they are distinct multicast groups + /// (`224.0.0.251` and `ff02::fb`) reaching potentially different listeners, + /// so sending a record on one group must not throttle it on the other. + pub(crate) last_multicast_v4: HashMap, + + /// Same as [`Self::last_multicast_v4`] but for this interface's IPv6 group. + pub(crate) last_multicast_v6: HashMap, +} + +impl DnsRegistry { + pub(crate) fn new() -> Self { + Self { + probing: HashMap::new(), + active: HashMap::new(), + new_timers: Vec::new(), + name_changes: HashMap::new(), + last_multicast_v4: HashMap::new(), + last_multicast_v6: HashMap::new(), + } + } + + /// Enforces the RFC 6762 section 6 multicast rate limit on `out`. + /// + /// A responder MUST NOT multicast a record on a given interface until at + /// least one second has elapsed since the last time that record was + /// multicast on that particular interface. + /// + /// `is_ipv4` selects the per-family bucket: the IPv4 and IPv6 groups on one + /// interface are throttled independently (see [`Self::last_multicast_v4`]). + /// + /// Drops from `out` any answer or additional record that was multicast within the + /// last second, and records `now` as the last-multicast time for the records kept. + /// + /// This must NOT be applied to probe queries, legacy unicast responses, or + /// goodbye packets, which are exempt from the rate limit. + pub(crate) fn apply_multicast_rate_limit( + &mut self, + out: &mut DnsOutgoing, + now: u64, + is_ipv4: bool, + ) { + let last_multicast = if is_ipv4 { + &mut self.last_multicast_v4 + } else { + &mut self.last_multicast_v6 + }; + + // Prune stale entries so the map stays bounded across name changes; + // any record older than the one-second window is irrelevant now. + last_multicast.retain(|_, last| now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS); + + out.retain_answers(|record| keep_after_rate_limit(last_multicast, record, now)); + + // Only touch additionals if an answer survived. + if out.answers_count() > 0 { + out.retain_additionals(|record| keep_after_rate_limit(last_multicast, record, now)); + } + } + + /// Returns the renamed name if a name change exists, otherwise returns the original name. + pub(crate) fn resolve_name<'a>(&'a self, name: &'a str) -> &'a str { + match self.name_changes.get(name) { + Some(new_name) => new_name, + None => name, + } + } + + pub(crate) fn is_probing_done( + &mut self, + answer: &T, + service_name: &str, + start_time: u64, + ) -> bool + where + T: DnsRecordExt + Send + 'static, + { + if let Some(active_records) = self.active.get(answer.get_name()) { + for record in active_records.iter() { + if answer.matches(record.as_ref()) { + debug!( + "found active record {} {}", + answer.get_type(), + answer.get_name(), + ); + return true; + } + } + } + + let probe = self + .probing + .entry(answer.get_name().to_string()) + .or_insert_with(|| { + debug!("new probe of {}", answer.get_name()); + Probe::new(start_time) + }); + + self.new_timers.push(probe.next_send); + + for record in probe.records.iter() { + if answer.matches(record.as_ref()) { + debug!( + "found existing record {} in probe of '{}'", + answer.get_type(), + answer.get_name(), + ); + probe.waiting_services.insert(service_name.to_string()); + return false; // Found existing probe for the same record. + } + } + + debug!( + "insert record {} into probe of {}", + answer.get_type(), + answer.get_name(), + ); + probe.insert_record(answer.clone_box()); + probe.waiting_services.insert(service_name.to_string()); + + false + } + + /// check all records in "probing" and "active": + /// if the record is SRV, and hostname is set to original, remove it. + /// and create a new SRV with "host" set to "new_name" and put into "probing". + pub(crate) fn update_hostname( + &mut self, + original: &str, + new_name: &str, + probe_time: u64, + ) -> bool { + let mut found_records = Vec::new(); + let mut new_timer_added = false; + + for (_name, probe) in self.probing.iter_mut() { + probe.records.retain(|record| { + if record.get_type() == RRType::SRV { + if let Some(srv) = record.any().downcast_ref::() { + if srv.host() == original { + let mut new_record = srv.clone(); + new_record.set_host(new_name.to_string()); + found_records.push(new_record); + return false; + } + } + } + true + }); + } + + for (_name, records) in self.active.iter_mut() { + records.retain(|record| { + if record.get_type() == RRType::SRV { + if let Some(srv) = record.any().downcast_ref::() { + if srv.host() == original { + let mut new_record = srv.clone(); + new_record.set_host(new_name.to_string()); + found_records.push(new_record); + return false; + } + } + } + true + }); + } + + for record in found_records { + let probe = match self.probing.get_mut(record.get_name()) { + Some(p) => { + p.start_time = probe_time; // restart this probe. + p + } + None => { + let new_probe = self + .probing + .entry(record.get_name().to_string()) + .or_insert_with(|| Probe::new(probe_time)); + new_timer_added = true; + new_probe + } + }; + + debug!( + "insert record {} with new hostname {new_name} into probe for: {}", + record.get_type(), + record.get_name() + ); + probe.insert_record(record.boxed()); + } + + new_timer_added + } +} + +/// RFC 6762 section 6 per-record, per-interface multicast rate-limit window: +/// a record must not be re-multicast until at least this many millis have +/// elapsed since it was last multicast on that interface. +pub(crate) const MULTICAST_RATE_LIMIT_MILLIS: u64 = 1000; + +/// Returns whether `record` may still be multicast under the RFC 6762 section 6 +/// rate limit, updating `last_multicast` to `now` when it is kept. +fn keep_after_rate_limit( + last_multicast: &mut HashMap, + record: &DnsRecordBox, + now: u64, +) -> bool { + let key = rate_limit_key(record); + match last_multicast.get(&key) { + Some(last) if now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS => false, + _ => { + last_multicast.insert(key, now); + true + } + } +} + +/// Builds the identity key for a record used by the RFC 6762 section 6 +/// multicast rate limit: name (case-insensitive) + type + rdata. TTL and the +/// cache-flush bit are intentionally excluded, so the same logical record maps +/// to a single key regardless of the TTL it is sent with. +fn rate_limit_key(record: &DnsRecordBox) -> String { + format!( + "{}-{}-{}", + record.get_name().to_lowercase(), + record.get_type(), + record.rdata_print(), + ) +} + +/// Returns a tuple of (service_type_domain, optional_sub_domain) +pub(crate) fn split_sub_domain(domain: &str) -> (&str, Option<&str>) { + if let Some((_, ty_domain)) = domain.rsplit_once("._sub.") { + (ty_domain, Some(domain)) + } else { + (domain, None) + } +} + +/// Returns true if `addr` is a unicast link-local IPv6 address. +/// Replicates the logic from `std::net::Ipv6Addr::is_unicast_link_local()`, which is not +/// stable on the current mdns-sd Rust version (1.71.0). +/// +/// https://github.com/rust-lang/rust/blob/9fc6b43126469e3858e2fe86cafb4f0fd5068869/library/core/src/net/ip_addr.rs#L1684 +pub(crate) fn is_unicast_link_local(addr: &Ipv6Addr) -> bool { + (addr.segments()[0] & 0xffc0) == 0xfe80 +} + +/// Represents a resolved service as a plain data struct. +/// This is from a client (i.e. querier) point of view. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub struct ResolvedService { + /// Service type and domain. For example, "_http._tcp.local." + pub ty_domain: String, + + /// Optional service subtype and domain. + /// + /// See RFC6763 section 7.1 about "Subtypes": + /// + /// For example, "_printer._sub._http._tcp.local." + pub sub_ty_domain: Option, + + /// Full name of the service. For example, "my-service._http._tcp.local." + pub fullname: String, + + /// Host name of the service. For example, "my-server1.local." + pub host: String, + + /// Port of the service. I.e. TCP or UDP port. + pub port: u16, + + /// Addresses of the service. IPv4 or IPv6 addresses. + pub addresses: HashSet, + + /// Properties of the service, decoded from TXT record. + pub txt_properties: TxtProperties, + + /// Source address observed on the response packet that introduced the + /// service PTR record. Locally constructed services do not have one. + #[cfg_attr(feature = "serde", serde(default))] + pub observed_source: Option, +} + +impl ResolvedService { + /// Returns the observed response source when this value came from the + /// network cache. + #[inline] + pub const fn get_observed_source(&self) -> Option { + self.observed_source + } + /// Returns true if the service data is valid, i.e. ready to be used. + pub fn is_valid(&self) -> bool { + let some_missing = self.ty_domain.is_empty() + || self.fullname.is_empty() + || self.host.is_empty() + || self.addresses.is_empty(); + !some_missing + } + + #[inline] + pub const fn get_subtype(&self) -> &Option { + &self.sub_ty_domain + } + + #[inline] + pub fn get_fullname(&self) -> &str { + &self.fullname + } + + #[inline] + pub fn get_hostname(&self) -> &str { + &self.host + } + + #[inline] + pub fn get_port(&self) -> u16 { + self.port + } + + #[inline] + pub fn get_addresses(&self) -> &HashSet { + &self.addresses + } + + pub fn get_addresses_v4(&self) -> HashSet { + self.addresses + .iter() + .filter_map(|ip| match ip { + ScopedIp::V4(ipv4) => Some(*ipv4.addr()), + _ => None, + }) + .collect() + } + + #[inline] + pub fn get_properties(&self) -> &TxtProperties { + &self.txt_properties + } + + #[inline] + pub fn get_property(&self, key: &str) -> Option<&TxtProperty> { + self.txt_properties.get(key) + } + + pub fn get_property_val(&self, key: &str) -> Option> { + self.txt_properties.get_property_val(key) + } + + pub fn get_property_val_str(&self, key: &str) -> Option<&str> { + self.txt_properties.get_property_val_str(key) + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, Ipv6Addr}; + + use if_addrs::{IfAddr, IfOperStatus, Ifv4Addr, Ifv6Addr, Interface}; + + use super::{decode_txt, encode_txt, u8_slice_to_hex, DnsRegistry, ServiceInfo, TxtProperty}; + use crate::{ + dns_parser::{DnsOutgoing, DnsPointer, RRType, CLASS_IN, FLAGS_QR_RESPONSE}, + IfKind, + IfPredicate, + }; + + /// RFC 6762 section 6: the same record must not be multicast on an + /// interface more than once per second, but is allowed again after a + /// second has elapsed. + #[test] + fn test_multicast_rate_limit() { + let mut registry = DnsRegistry::new(); + + let build_out = || { + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time( + DnsPointer::new( + "_test._tcp.local.", + RRType::PTR, + CLASS_IN, + 4500, + "inst._test._tcp.local.".to_string(), + ), + 0, + ); + out + }; + + let now = 1_000_000; + + // First multicast at `now`: the record passes through. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now, true); + assert_eq!(out.answers_count(), 1); + + // Again 500ms later: the record is throttled (dropped). + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now + 500, true); + assert_eq!(out.answers_count(), 0); + + // Exactly 1 second after the first send: allowed again. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now + 1000, true); + assert_eq!(out.answers_count(), 1); + } + + /// A single interface carries both IPv4 and IPv6, but they are distinct + /// multicast groups reaching different listeners, so the one-second limit + /// is tracked per family: multicasting a record on IPv4 must NOT throttle + /// the same record on IPv6 (and vice versa). Otherwise the shared PTR/SRV/ + /// TXT records would be stripped from whichever family is sent second. + #[test] + fn test_multicast_rate_limit_per_family() { + let mut registry = DnsRegistry::new(); + + let build_out = || { + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time( + DnsPointer::new( + "_test._tcp.local.", + RRType::PTR, + CLASS_IN, + 4500, + "inst._test._tcp.local.".to_string(), + ), + 0, + ); + out + }; + + let now = 1_000_000; + + // Multicast the record on IPv4: passes through. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now, true); + assert_eq!(out.answers_count(), 1); + + // The same record on IPv6 immediately after: must still pass, because + // the IPv6 group has its own bucket. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now, false); + assert_eq!(out.answers_count(), 1); + + // A second IPv4 send within the window is still throttled, confirming + // the IPv6 send did not reset (or get charged to) the IPv4 bucket. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now + 500, true); + assert_eq!(out.answers_count(), 0); + + // Likewise a second IPv6 send within the window is throttled. + let mut out = build_out(); + registry.apply_multicast_rate_limit(&mut out, now + 500, false); + assert_eq!(out.answers_count(), 0); + } + + /// When every answer is throttled the packet is not sent, so any surviving + /// additional record must NOT be stamped as multicast — otherwise a later + /// answer for that same record would be wrongly throttled even though it was + /// never put on the wire. + #[test] + fn test_multicast_rate_limit_additionals_not_stamped_without_answer() { + let mut registry = DnsRegistry::new(); + + let ptr_answer = || { + DnsPointer::new( + "_test._tcp.local.", + RRType::PTR, + CLASS_IN, + 4500, + "inst._test._tcp.local.".to_string(), + ) + }; + let extra = || { + DnsPointer::new( + "_other._tcp.local.", + RRType::PTR, + CLASS_IN, + 4500, + "inst._other._tcp.local.".to_string(), + ) + }; + + let now = 1_000_000; + + // Send the PTR answer once so it is throttled going forward. + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time(ptr_answer(), 0); + registry.apply_multicast_rate_limit(&mut out, now, true); + assert_eq!(out.answers_count(), 1); + + // 100ms later: PTR answer is throttled, and `extra` rides along as an + // additional. With no answer surviving, nothing is sent. + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time(ptr_answer(), 0); + out.add_additional_answer(extra()); + registry.apply_multicast_rate_limit(&mut out, now + 100, true); + assert_eq!(out.answers_count(), 0); + + // 200ms later: `extra` is now requested as a real answer. It must pass, + // because it was never actually multicast above (only carried as an + // unsent additional), so the 1-second limit does not apply to it. + let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE); + out.add_answer_at_time(extra(), 0); + registry.apply_multicast_rate_limit(&mut out, now + 200, true); + assert_eq!(out.answers_count(), 1); + } + + #[test] + fn test_txt_encode_decode() { + let properties = [ + TxtProperty::from(&("key1", "value1")), + TxtProperty::from(&("key2", "value2")), + ]; + + // test encode + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + assert_eq!( + encoded.len(), + "key1=value1".len() + "key2=value2".len() + property_count + ); + assert_eq!(encoded[0] as usize, "key1=value1".len()); + + // test decode + let decoded = decode_txt(&encoded); + assert!(properties[..] == decoded[..]); + + // test empty value + let properties = vec![TxtProperty::from(&("key3", ""))]; + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + assert_eq!(encoded.len(), "key3=".len() + property_count); + + let decoded = decode_txt(&encoded); + assert_eq!(properties, decoded); + + // test non-string value + let binary_val: Vec = vec![123, 234, 0]; + let binary_len = binary_val.len(); + let properties = vec![TxtProperty::from(("key4", binary_val))]; + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + assert_eq!(encoded.len(), "key4=".len() + binary_len + property_count); + + let decoded = decode_txt(&encoded); + assert_eq!(properties, decoded); + + // test value that contains '=' + let properties = vec![TxtProperty::from(("key5", "val=5"))]; + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + assert_eq!( + encoded.len(), + "key5=".len() + "val=5".len() + property_count + ); + + let decoded = decode_txt(&encoded); + assert_eq!(properties, decoded); + + // test a property that has no value. + let properties = vec![TxtProperty::from("key6")]; + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + assert_eq!(encoded.len(), "key6".len() + property_count); + let decoded = decode_txt(&encoded); + assert_eq!(properties, decoded); + + // test property at the 255-byte limit. + let properties = [TxtProperty::from( + String::from_utf8(vec![0x30; 255]).unwrap().as_str(), + )]; + let property_count = properties.len(); + let encoded = encode_txt(properties.iter()); + // `property_count` is added because each property has a length byte. + assert_eq!(encoded.len(), 255 + property_count); + let decoded = decode_txt(&encoded); + assert_eq!(properties.to_vec(), decoded); + } + + #[test] + fn test_txt_property_exceeds_255_bytes() { + let long_key = String::from_utf8(vec![0x30; 256]).unwrap(); + let result = ServiceInfo::new( + "_test._tcp.local.", + "test", + "host", + "", + 1234, + &[(long_key.as_str(), "")][..], + ); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("exceeding the 255-byte limit")); + + // A property exactly at 255 bytes should succeed. + // key (250 bytes) + "=" (1 byte) + value (4 bytes) = 255 bytes. + let key_at_limit = String::from_utf8(vec![0x30; 250]).unwrap(); + let result = ServiceInfo::new( + "_test._tcp.local.", + "test", + "host", + "", + 1234, + &[(key_at_limit.as_str(), "abcd")][..], + ); + assert!(result.is_ok()); + } + + #[test] + fn test_set_properties_from_txt() { + // Three duplicated keys. + let properties = [ + TxtProperty::from(&("one", "1")), + TxtProperty::from(&("ONE", "2")), + TxtProperty::from(&("One", "3")), + ]; + let encoded = encode_txt(properties.iter()); + + // Simple decode does not remove duplicated keys. + let decoded = decode_txt(&encoded); + assert_eq!(decoded.len(), 3); + + // ServiceInfo removes duplicated keys and keeps only the first one. + let mut service_info = + ServiceInfo::new("_test._tcp", "prop_test", "localhost", "", 1234, None).unwrap(); + service_info._set_properties_from_txt(&encoded); + assert_eq!(service_info.get_properties().len(), 1); + + // Verify the only one property. + let prop = service_info.get_properties().iter().next().unwrap(); + assert_eq!(prop.key, "one"); + assert_eq!(prop.val_str(), "1"); + } + + #[test] + fn test_u8_slice_to_hex() { + let bytes = [0x01u8, 0x02u8, 0x03u8]; + let hex = u8_slice_to_hex(&bytes); + assert_eq!(hex.as_str(), "0x010203"); + + let slice = "abcdefghijklmnopqrstuvwxyz"; + let hex = u8_slice_to_hex(slice.as_bytes()); + assert_eq!(hex.len(), slice.len() * 2 + 2); + assert_eq!( + hex.as_str(), + "0x6162636465666768696a6b6c6d6e6f707172737475767778797a" + ); + } + + #[test] + fn test_txt_property_debug() { + // Test UTF-8 property value. + let prop_1 = TxtProperty { + key: "key1".to_string(), + val: Some("val1".to_string().into()), + }; + let prop_1_debug = format!("{:?}", &prop_1); + assert_eq!( + prop_1_debug, + "TxtProperty {key: \"key1\", val: Some(\"val1\")}" + ); + + // Test non-UTF-8 property value. + let prop_2 = TxtProperty { + key: "key2".to_string(), + val: Some(vec![150u8, 151u8, 152u8]), + }; + let prop_2_debug = format!("{:?}", &prop_2); + assert_eq!( + prop_2_debug, + "TxtProperty {key: \"key2\", val: Some(0x969798)}" + ); + } + + #[test] + fn test_txt_decode_property_size_out_of_bounds() { + // Construct a TXT record with an invalid property length that would be out of bounds. + let encoded: Vec = vec![ + 0x0b, // Length 11 + b'k', b'e', b'y', b'1', b'=', b'v', b'a', b'l', b'u', b'e', + b'1', // key1=value1 (Length 11) + 0x10, // Length 16 (Would be out of bounds) + b'k', b'e', b'y', b'2', b'=', b'v', b'a', b'l', b'u', b'e', + b'2', // key2=value2 (Length 11) + ]; + // Decode the record content + let decoded = decode_txt(&encoded); + // We expect the out of bounds length for the second property to have caused the rest of the record content to be skipped. + // Test that we only parsed the first property. + assert_eq!(decoded.len(), 1); + // Test that the key of the property we parsed is "key1" + assert_eq!(decoded[0].key, "key1"); + } + + #[test] + fn test_is_address_supported() { + let mut service_info = + ServiceInfo::new("_test._tcp", "prop_test", "testhost", "", 1234, None).unwrap(); + + let intf_v6 = Interface { + name: "foo".to_string(), + index: Some(1), + addr: IfAddr::V6(Ifv6Addr { + ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0x1234, 0, 0, 1), + netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_v4 = Interface { + name: "bar".to_string(), + index: Some(1), + addr: IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(192, 1, 2, 3), + netmask: Ipv4Addr::new(255, 255, 0, 0), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_baz = Interface { + name: "baz".to_string(), + index: Some(1), + addr: IfAddr::V6(Ifv6Addr { + ip: Ipv6Addr::new(0x2003, 0xdb8, 0, 0, 0x1234, 0, 0, 1), + netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_loopback_v4 = Interface { + name: "foo".to_string(), + index: Some(1), + addr: IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(127, 0, 0, 1), + netmask: Ipv4Addr::new(255, 255, 255, 255), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_loopback_v6 = Interface { + name: "foo".to_string(), + index: Some(1), + addr: IfAddr::V6(Ifv6Addr { + ip: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1), + netmask: Ipv6Addr::new( + 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, + ), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_link_local_v4 = Interface { + name: "foo".to_string(), + index: Some(1), + addr: IfAddr::V4(Ifv4Addr { + ip: Ipv4Addr::new(169, 254, 0, 1), + netmask: Ipv4Addr::new(255, 255, 0, 0), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + let intf_link_local_v6 = Interface { + name: "foo".to_string(), + index: Some(1), + addr: IfAddr::V6(Ifv6Addr { + ip: Ipv6Addr::new(0xfe80, 0, 0, 0, 0x1234, 0, 0, 1), + netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0), + broadcast: None, + prefixlen: 16, + }), + oper_status: IfOperStatus::Up, + is_p2p: false, + #[cfg(windows)] + adapter_name: String::new(), + }; + + // supported addresses not specified + assert!(service_info.is_address_supported(&intf_v6)); + + // Interface not supported + service_info.set_interfaces(vec![ + IfKind::Name("foo".to_string()), + IfKind::Name("bar".to_string()), + ]); + assert!(!service_info.is_address_supported(&intf_baz)); + + // link-local only + service_info.set_link_local_only(true); + assert!(!service_info.is_address_supported(&intf_v4)); + assert!(!service_info.is_address_supported(&intf_v6)); + assert!(service_info.is_address_supported(&intf_link_local_v4)); + assert!(service_info.is_address_supported(&intf_link_local_v6)); + service_info.set_link_local_only(false); + + // supported interfaces: IfKing::All + service_info.set_interfaces(vec![IfKind::All]); + assert!(service_info.is_address_supported(&intf_v6)); + assert!(service_info.is_address_supported(&intf_v4)); + + // supported interfaces: IfKind::IPv6 + service_info.set_interfaces(vec![IfKind::IPv6]); + assert!(service_info.is_address_supported(&intf_v6)); + assert!(!service_info.is_address_supported(&intf_v4)); + + // supported interfaces: IfKind::IPv4 + service_info.set_interfaces(vec![IfKind::IPv4]); + assert!(service_info.is_address_supported(&intf_v4)); + assert!(!service_info.is_address_supported(&intf_v6)); + + // supported interfaces: IfKind::Addr + service_info.set_interfaces(vec![IfKind::Addr(intf_v6.ip())]); + assert!(service_info.is_address_supported(&intf_v6)); + assert!(!service_info.is_address_supported(&intf_v4)); + + // supported interfaces: IfKind::LoopbackV4 + service_info.set_interfaces(vec![IfKind::LoopbackV4]); + assert!(service_info.is_address_supported(&intf_loopback_v4)); + assert!(!service_info.is_address_supported(&intf_loopback_v6)); + + // supported interfaces: IfKind::LoopbackV6 + service_info.set_interfaces(vec![IfKind::LoopbackV6]); + assert!(!service_info.is_address_supported(&intf_loopback_v4)); + assert!(service_info.is_address_supported(&intf_loopback_v6)); + + // supported interfaces: IPv4 and name = "foo" + service_info.set_interfaces(vec![IfKind::Predicate(IfPredicate::new(|intf| { + intf.ip().is_ipv4() && intf.name == "foo" + }))]); + assert!(service_info.is_address_supported(&intf_loopback_v4)); + assert!(!service_info.is_address_supported(&intf_v4)); + assert!(!service_info.is_address_supported(&intf_loopback_v6)); + } + + #[test] + fn test_scoped_ip_set_detects_interface_id_change() { + use std::collections::HashSet; + + use crate::{InterfaceId, ScopedIp, ScopedIpV4}; + + let intf1 = InterfaceId { + name: "en0".to_string(), + index: 1, + }; + let intf2 = InterfaceId { + name: "en1".to_string(), + index: 2, + }; + let addr = Ipv4Addr::new(192, 168, 1, 100); + + let scoped_v4_one_intf = ScopedIpV4::new(addr, intf1); + let mut scoped_v4_two_intfs = scoped_v4_one_intf.clone(); + scoped_v4_two_intfs.add_interface_id(intf2); + + assert_ne!(scoped_v4_one_intf, scoped_v4_two_intfs); + + let set_old: HashSet = HashSet::from([ScopedIp::V4(scoped_v4_one_intf)]); + let set_new: HashSet = HashSet::from([ScopedIp::V4(scoped_v4_two_intfs)]); + + assert_ne!(set_old, set_new); + } + + #[cfg(test)] + #[cfg(feature = "serde")] + mod serde { + use std::{collections::HashSet, net::IpAddr}; + + use super::{Ipv4Addr, Ipv6Addr}; + use crate::{ResolvedService, ScopedIp, TxtProperties}; + + #[test] + fn test_deserialize_serialize() -> Result<(), Box> { + let addresses = HashSet::from([ + ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ScopedIp::from(IpAddr::V6(Ipv6Addr::new( + 0xfe80, 0x2001, 0x0db8, 0x85a3, 0x0000, 0x8a2e, 0x0370, 0x7334, + ))), + ]); + + let service = ResolvedService { + ty_domain: "_http._tcp.local.".to_owned(), + sub_ty_domain: None, + fullname: "example._http._tcp.local.".to_owned(), + host: "example.local.".to_owned(), + port: 1234, + addresses, + txt_properties: TxtProperties::new(), + observed_source: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))), + }; + + let json = serde_json::to_value(&service)?; + + let parsed: ResolvedService = serde_json::from_value(json)?; + + assert!(compare(&service, &parsed)); + + Ok(()) + } + + fn compare(service: &ResolvedService, other: &ResolvedService) -> bool { + service.ty_domain == other.ty_domain + && service.sub_ty_domain == other.sub_ty_domain + && service.fullname == other.fullname + && service.host == other.host + && service.port == other.port + && service.addresses == other.addresses + && service.txt_properties == other.txt_properties + && service.observed_source == other.observed_source + } + } +} diff --git a/vendor/mdns-sd/tests/addr_parse.rs b/vendor/mdns-sd/tests/addr_parse.rs new file mode 100644 index 0000000..f5a00b8 --- /dev/null +++ b/vendor/mdns-sd/tests/addr_parse.rs @@ -0,0 +1,148 @@ +use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +use mdns_sd::AsIpAddrs; + +#[test] +fn test_addr_str() { + assert_eq!( + "127.0.0.1".as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); + + let addr = "127.0.0.1".to_string(); + assert_eq!( + addr.as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); + + // verify that `&String` also works. + assert_eq!( + addr.as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); + + assert_eq!( + "127.0.0.1,127.0.0.2".as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + set.insert(Ipv4Addr::new(127, 0, 0, 2).into()); + + set + }) + ); + + let addr = "2001:db8::1".to_string(); + assert_eq!( + addr.as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into()); + + set + }) + ); + + assert_eq!( + "2001:db8::1,2001:db8::2".as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into()); + set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2).into()); + + set + }) + ); + + // verify that an empty string parsed into an empty set. + assert_eq!("".as_ip_addrs(), Ok(HashSet::new())); +} + +#[test] +fn test_addr_slice() { + assert_eq!( + (&["127.0.0.1"][..]).as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); + + assert_eq!( + (&["127.0.0.1", "127.0.0.2"][..]).as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + set.insert(Ipv4Addr::new(127, 0, 0, 2).into()); + + set + }) + ); + + assert_eq!( + (&vec!["127.0.0.1", "127.0.0.2"][..]).as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + set.insert(Ipv4Addr::new(127, 0, 0, 2).into()); + + set + }) + ); + + assert_eq!( + (&vec!["2001:db8::1", "2001:db8::2"][..]).as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into()); + set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2).into()); + + set + }) + ); +} + +#[test] +fn test_addr_ip() { + let ip: IpAddr = Ipv4Addr::new(127, 0, 0, 1).into(); + + assert_eq!( + ip.as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); + + assert_eq!( + ip.as_ip_addrs(), + Ok({ + let mut set = HashSet::new(); + set.insert(Ipv4Addr::new(127, 0, 0, 1).into()); + + set + }) + ); +} diff --git a/vendor/mdns-sd/tests/mdns_test.rs b/vendor/mdns-sd/tests/mdns_test.rs new file mode 100644 index 0000000..043f5a7 --- /dev/null +++ b/vendor/mdns-sd/tests/mdns_test.rs @@ -0,0 +1,2707 @@ +use std::{ + collections::{HashMap, HashSet}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + thread::sleep, + time::{Duration, SystemTime}, +}; + +use if_addrs::{IfAddr, Interface}; +use mdns_sd::{ + DaemonEvent, + DaemonStatus, + HostnameResolutionEvent, + IfKind, + InterfaceId, + IntoTxtProperties, + RRType, + ScopedIp, + ServiceDaemon, + ServiceEvent, + ServiceInfo, + TxtProperty, + UnregisterStatus, +}; +use test_log::test; + +/// This test covers: +/// register(announce), browse(query), response, unregister, shutdown. +#[test] +fn integration_success() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service + let ty_domain = "_mdns-sd-it._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + + let all_interfaces = my_ip_interfaces(); + println!("all interfaces count: {}", all_interfaces.len()); + // as we send only once per interface and ip we need a count of unique addresses to verify number of sent unregisters later on + let mut unique_intf_idx_ip_ver_set = HashSet::new(); + let mut non_idx_count = 0; + for intf in all_interfaces.iter() { + let ip_ver = match intf.addr { + IfAddr::V4(_) => 4u8, + IfAddr::V6(_) => 6u8, + }; + + // use the same approach as `IntfSock.multicast_send_tracker` + if let Some(idx) = intf.index { + if !unique_intf_idx_ip_ver_set.insert((idx, ip_ver)) { + println!("index {idx} IP v{ip_ver} repeated on interface {}, likely multi-addr on the same interface", intf.name); + } + } else { + non_idx_count += 1; + } + } + let unique_intf_idx_ip_ver_count = unique_intf_idx_ip_ver_set.len() + non_idx_count; + + let ifaddrs_set: HashSet<_> = all_interfaces.iter().map(|intf| intf.ip()).collect(); + let my_ifaddrs: Vec<_> = ifaddrs_set.into_iter().collect(); + let my_addrs_count = my_ifaddrs.len(); + println!("My IP {} addr(s):", my_ifaddrs.len()); + for item in my_ifaddrs.iter() { + println!("{}", &item); + } + + let host_name = "INTEGRATION_host.local."; + let port = 5200; + let mut properties = HashMap::new(); + properties.insert("property_1".to_string(), "test".to_string()); + properties.insert("property_2".to_string(), "1".to_string()); + properties.insert("property_3".to_string(), "1234".to_string()); + + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + host_name, + &my_ifaddrs[..], + port, + Some(properties), + ) + .expect("valid service info"); + let fullname = my_service.get_fullname().to_string(); + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for a service + let mut resolved_ips: HashSet = HashSet::new(); + let mut addr_count = 0; + + let browse_chan = d.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::SearchStarted(ty_domain) => { + println!("Search started for {}", &ty_domain); + } + ServiceEvent::ServiceFound(_ty_domain, fullname) => { + println!("Found a new service: {}", &fullname); + } + ServiceEvent::ServiceResolved(info) => { + let addrs: HashSet<_> = info + .get_addresses() + .iter() + .map(|a| a.to_ip_addr()) + .collect(); + addr_count = addrs.len(); + + println!( + "Resolved a new service: {} with {} addr(s)", + info.get_fullname(), + addrs.len() + ); + for a in addrs.iter() { + println!("{}", a); + } + if info.get_fullname().contains(&instance_name) { + resolved_ips.extend(addrs); + } + let hostname = info.get_hostname(); + assert_eq!(hostname, host_name); + + let service_port = info.get_port(); + assert_eq!(service_port, port); + + let properties = info.get_properties(); + assert!(properties.get("property_1").is_some()); + assert!(properties.get("property_2").is_some()); + assert_eq!(properties.len(), 3); + assert!(info.get_property("property_1").is_some()); + assert!(info.get_property("property_2").is_some()); + assert_eq!(info.get_property_val_str("property_1"), Some("test")); + assert_eq!(info.get_property_val_str("property_2"), Some("1")); + assert_eq!( + info.get_property_val("property_1").unwrap(), + Some("test".as_bytes()) + ); + } + _ => {} + } + } + + // All addrs should have been resolved. + assert_eq!(addr_count, my_addrs_count); + + // IP's can get resolved more than once if fx a cache-flush is asked from the sender of the + // MDNS records, so we look at unique IP addresses to see if they match the number of the + // network interfaces. + assert_eq!(resolved_ips.len(), my_addrs_count); + assert!(!resolved_ips.is_empty()); + + // Unregister the service + let receiver = d.unregister(&fullname).unwrap(); + let response = receiver.recv().unwrap(); + assert!(matches!(response, UnregisterStatus::OK)); + + let mut remove_count = 0; + let mut resolved = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceRemoved(_ty_domain, fullname) => { + println!("Removed service: {}", &fullname); + if fullname.contains(&instance_name) { + remove_count += 1; + } + break; + } + ServiceEvent::ServiceResolved(info) => { + if info.get_fullname() == fullname { + println!("Received a resolved service event after unregister"); + resolved = true; + } + } + _ => {} + } + } + + assert_eq!(remove_count, 1); + assert!( + !resolved, + "Resolved event should not be received after unregister" + ); + + // Stop browsing the service. + d.stop_browse(ty_domain).expect("Failed to stop browsing"); + + let mut stopped_count = 0; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::SearchStopped(ty) = event { + println!("Search stopped for {}", &ty); + stopped_count += 1; + break; + } + } + + assert_eq!(stopped_count, 1); + + // Verify metrics. + let metrics_receiver = d.get_metrics().unwrap(); + let metrics = metrics_receiver.recv().unwrap(); + println!("metrics: {:?}", &metrics); + assert_eq!(metrics["register"], 1); + assert_eq!(metrics["unregister"], 1); + assert!(metrics["register-resend"] >= 1); + + println!("unique interface set: {:?}", unique_intf_idx_ip_ver_set); + assert_eq!( + metrics["unregister-resend"], + unique_intf_idx_ip_ver_count as i64 + ); + assert!(metrics["browse"] >= 2); // browse has been retransmitted. + + // respond has been sent for every browse, or they are suppressed by "known answer". + let respond_count = metrics.get("respond").unwrap_or(&0); + let known_answer_count = metrics.get("known-answer-suppression").unwrap_or(&0); + assert!(*respond_count >= 2 || *known_answer_count > 0); + + // Test the special meta-query of "_services._dns-sd._udp.local." + let service2_type = "_my-service2._udp.local."; + let service2_instance = "instance2"; + let service2 = ServiceInfo::new( + service2_type, + service2_instance, + host_name, + &my_ifaddrs[..], + port, + None, + ) + .expect("valid service info"); + d.register(service2) + .expect("Failed to register the 2nd service"); + + // Browse using the special meta-query. + let meta_query = "_services._dns-sd._udp.local."; + let browse_chan = d.browse(meta_query).unwrap(); + let timeout = Duration::from_secs(2); + + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceFound(ty_domain, fullname) => { + println!("Found a service of {}: {}", &ty_domain, &fullname); + // Among all services found, should have our 2nd service. + if fullname == service2_type { + break; + } + } + e => { + println!("Received event {:?}", e); + sleep(Duration::from_millis(100)); + } + }, + Err(e) => { + panic!("browse error: {}", e); + } + } + } + + // Shutdown + d.shutdown().unwrap(); +} + +#[test] +fn service_without_properties_with_alter_net_v4() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service without properties. + let ty_domain = "_serv-no-prop._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let if_addrs: Vec = my_ip_interfaces() + .into_iter() + .filter(|iface| iface.addr.ip().is_ipv4()) + .collect(); + 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-v4.local."; + let port = 5201; + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + host_name, + &host_ip[..], + port, + None, + ) + .expect("valid service info"); + let fullname = my_service.get_fullname().to_string(); + d.register(my_service) + .expect("Failed to register our service"); + println!("Registered service with host_ip: {:?}", &host_ip); + + // Browse for a service + let browse_chan = d.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let timer = std::time::Instant::now() + timeout; + let mut found = false; + while std::time::Instant::now() < timer { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + info.get_addresses() + ); + // match only our service and not v6 one + if info.get_addresses_v4().is_empty() { + continue; + } + if fullname.as_str() == info.get_fullname() { + let addrs = info.get_addresses_v4(); + assert_eq!(addrs.len(), 1); // first_ipv4 but no alter_ipv. + found = true; + break; + } + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + panic!("browse error: {}", e); + } + } + } + + d.shutdown().unwrap(); + assert!(found); +} + +#[test] +fn service_without_properties_with_alter_net_v6() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service without properties. + let ty_domain = "_serv-no-prop._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let if_addrs: Vec = my_ip_interfaces() + .into_iter() + .filter(|iface| iface.addr.ip().is_ipv6()) + .collect(); + 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-v6.local."; + let port = 5201; + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + host_name, + &host_ip[..], + port, + None, + ) + .expect("valid service info"); + let fullname = my_service.get_fullname().to_string(); + d.register(my_service) + .expect("Failed to register our service"); + println!("Registered service with host_ip: {:?}", &host_ip); + + // Browse for a service + let browse_chan = d.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let timer = std::time::Instant::now() + timeout; + let mut found = false; + while std::time::Instant::now() < timer { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + info.get_addresses() + ); + // match only our service and not v4 one + if fullname.as_str() == info.get_fullname() { + let addrs: Vec<_> = info + .get_addresses() + .iter() + .filter(|a| a.is_ipv6()) + .collect(); + if addrs.is_empty() { + continue; // In case IPv4 addr received first. + } + assert_eq!(addrs.len(), 1); // first_ipv6 but no alter_ipv. + found = true; + break; + } + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + panic!("browse error: {}", e); + } + } + } + + d.shutdown().unwrap(); + assert!(found); +} + +#[test] +fn service_txt_properties_case_insensitive() { + // Register a service with properties. + let domain = "_serv-properties._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "properties_host.local."; + let port = 5201; + let properties = [ + ("prop_CAP_CASE", "one"), + ("prop_cap_case", "two"), + ("prop_Cap_Lower", "three"), + ]; + + let my_service = ServiceInfo::new(domain, &instance_name, host_name, "", port, &properties[..]) + .expect("valid service info") + .enable_addr_auto(); + let props = my_service.get_properties(); + assert_eq!(props.len(), 2); + + // Verify `get_property()` method is case insensitive and returns + // the first property with the same key. + let prop_cap_case = my_service.get_property("prop_CAP_CASE").unwrap(); + assert_eq!(prop_cap_case.val_str(), "one"); + assert_eq!(prop_cap_case.val(), Some("one".as_bytes())); + + // Verify the original property name is kept. + let prop_mixed = my_service.get_property("prop_cap_lower").unwrap(); + assert_eq!(prop_mixed.key(), "prop_Cap_Lower"); +} + +#[test] +fn service_txt_properties_key_ascii() { + let domain = "_mdns-ascii._tcp.local."; + let instance = "test_service_info_key_ascii"; + let port = 5202; + + // Verify that a key must contain ASCII only. E.g. cannot have emojis. + let properties = [("prop_ascii", "one"), ("prop_🤗", "hugging_face")]; + let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]); + assert!(my_service.is_err()); + if let Err(e) = my_service { + let msg = format!("ERROR: {}", e); + assert!(msg.contains("not ASCII")); + } + + // Verify that a key cannot contain '='. + let properties = [("prop_ascii", "one"), ("prop_=", "equal sign")]; + let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]); + assert!(my_service.is_err()); + if let Err(e) = my_service { + let msg = format!("ERROR: {}", e); + assert!(msg.contains('=')); + } + + // Verify that properly formatted keys are OK. + let properties = [("prop_ascii", "one"), ("prop_2", "two")]; + let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]); + assert!(my_service.is_ok()); +} + +#[test] +fn test_txt_properties_into_hashmap_str() { + // Test valid UTF-8 properties + let properties = [("key1", "val1"), ("key2", "val2")].into_txt_properties(); + let property_map = properties.into_property_map_str(); + println!("property_map: {:?}", property_map); + assert_eq!(property_map.len(), 2); + assert_eq!(property_map.get("key1"), Some(&"val1".to_string())); + assert_eq!(property_map.get("key2"), Some(&"val2".to_string())); + + // Test property with no value and property with invalid UTF-8 + let invalid_vec: Vec = vec![200, 200]; // Invalid UTF-8 bytes + let prop1 = TxtProperty::from("key1"); + let prop2 = TxtProperty::from(("key2", invalid_vec.as_slice())); + let properties = vec![prop1, prop2].into_txt_properties(); + let property_map = properties.into_property_map_str(); + + // Property with no value should map to empty string + // Property with invalid UTF-8 should be skipped + assert_eq!(property_map.get("key1"), Some(&"".to_string())); + assert_eq!(property_map.len(), 1); +} + +#[test] +fn test_into_txt_properties() { + // Verify (&str, String) tuple is supported. + let properties = [("key1", String::from("val1"))]; + let txt_props = properties.into_txt_properties(); + assert_eq!(txt_props.get_property_val_str("key1").unwrap(), "val1"); + assert_eq!( + txt_props.get_property_val("key1").unwrap(), + Some("val1".as_bytes()) + ); + + // Verify (String, String) tuple is supported. + let properties = [(String::from("key2"), String::from("val2"))]; + let txt_props = properties.into_txt_properties(); + assert_eq!(txt_props.get_property_val_str("key2").unwrap(), "val2"); +} + +#[test] +fn test_info_as_resolved_service() { + let sub_ty_domain = "_printer._sub._test._tcp.local."; + let service_info = ServiceInfo::new( + sub_ty_domain, + "my_instance", + "my_host.local.", + "192.168.0.1", + 5200, + None, + ) + .unwrap(); + let resolved_service = service_info.as_resolved_service(); + assert!(resolved_service.is_valid()); + assert_eq!(resolved_service.sub_ty_domain.unwrap(), sub_ty_domain); + assert_eq!(resolved_service.ty_domain, "_test._tcp.local."); + + let info_missing_addr = ServiceInfo::new( + "_test._tcp.local.", + "my_instance", + "my_host.local.", + "", + 5200, + None, + ) + .unwrap(); + let invalid_service = info_missing_addr.as_resolved_service(); + assert!(!invalid_service.is_valid()); + assert!(invalid_service.sub_ty_domain.is_none()); +} + +/// Test enabling an interface using its name, for example "en0". +/// Also tests an instance name with Upper Case. +#[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 = "named_intf_host.local."; + let host_ipv4 = ""; + let port = 5202; + let my_service = ServiceInfo::new( + my_ty_domain, + "UpperCaseInstance", + 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; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + 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); + } + } + } + + assert!(!resolved); + + // 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(3); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + 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); + } + } + } + + 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.local."; + 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; + + // run till the timeout and collect the resolved addresses + // from all enabled interfaces. + while let Ok(event) = browse_chan.recv_timeout(timeout) { + 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().is_empty()); + for addr in info.get_addresses().iter() { + assert!(addr.is_ipv4()); + } + // We don't break here, as there could be more addresses coming. + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + d.shutdown().unwrap(); +} + +#[test] +fn service_ipv6_link_local_only() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + let service_ipv6_intf = "_test_ipv6_intf._udp.local."; + let host_name = "my_host_ipv6_intf.local."; + let host_ipv4 = ""; + let port = 5201; + let mut my_service = ServiceInfo::new( + service_ipv6_intf, + "my_instance", + host_name, + host_ipv4, + port, + None, + ) + .expect("invalid service info") + .enable_addr_auto(); + my_service.set_interfaces(vec![IfKind::IPv6]); + my_service.set_link_local_only(true); + let result = d.register(my_service); + assert!(result.is_ok()); + + // Browse for a services. Verify all addresses are link-local IPv6. + let browse_chan = d.browse(service_ipv6_intf).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + // run till the timeout and collect the resolved addresses + // from all enabled interfaces. + while let Ok(event) = browse_chan.recv_timeout(timeout) { + 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().is_empty()); + for addr in info.get_addresses().iter() { + assert!(addr.is_ipv6()); + assert!( + matches!(addr.to_ip_addr(), IpAddr::V6(ipv6) if (ipv6.segments()[0] & 0xffc0) == 0xfe80) + ); + } + // We don't break here, as there could be more addresses coming. + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + d.shutdown().unwrap(); +} + +#[test] +fn test_disable_interface_cache() { + // Create a server + let server = ServiceDaemon::new().expect("Failed to create the server"); + + // Register a service with one IPv4. + let ty_domain = "_disable-intf._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); + let ipv4_list: Vec<_> = my_ip_interfaces() + .iter() + .map(|iface| iface.ip()) + .filter(|ip| ip.is_ipv4() && !ip.is_loopback()) + .collect(); + + let host_name = "disabled_intf_host.local."; + let port = 5201; + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + host_name, + &ipv4_list[..], + port, + None, + ) + .expect("Invalid service info"); + server + .register(my_service) + .expect("Failed to register our service"); + + // Create a client + let client = ServiceDaemon::new().expect("Failed to create the client"); + + // Give it some time to cache mDNS records. + sleep(Duration::from_secs(1)); + + // Disable the interface for the client. + println!("Disabling interface with IP: {:?}", ipv4_list); + client.disable_interface(ipv4_list).unwrap(); + + // Browse for the service. + let handle = client.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(1); + let mut resolved = false; + + // run till timeout and it should not resolve. + while let Ok(event) = handle.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + info.get_addresses() + ); + resolved = true; + break; + } + } + + // We cannot resolve the service because the interface is disabled. + assert!(!resolved); + + // Clean up. + server.shutdown().unwrap(); + client.shutdown().unwrap(); +} + +#[test] +fn service_with_invalid_addr_v4() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service without properties. + let ty_domain = "_invalid-addr._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let if_addrs: Vec = my_ip_interfaces() + .into_iter() + .filter(|iface| iface.addr.ip().is_ipv4()) + .collect(); + let alter_ip = ipv4_alter_net(&if_addrs); + let host_name = "invalid_ipv4_host.local."; + let port = 5201; + let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None) + .expect("valid service info"); + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for a service + let browse_chan = d.browse(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) => { + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + info.get_addresses() + ); + resolved = true; + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + println!("browse error: {}", e); + break; + } + } + } + + d.shutdown().unwrap(); + + // We cannot resolve the service because the published address + // is not valid in the LAN. + assert!(!resolved); +} + +#[test] +fn service_with_invalid_addr_v6() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service without properties. + let ty_domain = "_invalid-addr._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let if_addrs: Vec = my_ip_interfaces() + .into_iter() + .filter(|iface| iface.addr.ip().is_ipv6()) + .collect(); + let alter_ip = ipv6_alter_net(&if_addrs); + let host_name = "my_host.local."; + let port = 5201; + let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None) + .expect("valid service info"); + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for a service + let browse_chan = d.browse(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) => { + println!( + "Resolved a service of {} addr(s): {:?}", + &info.get_fullname(), + info.get_addresses() + ); + resolved = true; + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + println!("browse error: {}", e); + break; + } + } + } + + d.shutdown().unwrap(); + + // We cannot resolve the service because the published address + // is not valid in the LAN. + assert!(!resolved); +} + +#[test] +fn service_with_loopback_addr() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + d.enable_interface(IfKind::LoopbackV4) + .expect("Failed to enable loopback interface"); + + // Define a unique service type and instance name. + let ty_domain = "_test-loopback._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); + + // Use a loopback address (127.0.0.1) for the service. + let loopback_ip: IpAddr = "127.0.0.1".parse().unwrap(); + let host_name = "localhost.local."; + let port = 5201; + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + host_name, + loopback_ip, + port, + None, + ) + .expect("valid service info"); + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for the service. + let browse_chan = d.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let mut found_loopback = false; + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved service {} with addresses: {:?}", + info.get_fullname(), + info.get_addresses() + ); + // Check that at least one of the addresses is a loopback address. + if info.get_addresses().iter().any(|ip| ip.is_loopback()) { + found_loopback = true; + } + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + println!("browse error: {}", e); + break; + } + } + } + + d.shutdown().unwrap(); + + // Assert that the resolved service includes a loopback address. + assert!( + found_loopback, + "The service should include a loopback address" + ); +} + +#[test] +fn subtype() { + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service with a subdomain + let subtype_domain = "_directory._sub._test-subtype._tcp.local."; + let ty_domain = "_test-subtype._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_ipv4 = my_ip_interfaces()[0].ip().to_string(); + let host_name = "subtype_host.local."; + let port = 5201; + let my_service = ServiceInfo::new( + subtype_domain, + &instance_name, + host_name, + host_ipv4, + port, + None, + ) + .expect("valid service info"); + let fullname = my_service.get_fullname().to_string(); + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for the service via ty_domain and subtype_domain + for domain in [ty_domain, subtype_domain].iter() { + let browse_chan = d.browse(domain).unwrap(); + let timeout = Duration::from_secs(2); + loop { + match browse_chan.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service of {} subdomain {:?}", + &info.get_fullname(), + info.get_subtype() + ); + assert_eq!(fullname.as_str(), info.get_fullname()); + assert_eq!(subtype_domain, info.get_subtype().as_ref().unwrap()); + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + panic!("browse error: {}", e); + } + } + } + } + + d.shutdown().unwrap(); +} + +/// Verify service name has to be valid. +#[test] +fn test_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 = ""; + let host_name = "my_host.local."; + let port = 5200; + let mut my_service = ServiceInfo::new( + service_name_too_long, + "my_instance", + host_name, + host_ipv4, + port, + None, + ) + .expect("valid service info") + .enable_addr_auto(); + + my_service.set_requires_probe(false); + + 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(_))); + if let DaemonEvent::Error(e) = event { + 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 mut published = false; + let publish_timeout = 1200; + while let Ok(event) = monitor.recv_timeout(Duration::from_millis(publish_timeout)) { + match event { + DaemonEvent::Announce(_, _) => { + published = true; + break; + } + other => { + println!("other daemon events: {:?}", other); + } + } + } + assert!(published); + + // 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(); +} + +#[test] +fn service_new_publish_after_browser() { + let service_type = "_new-pub._udp.local."; + let daemon = ServiceDaemon::new().expect("Failed to create a new daemon"); + + // First, starts the browser. + let receiver = daemon.browse(service_type).unwrap(); + + sleep(Duration::from_millis(1000)); + + let txt_properties = [("key1", "value1")]; + let service_info = ServiceInfo::new( + "_new-pub._udp.local.", + "test1", + "my_host.local.", + "", + 1234, + &txt_properties[..], + ) + .expect("valid service info") + .enable_addr_auto(); + + // Second, publish a service. + let result = daemon.register(service_info); + assert!(result.is_ok()); + + let mut resolved = false; + let timeout = Duration::from_secs(2); + loop { + match receiver.recv_timeout(timeout) { + Ok(event) => match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service of {} addr(s): {:?} props: {:?}", + &info.get_fullname(), + info.get_addresses(), + info.get_properties() + ); + resolved = true; + break; + } + e => { + println!("Received event {:?}", e); + } + }, + Err(e) => { + println!("browse error: {}", e); + break; + } + } + } + + assert!(resolved); + daemon.shutdown().unwrap(); +} + +fn is_apple_p2p_by_name(name: &str) -> bool { + let p2p_prefixes = ["awdl", "llw"]; + p2p_prefixes.iter().any(|prefix| name.starts_with(prefix)) +} + +fn my_ip_interfaces() -> Vec { + if_addrs::get_if_addrs() + .unwrap_or_default() + .into_iter() + .filter_map(|i| { + if i.is_loopback() || i.is_p2p() || is_apple_p2p_by_name(&i.name) { + None + } else { + match &i.addr { + IfAddr::V4(ifv4) => + // Use a 'bind' to check if this is a valid IPv4 addr. + { + match std::net::UdpSocket::bind((ifv4.ip, 0)) { + Ok(_) => Some(i), + Err(e) => { + println!("failed to bind {}: {e}, skipped.", ifv4.ip); + None + } + } + } + IfAddr::V6(ifv6) => + // Use a 'bind' to check if this is a valid IPv6 addr. + { + let mut sock = std::net::SocketAddrV6::new(ifv6.ip, 0, 0, 0); + if i.is_link_local() { + // Only link local IPv6 address requires to specify scope_id + sock.set_scope_id(i.index.unwrap_or(0)); + } + + match std::net::UdpSocket::bind(sock) { + Ok(_) => Some(i), + Err(e) => { + println!("failed to bind {}: {e}, skipped.", ifv6.ip); + None + } + } + } + } + } + }) + .collect() +} + +/// Returns a made-up IPv4 address "net.1.1.1", where +/// `net` is one higher than any of IPv4 addresses on the host. +/// +/// The idea is that this made-up address does not belong to +/// the same network as any of the host addresses. +fn ipv4_alter_net(if_addrs: &[Interface]) -> IpAddr { + let mut net_max = 0; + for if_addr in if_addrs.iter() { + match &if_addr.addr { + IfAddr::V4(iface) => { + let net = iface.ip.octets()[0]; + if net > net_max { + net_max = net; + } + } + _ => panic!(), + } + } + Ipv4Addr::new(net_max + 1, 1, 1, 1).into() +} + +/// Returns a made-up IPv6 address "net:1:1:1:1:1:1:1", where +/// `net` is one higher than any of IPv6 addresses on the host. +/// +/// The idea is that this made-up address does not belong to +/// the same network as any of the host addresses. +fn ipv6_alter_net(if_addrs: &[Interface]) -> IpAddr { + let mut net_max = 0; + for if_addr in if_addrs.iter() { + match &if_addr.addr { + IfAddr::V6(iface) => { + let net = iface.ip.octets()[0]; + if net > net_max { + net_max = net; + } + } + _ => panic!(), + } + } + Ipv6Addr::new(net_max as u16 + 1, 1, 1, 1, 1, 1, 1, 1).into() +} + +#[test] +fn test_shutdown() { + let mdns = ServiceDaemon::new().unwrap(); + + // Check the status. + let receiver = mdns.status().unwrap(); + let status = receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Running)); + + // Shutdown the daemon immediately. + let receiver = mdns.shutdown().unwrap(); + let status = receiver.recv().unwrap(); + println!("daemon status: {:?}", status); + + // Try to register and it should fail. + let service_type = "_mdns-sd-my-test._udp.local."; + let instance_name = "my_instance"; + let ip = "192.168.1.12"; + let host_name = "192.168.1.12.local."; + let port = 5200; + let properties = [("property_1", "test"), ("property_2", "1234")]; + + let my_service = ServiceInfo::new( + service_type, + instance_name, + host_name, + ip, + port, + &properties[..], + ) + .unwrap(); + + let result = mdns.register(my_service); + assert!(result.is_err()); + + // Check the status again. + let receiver = mdns.status().unwrap(); + let status = receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); +} + +#[test] +fn test_hostname_resolution() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + let hostname = "my_host._tcp.local."; + let service_ip_addr: ScopedIp = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.into()) + .unwrap(); + + let my_service = ServiceInfo::new( + "_host_res_test._tcp.local.", + "my_instance", + hostname, + &[service_ip_addr.to_ip_addr()] as &[IpAddr], + 1234, + None, + ) + .expect("invalid service info"); + d.register(my_service).unwrap(); + + let event_receiver = d.resolve_hostname(hostname, Some(2000)).unwrap(); + let resolved = loop { + match event_receiver.recv() { + Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => { + assert!(found_hostname == hostname); + assert!(addresses.contains(&service_ip_addr)); + break true; + } + Ok(HostnameResolutionEvent::SearchStopped(_)) => break false, + Ok(event) => println!("Received event {:?}", event), + Err(_) => break false, + } + }; + + assert!(resolved); + d.shutdown().unwrap(); +} + +#[test] +fn test_hostname_resolution_case_insensitive() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + let hostname = "My_casE_HOST.local."; + let service_ip_addr: ScopedIp = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.into()) + .unwrap(); + + let my_service = ServiceInfo::new( + "_host_case_test._tcp.local.", + "my_instance", + hostname, + &[service_ip_addr.to_ip_addr()] as &[IpAddr], + 1234, + None, + ) + .expect("invalid service info"); + d.register(my_service).unwrap(); + + // Verify that lowercase hostname resolves correctly. + let hostname_lower = hostname.to_lowercase(); + let event_receiver = d.resolve_hostname(&hostname_lower, Some(2000)).unwrap(); + let resolved = loop { + match event_receiver.recv() { + Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => { + assert!(found_hostname == hostname); + assert!(addresses.contains(&service_ip_addr)); + break true; + } + Ok(HostnameResolutionEvent::SearchStopped(_)) => break false, + Ok(_event) => {} + Err(_) => break false, + } + }; + + assert!(resolved); + + // Verify that any-case hostname resolves correctly. + let hostname_other = "MY_CASe_hOST.local."; + let event_receiver = d.resolve_hostname(hostname_other, Some(2000)).unwrap(); + let resolved = loop { + match event_receiver.recv() { + Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => { + assert!(found_hostname == hostname); + assert!(addresses.contains(&service_ip_addr)); + break true; + } + Ok(HostnameResolutionEvent::SearchStopped(_)) => break false, + Ok(_event) => {} + Err(_) => break false, + } + }; + assert!(resolved); + + d.shutdown().unwrap(); +} + +#[test] +fn hostname_resolution_timeout() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + let hostname = "nonexistent._tcp.local."; + + let before = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("failed to get current UNIX time") + .as_millis() as u64; + let event_receiver = d.resolve_hostname(hostname, Some(2000)).unwrap(); + let resolved = loop { + match event_receiver.recv() { + Ok(HostnameResolutionEvent::AddressesFound(found_hostname, _addresses)) => { + assert!(found_hostname == hostname); + break true; + } + Ok(HostnameResolutionEvent::SearchTimeout(_)) => break false, + Ok(event) => println!("Received event {:?}", event), + Err(_) => break false, + } + }; + let after = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("failed to get current UNIX time") + .as_millis() as u64; + + assert!(!resolved); + + println!("Time spent resolving: {} ms", after - before); + assert!(after - before >= 2000 - 5); + assert!(after - before < 2000 + 1000); + + d.shutdown().unwrap(); +} + +#[test] +fn test_cache_flush_record() { + // Create a daemon + let server = ServiceDaemon::new().expect("Failed to create server"); + let service = "_test_cache_ptr._udp.local."; + let host_name = "my_host_tmp_cache_flush.local."; + + // use a single IPv4 addr + let mut service_ip_addr = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let port = 5201; + let properties = [("key", "value")]; + let mut my_service = ServiceInfo::new( + service, + "my_instance", + host_name, + service_ip_addr, + port, + &properties[..], + ) + .expect("invalid service info"); + let result = server.register(my_service.clone()); + assert!(result.is_ok()); + + // Browse for a service + let client = ServiceDaemon::new().expect("Failed to create client"); + let browse_chan = client.browse(service).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + timed_println(format!("Resolved a service of {}", &info.get_fullname())); + timed_println(format!("JLN service: {:?}", info)); + break; + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + + // Stop browsing for a moment. + client.stop_browse(service).unwrap(); + sleep(Duration::from_secs(2)); // Let the cache record be surely older than 1 second. + + // Modify the IPv4 address for the service. + if let IpAddr::V4(ipv4) = service_ip_addr { + let bytes = ipv4.octets(); + service_ip_addr = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1)); + } else { + panic!(); + } + + // Re-register the service to update the IPv4 addr. + my_service = ServiceInfo::new( + service, + "my_instance", + host_name, + service_ip_addr, + port, + &properties[..], + ) + .unwrap(); + let result = server.register(my_service); + assert!(result.is_ok()); + + timed_println(format!( + "Re-registered with updated IPv4 addr: {}", + &service_ip_addr + )); + + // Wait for the new registration sent out and cache flushed. + sleep(Duration::from_secs(2)); + + // Browse for the updated IPv4 address. + let browse_chan = client.browse(service).unwrap(); + resolved = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + // Verify the address flushed and updated. + let new_addrs = info.get_addresses(); + timed_println(format!("new address resolved: {:?}", new_addrs)); + if new_addrs.len() == 1 { + let first_addr = new_addrs.iter().next().unwrap(); + assert_eq!(&first_addr.to_ip_addr(), &service_ip_addr); + resolved = true; + break; + } + } + e => { + timed_println(format!("Received event {:?}", e)); + } + } + } + + assert!(resolved); + server.shutdown().unwrap(); + client.shutdown().unwrap(); +} + +#[test] +fn test_cache_flush_remove_one_addr() { + // Create a daemon + let server = ServiceDaemon::new().expect("Failed to create server"); + let service = "_remove_one_addr._udp.local."; + let host_name = "remove_one_addr_host.local."; + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Make 2nd IPv4 address for the service. + let ip_addr2 = match ip_addr1 { + IpAddr::V4(ipv4) => { + let bytes = ipv4.octets(); + IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1)) + } + _ => { + panic!() + } + }; + + let port = 5201; + let mut my_service = ServiceInfo::new( + service, + "my_instance", + host_name, + &[ip_addr1, ip_addr2][..], + port, + None, + ) + .expect("invalid service info"); + let result = server.register(my_service.clone()); + assert!(result.is_ok()); + + // Browse for a service + let client = ServiceDaemon::new().expect("Failed to create client"); + let browse_chan = client.browse(service).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + println!("Resolved a service of {}", &info.get_fullname()); + break; + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + + // Stop browsing for a moment. + client.stop_browse(service).unwrap(); + sleep(Duration::from_secs(2)); // Wait 1 more second for the 2nd announcement + + // Re-register the service to have only 1 addr. + my_service = ServiceInfo::new(service, "my_instance", host_name, ip_addr1, port, None).unwrap(); + let result = server.register(my_service.clone()); + assert!(result.is_ok()); + + println!("Re-registered with updated IPv4 addr"); + + // Wait for the new registration sent out and cache flushed. + sleep(Duration::from_secs(2)); + + // Browse for the updated IPv4 address. + let browse_chan = client.browse(service).unwrap(); + resolved = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + // Verify the address flushed and updated. + let new_addrs = info.get_addresses(); + if new_addrs.len() == 1 { + let first_addr = new_addrs.iter().next().unwrap(); + assert_eq!(&first_addr.to_ip_addr(), &ip_addr1); + resolved = true; + break; + } + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + server.shutdown().unwrap(); + client.shutdown().unwrap(); +} + +/// Test to verify that the cache flush of SRV records +/// do not remove the service instance. +#[test] +fn test_cache_flush_srv() { + // Create a daemon + let server = ServiceDaemon::new().expect("Failed to create server"); + let service = "_test_cache_srv._udp.local."; + let old_host_name = "old_srv_host.local."; + let new_host_name = "new_srv_host.local."; + + // Use a single IPv4 address + let service_ip_addr = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let port = 5201; + let properties = [("key", "value")]; + let mut my_service = ServiceInfo::new( + service, + "my_instance", + old_host_name, + service_ip_addr, + port, + &properties[..], + ) + .expect("invalid service info"); + let result = server.register(my_service.clone()); + assert!(result.is_ok()); + + // Browse for the service + let client = ServiceDaemon::new().expect("Failed to create client"); + let browse_chan = client.browse(service).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + assert_eq!(info.get_hostname(), old_host_name); + break; + } + e => { + println!("Received event {:?}", e); + } + } + } + + assert!(resolved); + + sleep(Duration::from_secs(2)); // Let the cache record be older than 1 second + + // Re-register the service with a new host + my_service = ServiceInfo::new( + service, + "my_instance", + new_host_name, + service_ip_addr, + port, + &properties[..], + ) + .unwrap(); + let result = server.register(my_service); + assert!(result.is_ok()); + + println!("Re-registered with updated SRV host: {}", new_host_name); + + // Wait for the new registration to be sent out and cache flushed + sleep(Duration::from_secs(2)); + + // Browse for the updated SRV record + let timeout = Duration::from_secs(2); + let mut removed = false; + while let Ok(event) = browse_chan.recv_timeout(timeout) { + println!("Received event {:?}", event); + + match event { + ServiceEvent::ServiceRemoved(ty_domain, instance_name) => { + // Verify the SRV host was flushed and updated + assert_eq!(ty_domain, service); + assert!(instance_name.starts_with("my_instance")); + removed = true; + } + ServiceEvent::ServiceResolved(info) => { + assert_eq!(info.get_hostname(), new_host_name); + resolved = true; + } + _e => {} + } + } + + assert!(!removed); + assert!(resolved); + server.shutdown().unwrap(); + client.shutdown().unwrap(); +} + +#[test] +fn test_known_answer_suppression() { + // Create a daemon + let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server"); + + // Register a service + let ty_domain = "_known-answer._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let host_name = "known_answer_server.local."; + let port = 5200; + + // Publish the service + let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + mdns_server + .register(my_service) + .expect("Failed to register my service"); + + // Browse the service + let client = ServiceDaemon::new().expect("Failed to create mdns client"); + let browse_chan = client.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + resolved = true; + println!("Resolved a service of {}", &info.get_fullname()); + break; + } + other => { + println!("Received event {:?}", other); + } + } + } + assert!(resolved); + + // Browse again to trigger Known Answer Suppression for sure. + let browse_chan = client.browse(ty_domain).unwrap(); + resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + resolved = true; + println!("Resolved a service of {}", &info.get_fullname()); + break; + } + } + assert!(resolved); + + // Give the server daemon chances to handle the browse query again. + sleep(Duration::from_secs(1)); + + // Verify Known Answer Suppression happened. + let metrics_receiver = mdns_server.get_metrics().unwrap(); + let metrics = metrics_receiver.recv().unwrap(); + println!("metrics: {:?}", &metrics); + assert!(metrics["known-answer-suppression"] > 0); +} + +#[test] +fn test_domain_suffix_in_browse() { + let mdns_client = ServiceDaemon::new().expect("failed to create mDNS client"); + assert!(mdns_client.browse("_service-name._tcp.local").is_err()); + assert!(mdns_client.browse("_service-name._tcp.local.").is_ok()); + mdns_client.shutdown().unwrap(); +} + +#[test] +fn test_name_conflict_resolution() { + // This test registers two services using the same names, but different IP addresses. + let ty_domain = "_conflict-test._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "conflict_host.local."; + let port = 5200; + + // Register the first service. + let server1 = ServiceDaemon::new().expect("failed to start server1"); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server1 + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // Register the second service. + let server2 = ServiceDaemon::new().expect("failed to start server2"); + + // Modify the IPv4 address for the service. + let IpAddr::V4(ipv4) = ip_addr1 else { + panic!(); + }; + let bytes = ipv4.octets(); + let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1)); + + let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None) + .expect("failed to create ServiceInfo for service2"); + server2 + .register(service2) + .expect("failed to register service2"); + + // Verify name change event for the second service, due to the name conflict. + let server2_monitor = server2.monitor().unwrap(); + let timeout = Duration::from_secs(2); + let mut name_changed = false; + while let Ok(event) = server2_monitor.recv_timeout(timeout) { + match event { + DaemonEvent::NameChange(change) => { + println!("server2 daemon event: {:?}", change); + name_changed = true; + break; + } + other => println!("server2 other event: {:?}", other), + } + } + assert!(name_changed); + + // Verify both services are resolved. + let client = ServiceDaemon::new().expect("failed to create mdns client"); + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(3); + let mut service_names = HashSet::new(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + + service_names.insert(info.get_fullname().to_string()); + + // Find and verify name conflict resolution. + if info.get_fullname().contains("(2)") { + assert_eq!(info.get_hostname(), "conflict_host-2.local."); + } + + // Stop the wait if both are resolved. + if service_names.len() == 2 { + break; + } + } + } + + // Verify that we have resolve two services instead of one. + assert_eq!(service_names.len(), 2); +} + +#[test] +fn test_name_tiebreaking() { + // This test registers two services using the same names, but different IP addresses, + // same as `test_name_conflict_resolution`, the only difference being that two servers + // do the probing at the same time. Hence tiebreaking. Server2 should win. + + let ty_domain = "_tiebreaking._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "tiebreaking_host.local."; + let port = 5200; + + // Register the first service. + let server1 = ServiceDaemon::new().expect("failed to start server1"); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server1 + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // Register the second service immediately to trigger tiebreaking. + let server2 = ServiceDaemon::new().expect("failed to start server2"); + + // Modify the IPv4 address for the service. + let IpAddr::V4(ipv4_2) = ip_addr1 else { + panic!(); + }; + let bytes = ipv4_2.octets(); + let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1)); + + let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None) + .expect("failed to create ServiceInfo for service2"); + server2 + .register(service2) + .expect("failed to register service2"); + + // Verify name change event for the first service, per tiebreaking rules. + // Timeout is set to 3 seconds for: + // - Initial probing (750ms) + // - 1 second wait after LOST the tiebreaking + // - New probing (750ms) + // Total: 2.5s + some margin => 3s + let server1_monitor = server1.monitor().unwrap(); + let timeout = Duration::from_secs(3); + let mut name_changed = false; + + while let Ok(event) = server1_monitor.recv_timeout(timeout) { + match event { + DaemonEvent::NameChange(change) => { + println!("server1 daemon event: {:?}", change); + name_changed = true; + break; + } + other => println!("server1 other event: {:?}", other), + } + } + assert!(name_changed); + + // Verify both services are resolved. + let client = ServiceDaemon::new().expect("failed to create mdns client"); + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(3); + let mut resolved_services = vec![]; + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + + resolved_services.push(info); + if resolved_services.len() == 2 { + break; + } + } + } + + // Verify that we have resolve two services instead of one. + assert_eq!(resolved_services.len(), 2); + + // Verify that server2 (its ip_addr2) won the tiebreaking for the hostname. + for resolved_service in resolved_services { + if resolved_service.get_hostname() == host_name { + let service_addr = resolved_service.get_addresses().iter().next().unwrap(); + assert_eq!(&service_addr.to_ip_addr(), &ip_addr2); + println!("server2 won the tiebreaking"); + } + } +} + +#[test] +fn test_name_conflict_3() { + // Similar to `test_name_conflict_resolution` but with 3 servers. + let ty_domain = "_conflict-3._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "conflict3_host.local."; + let port = 5200; + + // Register the first service. + let server1 = ServiceDaemon::new().expect("failed to start server1"); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server1 + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // Register the second service. + let server2 = ServiceDaemon::new().expect("failed to start server2"); + + // Modify the IPv4 address for the service. + let IpAddr::V4(ipv4) = ip_addr1 else { + panic!(); + }; + let bytes = ipv4.octets(); + let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1)); + + let info2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None) + .expect("failed to create ServiceInfo for service2"); + server2 + .register(info2) + .expect("failed to register service2"); + + // Verify name change event for the second service, due to the name conflict. + let server2_monitor = server2.monitor().unwrap(); + let timeout = Duration::from_secs(2); + let mut name_changed = false; + while let Ok(event) = server2_monitor.recv_timeout(timeout) { + match event { + DaemonEvent::NameChange(change) => { + println!("server2 daemon event: {:?}", change); + name_changed = true; + } + other => println!("server2 other event: {:?}", other), + } + } + assert!(name_changed); + + // Register the third service + let server3 = ServiceDaemon::new().expect("failed to start server2"); + + // Modify the IPv4 address for the service. + let ip_addr3 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 2)); + + let info3 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr3, port, None) + .expect("failed to create ServiceInfo for service2"); + + server3 + .register(info3) + .expect("failed to register service2"); + + let server3_monitor = server3.monitor().unwrap(); + let timeout = Duration::from_secs(3); + name_changed = false; + while let Ok(event) = server3_monitor.recv_timeout(timeout) { + match event { + DaemonEvent::NameChange(change) => { + println!("server3 daemon event: {:?}", change); + name_changed = true; + break; + } + other => println!("server3 other event: {:?}", other), + } + } + assert!(name_changed); + + // Verify all services are resolved. + let client = ServiceDaemon::new().expect("failed to create mdns client"); + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(3); + let mut service_names = HashSet::new(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + + service_names.insert(info.get_fullname().to_string()); + if service_names.len() >= 3 { + break; + } + } + } + + // Verify that we have resolve two services instead of one. + assert_eq!(service_names.len(), 3); +} + +#[test] +fn test_verify_srv() { + // start a server + let ty_domain = "_verify-srv._udp.local."; + let host_name = "verify_srv.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let port = 5200; + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Register the service. + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + let fullname = service1.get_fullname().to_string(); + + let server1 = ServiceDaemon::new().expect("failed to start server"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // start a client + let client = ServiceDaemon::new().expect("failed to start client"); + let receiver = client.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(2); + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!("service resolved: {:?}", info); + break; + } + } + + // kill the server without unregister (i.e. not-graceful-shutdown) + server1.shutdown().unwrap(); + sleep(Duration::from_secs(1)); + + // check `ServiceRemoved` + client.verify(fullname, Duration::from_secs(3)).unwrap(); + let timeout = Duration::from_secs(4); + let mut service_removal = false; + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceRemoved(service_type, fullname) = event { + service_removal = true; + println!("service removed: {service_type} : {fullname}"); + break; + } + } + + assert!(service_removal); +} + +#[test] +fn test_multicast_loop_v4() { + let ty_domain = "_loop_v4._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "loop_v4_host.local."; + let port = 5200; + + // Register the first service. + let server = ServiceDaemon::new().expect("failed to start server"); + server.set_multicast_loop_v4(false).unwrap(); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // start a client i.e. querier. + let mut resolved = false; + let client = ServiceDaemon::new().expect("failed to create mdns client"); + + // For Windows, IP_MULTICAST_LOOP option works only on the receive path. + client.set_multicast_loop_v4(false).unwrap(); + + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(2); + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + resolved = true; + break; + } + } + + assert!(!resolved); + + // enable loopback and try again. + server.set_multicast_loop_v4(true).unwrap(); + client.set_multicast_loop_v4(true).unwrap(); + let receiver = client.browse(ty_domain).unwrap(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + resolved = true; + break; + } + } + + assert!(resolved); +} + +#[test] +fn test_multicast_loop_v6() { + let ty_domain = "_loop_v6._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "loop_v6_host.local."; + let port = 5200; + + // Register the first service. + let server = ServiceDaemon::new().expect("failed to start server"); + server.set_multicast_loop_v6(false).unwrap(); + + // Get a single IPv6 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv6()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // start a client i.e. querier. + let mut resolved = false; + let client = ServiceDaemon::new().expect("failed to create mdns client"); + + // For Windows, IP_MULTICAST_LOOP option works only on the receive path. + client.set_multicast_loop_v6(false).unwrap(); + + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(2); + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses() + ); + resolved = true; + break; + } + } + + assert!(!resolved); + + // enable loopback and try again. + server.set_multicast_loop_v6(true).unwrap(); + client.set_multicast_loop_v6(true).unwrap(); + + let receiver = client.browse(ty_domain).unwrap(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses() + ); + resolved = true; + break; + } + } + + assert!(resolved); +} + +#[test] +fn test_set_ip_check_interval() { + // Create a daemon + let server = ServiceDaemon::new().expect("Failed to create server"); + let service = "_ip_check._udp.local."; + let host_name = "test_ip_check_host.local."; + + // use a single IPv4 addr + let service_ip_addr = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let port = 5201; + let my_service = ServiceInfo::new( + service, + "my_instance", + host_name, + service_ip_addr, + port, + None, + ) + .expect("invalid service info"); + let result = server.register(my_service.clone()); + assert!(result.is_ok()); + + // Set the IP check interval. + server.set_ip_check_interval(3).unwrap(); + let interval = server.get_ip_check_interval().unwrap(); + assert_eq!(interval, 3); + + server.set_ip_check_interval(u32::MAX).unwrap(); + let interval = server.get_ip_check_interval().unwrap(); + assert_eq!(interval, u32::MAX); + + server.set_ip_check_interval(0).unwrap(); + let interval = server.get_ip_check_interval().unwrap(); + assert_eq!(interval, 0); + + server.shutdown().unwrap(); +} + +#[test] +fn test_rfc6763_escaping() { + // Test RFC 6763 Section 4.3: dots and backslashes must be escaped + let instance_name = "My.Path\\Service"; + let service_type = "_rfc6763._tcp.local."; + + let service = ServiceInfo::new( + service_type, + instance_name, + "rfc6763.local.", + "", + 5555, + None, + ) + .expect("Failed to create service"); + + // Verify escaping: dots become \. and backslashes become \\ + let fullname = service.get_fullname(); + println!("Escaping test: {}", fullname); + assert!( + fullname.contains("My\\.Path\\\\Service"), + "Dots and backslashes should be escaped" + ); +} + +#[test] +fn test_rfc6763_utf8_support() { + // Test RFC 6763 Section 4.1.1: UTF-8 support including emojis + // Also verify UTF-8 works with escaping (dots and backslashes) + + let service1 = ServiceInfo::new( + "_utf8._tcp.local.", + "mdns.lib 🌐", + "test.local.", + "", + 80, + None, + ) + .expect("Failed to create service with emojis"); + assert!(service1.get_fullname().contains("mdns\\.lib 🌐")); + + // UTF-8 + escaping: "Café €.v1\2024" + let service2 = ServiceInfo::new( + "_utf8escape._tcp.local.", + "Café €.v1\\2024", + "test.local.", + "", + 80, + None, + ) + .expect("Failed to create service with UTF-8 and escapes"); + + let fullname = service2.get_fullname(); + println!("UTF-8 + escaping fullname: {}", fullname); + assert!( + fullname.contains("Café €\\.v1\\\\2024"), + "UTF-8 preserved, dots and backslashes escaped" + ); +} + +#[test] +fn test_rfc6763_utf8_network_integration() { + // Test that UTF-8 with dots and backslashes works end-to-end over the network + // This service will be registered and browsed to verify full functionality + + // Create a daemon + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service with UTF-8, dots, and backslashes + let ty_domain = "_utf8-network._tcp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_base = now.as_micros().to_string(); + + // Instance name with UTF-8 characters (emoji, accents), dots, and backslashes + let instance_name = format!("Café.Service\\{} 🌐", instance_base); + + let host_name = "utf8_network_host.local."; + let port = 5203; + + let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, "", port, None) + .expect("valid service info") + .enable_addr_auto(); + + let fullname = my_service.get_fullname().to_string(); + println!("Registered service fullname: {}", &fullname); + + // Verify the fullname has proper escaping + assert!(fullname.contains("Café\\.Service\\\\")); + assert!(fullname.contains('🌐')); + + d.register(my_service) + .expect("Failed to register our service"); + + // Browse for the service + let browse_chan = d.browse(ty_domain).unwrap(); + let timeout = Duration::from_secs(3); + let mut resolved = false; + + while let Ok(event) = browse_chan.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + let resolved_fullname = info.get_fullname(); + println!("Resolved service: {}", resolved_fullname); + + // Compare by checking if the resolved fullname contains our instance base + // and UTF-8 characters (network returns decoded names without escaping) + if resolved_fullname.contains(&instance_base) + && resolved_fullname.contains("Café") + && resolved_fullname.contains('🌐') + && resolved_fullname.contains(ty_domain) + { + resolved = true; + println!("Successfully found UTF-8 service over network!"); + + // Verify the service details + assert_eq!(info.get_port(), port); + assert!(!info.get_addresses().is_empty()); + + // Verify UTF-8 characters are preserved + assert!(resolved_fullname.contains("Café.Service")); + assert!(resolved_fullname.contains('🌐')); + break; + } + } + ServiceEvent::SearchStarted(_) => { + println!("Search started for {}", ty_domain); + } + ServiceEvent::ServiceFound(_, found_fullname) => { + println!("Service found: {}", found_fullname); + } + _ => {} + } + } + + assert!( + resolved, + "UTF-8 service with dots and backslashes should be resolved over the network" + ); + + d.shutdown().unwrap(); +} + +#[test] +fn test_interface_id_get_addrs() { + // Get actual interfaces from the OS to build a valid InterfaceId. + let if_addrs = if_addrs::get_if_addrs().expect("failed to get interfaces"); + let first = if_addrs.first().expect("no interfaces found"); + let intf_id = InterfaceId::from(first); + + let addrs = intf_id.get_addrs(); + assert!( + !addrs.is_empty(), + "interface {} should have at least one address", + intf_id.name + ); + assert!( + addrs.contains(&first.ip()), + "get_addrs() should contain the address we constructed from: {}", + first.ip() + ); +} + +/// A helper function to include a timestamp for println. +fn timed_println(msg: String) { + let now = SystemTime::now(); + let formatted_time = humantime::format_rfc3339(now); + println!("[{}] {}", formatted_time, msg); +} + +#[test] +fn test_goodbye_uses_conflict_resolved_name() { + // When probing renames a service due to a conflict, unregistering it must + // send the goodbye records under the renamed (cached-by-peers) name, so + // that browsers drop the entry immediately instead of waiting out TTLs. + let ty_domain = "_conflict-bye._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "conflict_bye_host.local."; + let port = 5200; + + // Register the first service. + let server1 = ServiceDaemon::new().expect("failed to start server1"); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None) + .expect("valid service info"); + server1 + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // Register the second service with the same names to force a conflict. + let server2 = ServiceDaemon::new().expect("failed to start server2"); + let server2_monitor = server2.monitor().unwrap(); + + let IpAddr::V4(ipv4) = ip_addr1 else { + panic!(); + }; + let bytes = ipv4.octets(); + let ip_addr2 = IpAddr::V4(Ipv4Addr::new( + bytes[0], + bytes[1], + bytes[2], + bytes[3] % 254 + 1, + )); + + let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None) + .expect("failed to create ServiceInfo for service2"); + let service2_fullname = service2.get_fullname().to_string(); + server2 + .register(service2) + .expect("failed to register service2"); + + // Wait for the conflict to be resolved by renaming service2. + let timeout = Duration::from_secs(2); + let mut renamed_instance = None; + while let Ok(event) = server2_monitor.recv_timeout(timeout) { + match event { + DaemonEvent::NameChange(change) => { + println!("server2 name change: {:?}", change); + if change.rr_type == RRType::SRV { + renamed_instance = Some(change.new_name); + break; + } + } + other => println!("server2 other event: {:?}", other), + } + } + let renamed_instance = renamed_instance.expect("service2 was not renamed"); + + // Browse until the renamed instance is resolved. + let client = ServiceDaemon::new().expect("failed to create mdns client"); + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(3); + let mut resolved = false; + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceResolved(info) = event { + println!("Resolved a service: {}", info.get_fullname()); + if info.get_fullname() == renamed_instance { + resolved = true; + break; + } + } + } + assert!(resolved, "the renamed instance was not resolved"); + + // Unregister service2 (by its original fullname) and verify the client + // sees the renamed instance removed via the goodbye packets, well before + // any record TTL could expire. + let receiver2 = server2 + .unregister(&service2_fullname) + .expect("failed to unregister service2"); + let status = receiver2.recv_timeout(Duration::from_secs(2)).unwrap(); + println!("unregister status: {:?}", status); + + let timeout = Duration::from_secs(5); + let mut removed = false; + while let Ok(event) = receiver.recv_timeout(timeout) { + if let ServiceEvent::ServiceRemoved(_ty, fullname) = event { + println!("Removed a service: {fullname}"); + if fullname == renamed_instance { + removed = true; + break; + } + } + } + assert!(removed, "no goodbye seen for the renamed instance"); + + server1.shutdown().unwrap(); + server2.shutdown().unwrap(); + client.shutdown().unwrap(); +} diff --git a/vendor/mdns-sd/tests/shutdown_test.rs b/vendor/mdns-sd/tests/shutdown_test.rs new file mode 100644 index 0000000..eb882d0 --- /dev/null +++ b/vendor/mdns-sd/tests/shutdown_test.rs @@ -0,0 +1,381 @@ +use std::{ + collections::HashSet, + thread::sleep, + time::{Duration, SystemTime}, +}; + +use mdns_sd::{DaemonStatus, HostnameResolutionEvent, ServiceDaemon, ServiceEvent, ServiceInfo}; +use test_log::test; + +/// Test that shutdown properly unregisters all services +#[test] +fn test_shutdown_unregisters_services() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Register a service + let ty_domain = "_shutdown-test1._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = format!("shutdown-test-{}", now.as_micros()); + + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + "shutdown-host.local.", + "", + 5300, + None, + ) + .expect("valid service info") + .enable_addr_auto(); + + let fullname = my_service.get_fullname().to_string(); + d.register(my_service).expect("Failed to register service"); + + // Give it time to announce + sleep(Duration::from_millis(500)); + + // Browse for the service in another daemon to verify it's announced + let d2 = ServiceDaemon::new().expect("Failed to create daemon"); + let browse_chan = d2.browse(ty_domain).unwrap(); + + let mut found = false; + let timeout = Duration::from_secs(2); + let timer = std::time::Instant::now() + timeout; + while std::time::Instant::now() < timer { + if let Ok(ServiceEvent::ServiceResolved(info)) = + browse_chan.recv_timeout(Duration::from_millis(100)) + { + if info.get_fullname() == fullname { + found = true; + break; + } + } + } + assert!(found, "Service should be discovered before shutdown"); + + // Now shutdown the first daemon + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Give time for goodbye packets to be sent + sleep(Duration::from_millis(500)); + + // Verify the service is removed + let mut removed = false; + let timer = std::time::Instant::now() + Duration::from_secs(2); + while std::time::Instant::now() < timer { + if let Ok(ServiceEvent::ServiceRemoved(_, removed_fullname)) = + browse_chan.recv_timeout(Duration::from_millis(100)) + { + if removed_fullname == fullname { + removed = true; + break; + } + } + } + + assert!(removed, "Service should be removed after shutdown"); + + d2.shutdown().unwrap(); +} + +/// Test that shutdown properly stops all browse operations +#[test] +fn test_shutdown_stops_browse() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Start browsing + let ty_domain = "_shutdown-browse-test._udp.local."; + let browse_chan = d.browse(ty_domain).unwrap(); + + // Give it time to start + sleep(Duration::from_millis(100)); + + // Shutdown + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Verify we receive SearchStopped event + let mut search_stopped = false; + let timeout = Duration::from_secs(2); + let timer = std::time::Instant::now() + timeout; + + while std::time::Instant::now() < timer { + match browse_chan.recv_timeout(Duration::from_millis(100)) { + Ok(ServiceEvent::SearchStopped(stopped_ty)) => { + if stopped_ty == ty_domain { + search_stopped = true; + break; + } + } + Ok(_) => continue, + Err(_) => break, + } + } + + assert!( + search_stopped, + "Browse should be stopped with SearchStopped event" + ); +} + +/// Test that shutdown properly stops all hostname resolution +#[test] +fn test_shutdown_stops_hostname_resolution() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Start hostname resolution + let hostname = "test-shutdown-host.local."; + let resolve_chan = d.resolve_hostname(hostname, None).unwrap(); + + // Give it time to start + sleep(Duration::from_millis(100)); + + // Shutdown + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Verify we receive SearchStopped event + let mut search_stopped = false; + let timeout = Duration::from_secs(2); + let timer = std::time::Instant::now() + timeout; + + while std::time::Instant::now() < timer { + match resolve_chan.recv_timeout(Duration::from_millis(100)) { + Ok(HostnameResolutionEvent::SearchStopped(stopped_hostname)) => { + if stopped_hostname.to_lowercase() == hostname.to_lowercase() { + search_stopped = true; + break; + } + } + Ok(_) => continue, + Err(_) => break, + } + } + + assert!( + search_stopped, + "Hostname resolution should be stopped with SearchStopped event" + ); +} + +/// Test that shutdown sends proper notifications to monitors +#[test] +fn test_shutdown_notifies_monitors() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Monitor daemon events + let _monitor_chan = d.monitor().unwrap(); + + // Register a service + let ty_domain = "_shutdown-monitor-test._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = format!("monitor-test-{}", now.as_micros()); + + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + "monitor-host.local.", + "", + 5301, + None, + ) + .expect("valid service info") + .enable_addr_auto(); + + d.register(my_service).expect("Failed to register service"); + + // Give it time to register + sleep(Duration::from_millis(300)); + + // Shutdown + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Give time for events to be processed + sleep(Duration::from_millis(300)); + + // The monitor channel should eventually be closed or receive notification + // For now we just verify that shutdown completes successfully + // Future enhancement: add specific DaemonEvent for shutdown +} + +/// Test that shutdown handles multiple registered services +#[test] +fn test_shutdown_multiple_services() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + let ty_domain = "_shutdown-multi-test._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros(); + + // Register multiple services + let mut fullnames = Vec::new(); + for i in 0..3 { + let instance_name = format!("multi-test-{}-{}", now, i); + let my_service = ServiceInfo::new( + ty_domain, + &instance_name, + &format!("multi-host-{}.local.", i), + "", + 5302 + i, + None, + ) + .expect("valid service info") + .enable_addr_auto(); + + fullnames.push(my_service.get_fullname().to_string()); + d.register(my_service).expect("Failed to register service"); + } + + // Give time to announce + sleep(Duration::from_millis(500)); + + // Browse for services in another daemon + let d2 = ServiceDaemon::new().expect("Failed to create daemon"); + let browse_chan = d2.browse(ty_domain).unwrap(); + + // Verify services are discovered + let mut found_services = HashSet::new(); + let timeout = Duration::from_secs(3); + let timer = std::time::Instant::now() + timeout; + while std::time::Instant::now() < timer && found_services.len() < fullnames.len() { + if let Ok(ServiceEvent::ServiceResolved(info)) = + browse_chan.recv_timeout(Duration::from_millis(100)) + { + let fullname = info.get_fullname().to_string(); + if fullnames.contains(&fullname) { + found_services.insert(fullname); + } + } + } + + println!("Found {} services before shutdown", found_services.len()); + + // Shutdown the first daemon + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Give time for goodbye packets + sleep(Duration::from_millis(500)); + + // Verify all services are removed + let mut removed_services = HashSet::new(); + let timer = std::time::Instant::now() + Duration::from_secs(3); + while std::time::Instant::now() < timer && removed_services.len() < found_services.len() { + if let Ok(ServiceEvent::ServiceRemoved(_, removed_fullname)) = + browse_chan.recv_timeout(Duration::from_millis(100)) + { + if fullnames.contains(&removed_fullname) { + removed_services.insert(removed_fullname); + } + } + } + + println!("Removed {} services after shutdown", removed_services.len()); + assert_eq!( + removed_services.len(), + found_services.len(), + "All discovered services should be removed after shutdown" + ); + + d2.shutdown().unwrap(); +} + +/// Test that operations fail gracefully after shutdown +#[test] +fn test_operations_fail_after_shutdown() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // Shutdown + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Try various operations - they should all fail + let ty_domain = "_post-shutdown-test._udp.local."; + + // Try to register + let my_service = ServiceInfo::new(ty_domain, "test", "test.local.", "", 5303, None).unwrap(); + let result = d.register(my_service); + assert!(result.is_err(), "Register should fail after shutdown"); + + // Try to browse + let result = d.browse(ty_domain); + assert!(result.is_err(), "Browse should fail after shutdown"); + + // Try to resolve hostname + let result = d.resolve_hostname("test.local.", None); + assert!( + result.is_err(), + "Resolve hostname should fail after shutdown" + ); + + // Status should return Shutdown + let status_receiver = d.status().unwrap(); + let status = status_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); +} + +/// Test that shutdown is idempotent (can be called multiple times) +#[test] +fn test_shutdown_idempotent() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + + // First shutdown + let shutdown_receiver1 = d.shutdown().unwrap(); + let status1 = shutdown_receiver1.recv().unwrap(); + assert!(matches!(status1, DaemonStatus::Shutdown)); + + // Second shutdown should also work (or fail gracefully) + let result = d.shutdown(); + // Either succeeds or returns an error (both acceptable) + if let Ok(shutdown_receiver2) = result { + // If it succeeds, status should still be Shutdown + let status2 = shutdown_receiver2.recv().unwrap(); + assert!(matches!(status2, DaemonStatus::Shutdown)); + } +} + +/// Test shutdown with concurrent operations +#[test] +fn test_shutdown_concurrent_operations() { + let d = ServiceDaemon::new().expect("Failed to create daemon"); + let d_clone = d.clone(); + + // Start a browse operation in another thread + let handle = std::thread::spawn(move || { + let browse_chan = d_clone.browse("_concurrent-test._udp.local.").unwrap(); + + // Keep receiving until channel closes or SearchStopped is received + loop { + match browse_chan.recv_timeout(Duration::from_secs(5)) { + Ok(ServiceEvent::SearchStopped(_)) => break, + Ok(_) => continue, + Err(_) => break, + } + } + }); + + // Give the browse time to start + sleep(Duration::from_millis(100)); + + // Shutdown while browse is active + let shutdown_receiver = d.shutdown().unwrap(); + let status = shutdown_receiver.recv().unwrap(); + assert!(matches!(status, DaemonStatus::Shutdown)); + + // Wait for the browse thread to complete + handle.join().expect("Browse thread should complete"); +}