fix(mdns): retain origin and bound daemon state

Vendor the pinned mdns-sd 0.21.1 source so response records retain their observed source IP. Bound unauthenticated cache records to 1024 globally and 128 per source, deduplicate and cap timers at 4096, and poll at least once per second for expiry cleanup.

Expose packet provenance through lanspread-mdns, drop originless resolutions, and add a dedicated vendor test recipe while keeping third-party sources outside workspace formatting and Clippy.

Test Plan:
- just mdns-vendor-test (106 tests passed with socket access)
- just test
- just fmt
- just clippy
- cache/per-source, exact-refresh, timer-cap, and serde tests
- git diff --check
This commit is contained in:
2026-09-12 13:08:26 +02:00
parent 49f8eef7b5
commit 4d5881d6b0
23 changed files with 18879 additions and 41 deletions
Generated
+1 -27
View File
@@ -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"
+4 -11
View File
@@ -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,
+9
View File
@@ -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<String, String>,
@@ -260,6 +263,11 @@ impl MdnsBrowser {
) -> Option<MdnsService> {
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,
+8 -3
View File
@@ -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 -- \
+1
View File
@@ -0,0 +1 @@
/target/
+949
View File
@@ -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<dyn AsIpAddrs> (#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<dyn DnsRecordExt> 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<Receiver<DaemonStatus>>` (#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<Option<&[u8]>>`
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<HashMap>`.
- `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
+116
View File
@@ -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 <keepsimple@gmail.com>"]
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"
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "mdns-sd"
version = "0.21.1"
authors = ["keepsimple <keepsimple@gmail.com>"]
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"
+18
View File
@@ -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.
+201
View File
@@ -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.
+21
View File
@@ -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.
+81
View File
@@ -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.
+100
View File
@@ -0,0 +1,100 @@
//! A mDNS query client.
//!
//! Run with:
//!
//! cargo run --example query <service_type_without_domain>
//!
//! 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 <service_type_without_domain_postfix> [--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");
}
+163
View File
@@ -0,0 +1,163 @@
//! Registers a mDNS service.
//!
//! Run with:
//!
//! cargo run --example register <service_type> <instance_name> <hostname> [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<String> = 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 <service_type> <instance_name> <hostname> [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-<timestamp>.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");
}
+1219
View File
@@ -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<String, HashSet<String>>,
/// Set of instance names that lost records but still have PTR entries.
pub(crate) modified_instances: HashSet<String>,
}
/// 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<String, Vec<DnsRecordIntf>>,
/// DnsSrv records indexed by the fullname of an instance
srv: HashMap<String, Vec<DnsRecordIntf>>,
/// DnsTxt records indexed by the fullname of an instance
txt: HashMap<String, Vec<DnsRecordIntf>>,
/// DnsAddr records indexed by the hostname in lowercase.
addr: HashMap<String, Vec<DnsRecordIntf>>,
/// A reverse lookup table from "instance fullname" to "subtype PTR name"
subtype: HashMap<String, String>,
/// Negative responses:
/// A map from "instance fullname" to DnsNSec.
nsec: HashMap<String, Vec<DnsRecordIntf>>,
}
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<String, Vec<DnsRecordIntf>> {
&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<DnsRecordIntf>> {
self.ptr.get(ty_domain)
}
pub(crate) fn get_srv(&self, fullname: &str) -> Option<&Vec<DnsRecordIntf>> {
self.srv.get(fullname)
}
pub(crate) fn get_txt(&self, fullname: &str) -> Option<&Vec<DnsRecordIntf>> {
self.txt.get(fullname)
}
pub(crate) fn get_addr(&self, hostname: &str) -> Option<&Vec<DnsRecordIntf>> {
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<String> {
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::<DnsSrv>() {
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<String, HashSet<ScopedIp>> {
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::<DnsAddress>() {
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<u64>,
) -> 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::<DnsSrv>() 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<u64>,
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::<DnsPointer>() {
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::<DnsPointer>())
.map(|ptr| ptr.alias().to_string())
.collect::<HashSet<_>>();
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<String, HashSet<ScopedIp>> {
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::<DnsAddress>() {
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<String, HashSet<String>> {
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::<DnsPointer>() {
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::<DnsPointer>() {
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::<DnsPointer>() {
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::<DnsSrv>() {
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::<DnsSrv>() {
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<u64> {
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<String, Vec<RRType>>, HashSet<u64>) {
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::<DnsPointer>()
.map(|ptr| ptr.alias())
})
.collect();
let mut refresh_due: HashMap<String, Vec<RRType>> = HashMap::new();
let mut new_timers = HashSet::new();
for instance in instances {
// Check SRV records.
let refresh_timers: HashSet<u64> = 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<u64> = 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<String>, HashSet<u64>) {
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::<DnsPointer>()
.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<String> = self
.srv
.get(instance)
.into_iter()
.flatten()
.filter_map(|record| {
record
.record
.any()
.downcast_ref::<DnsSrv>()
.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<u64> = 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::<DnsAddress>()
.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::<DnsAddress>() 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<String> = 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::<DnsPointer>() {
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::<DnsPointer>() {
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::<DnsSrv>() {
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<u64>,
) {
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::<DnsAddress>() {
if let Some(addr_b) = incoming.any().downcast_ref::<DnsAddress>() {
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<IpAddr> = 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);
}
}
+3654
View File
@@ -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<IpAddr> {
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<InterfaceId>,
}
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<IpAddr> 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<Self> {
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<dyn DnsRecordExt>;
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<String>,
}
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<u64> {
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::<Self>() {
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::<Self>() {
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>() {
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::<Self>() {
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::<Self>() {
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>() {
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::<Self>() {
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::<Self>() {
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::<Self>() 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<u8>,
}
impl DnsTxt {
pub fn new(name: &str, class: u16, ttl: u32, text: Vec<u8>) -> 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::<Self>() {
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::<Self>() {
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>() {
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<TxtProperty> {
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<Vec<u8>>,
}
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<K, V> 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<K, V> 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::<Self>() {
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::<Self>() {
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::<Self>() {
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<u8>,
}
impl DnsNSec {
pub fn new(
name: &str,
class: u16,
ttl: u32,
next_domain: String,
type_bitmap: Vec<u8>,
) -> 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<u16> {
// 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::<Self>() {
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::<Self>() {
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::<Self>() {
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<u8>,
/// k: name, v: offset
names: HashMap<String, u16>,
/// 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<String> {
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<DnsOutPacket>,
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<F>(&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<DnsOutPacket> {
// 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<DnsQuestion>,
answers: Vec<(DnsRecordBox, u64)>,
authorities: Vec<DnsRecordBox>,
additionals: Vec<DnsRecordBox>,
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<F>(&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<F>(&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<Vec<u8>> {
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<DnsOutPacket> {
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<u8>,
questions: Vec<DnsQuestion>,
answers: Vec<DnsRecordBox>,
authorities: Vec<DnsRecordBox>,
additional: Vec<DnsRecordBox>,
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<u8>, interface_id: InterfaceId) -> Result<Self> {
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<DnsRecordBox> {
&mut self.answers
}
pub fn authorities_mut(&mut self) -> &mut Vec<DnsRecordBox> {
&mut self.authorities
}
pub fn additionals_mut(&mut self) -> &mut Vec<DnsRecordBox> {
&mut self.additional
}
pub fn all_records(self) -> impl Iterator<Item = DnsRecordBox> {
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<Vec<DnsRecordBox>> {
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<Option<DnsRecordBox>> {
let rec: Option<DnsRecordBox> = 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<String> {
let length = self.data[self.offset];
self.offset += 1;
self.read_string(length as usize)
}
fn read_u16(&mut self) -> Result<u16> {
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<Vec<u8>> {
// 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<Vec<u8>> {
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<Ipv4Addr> {
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<Ipv6Addr> {
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<String> {
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<String> {
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<usize> {
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<u8> = 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<u8> = 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<u8>, 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<u8> = 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<u8> = 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<u8> = 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<u8> = 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);
}
}
+51
View File
@@ -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<T> = core::result::Result<T, Error>;
/// A simple macro to report all kinds of errors.
macro_rules! e_fmt {
($($arg:tt)+) => {
Error::Msg(format!($($arg)+))
};
}
pub(crate) use e_fmt;
+219
View File
@@ -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 <channel> 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
}
+6681
View File
@@ -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 "<Instance>.<Service>.<Domain>"
// 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 "<Service>.<Domain>",
// 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. <service>.<domain>.
// Every <service> 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. <instance>.<service>.<domain>
// 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<T> = core::result::Result<T, InternalError>;
/// 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<Self> {
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<String, i64>;
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<Command>,
/// 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> {
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<Self> {
// 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<Receiver<ServiceEvent>> {
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<Receiver<ServiceEvent>> {
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<u64>,
) -> Result<Receiver<HostnameResolutionEvent>> {
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<Receiver<UnregisterStatus>> {
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<Receiver<DaemonEvent>> {
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<Receiver<DaemonStatus>> {
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<Receiver<DaemonStatus>> {
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<Receiver<Metrics>> {
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<u32> {
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: <https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2>
///
/// "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: <https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2>
///
/// "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<Command>,
port: u16,
cmd_sender: Sender<Command>,
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<MyUdpSocket> {
// 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<PktInfoUdpSocket> {
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<IpAddr>` 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<IpAddr> 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<IfKind>,
}
/// A trait that converts a type into a Vec of `IfKind`.
pub trait IntoIfKindVec {
fn into_vec(self) -> IfKindVec;
}
impl<T: Into<IfKind>> IntoIfKindVec for T {
fn into_vec(self) -> IfKindVec {
let if_kind: IfKind = self.into();
IfKindVec {
kinds: vec![if_kind],
}
}
}
impl<T: Into<IfKind>> IntoIfKindVec for Vec<T> {
fn into_vec(self) -> IfKindVec {
let kinds: Vec<IfKind> = 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<dyn Fn(&Interface) -> 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<u32, MyIntf>,
/// A common socket for IPv4 interfaces. It's None if IPv4 is disabled in OS kernel.
ipv4_sock: Option<MyUdpSocket>,
/// A common socket for IPv6 interfaces. It's None if IPv6 is disabled in OS kernel.
ipv6_sock: Option<MyUdpSocket>,
/// Local registered services keyed by service full names.
my_services: HashMap<String, ServiceInfo>,
/// Received DNS records.
cache: DnsCache,
/// Registered service records, keyed by interface index.
dns_registry_map: HashMap<u32, DnsRegistry>,
/// Active "Browse" commands.
service_queriers: HashMap<String, Sender<ServiceEvent>>, // <ty_domain, channel::sender>
/// 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<String, (Sender<HostnameResolutionEvent>, Option<u64>)>, // <hostname, (channel::sender, UNIX timestamp in millis)>
/// All repeating transmissions.
retransmissions: Vec<ReRun>,
/// Query responses deferred per RFC 6762 §6.
delayed_responses: Vec<DelayedResponse>,
counters: Metrics,
/// Waits for incoming packets.
poller: Poll,
/// Channels to notify events.
monitors: Vec<Sender<DaemonEvent>>,
/// 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<MaxPacketSizeSelection>,
/// All interface selections called to the daemon.
if_selections: Vec<IfSelection>,
/// 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<Reverse<u64>>,
status: DaemonStatus,
/// Service instances that are pending for resolving SRV and TXT.
pending_resolves: HashSet<String>,
/// Service instances that are already resolved.
resolved: HashSet<String>,
multicast_loop_v4: bool,
multicast_loop_v6: bool,
accept_unsolicited: bool,
include_apple_p2p: bool,
cmd_sender: Sender<Command>,
signal_addr: SocketAddr,
#[cfg(test)]
test_down_interfaces: HashSet<String>,
}
/// 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<Command>,
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<String> = 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<String> = 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<String> = 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<Command>) -> Option<Command> {
// 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<String> = 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<IfKind>) {
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<IfKind>) {
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<IfKind>, 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<u64> {
self.timers.peek().map(|Reverse(v)| *v)
}
fn _pop_earliest_timer(&mut self) -> Option<u64> {
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<Interface>) -> HashSet<Interface> {
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<Interface>) {
// 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<u32, Vec<&IfAddr>> =
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<IpAddr> {
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<u8> {
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<HostnameResolutionEvent>,
timeout: Option<u64>,
) {
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::<DnsSrv>() {
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<ServiceEvent>,
now: u64,
) {
let mut resolved: HashSet<String> = HashSet::new();
let mut unresolved: HashSet<String> = 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::<DnsPointer>() {
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<HostnameResolutionEvent>,
) {
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<ResolvedService> {
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::<DnsPointer>()
.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::<DnsSrv>() {
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::<DnsTxt>() {
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::<DnsAddress>() {
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::<DnsPointer>() {
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::<DnsPointer>()
{
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::<DnsAddress>() {
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<String>) {
if updated_instances.is_empty() {
return;
}
let mut resolved: HashSet<String> = HashSet::new();
let mut unresolved: HashSet<String> = 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::<DnsPointer>() 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.<Domain>".
// 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<IpAddr> = 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<String, HashSet<String>>) {
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<HashMap<String, i64>>) {
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::<usize>())
.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<ServiceEvent>,
) {
let pretty_addrs: Vec<String> = 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<HostnameResolutionEvent>,
timeout: Option<u64>,
) {
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<UnregisterStatus>,
) {
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<u8>, 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::<Vec<_>>();
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<IpAddr>,
) {
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<ResolvedService>),
/// 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<ScopedIp>),
/// One or more addresses for a hostname has been removed.
AddressesRemoved(String, HashSet<ScopedIp>),
/// 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<ServiceEvent>),
/// Resolve a hostname to IP addresses.
ResolveHostname(String, u32, Sender<HostnameResolutionEvent>, Option<u64>), // (hostname, next_time_delay_in_seconds, sender, timeout_in_milliseconds)
/// Register a service
Register(Box<ServiceInfo>),
/// Unregister a service
Unregister(String, Sender<UnregisterStatus>), // (fullname)
/// Announce again a service to local network
RegisterResend(String, u32), // (fullname)
/// Resend unregister packet.
UnregisterResend(Vec<u8>, 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<Metrics>),
/// Get the current status of the daemon.
GetStatus(Sender<DaemonStatus>),
/// Monitor noticeable events in the daemon.
Monitor(Sender<DaemonEvent>),
SetOption(DaemonOption),
GetOption(Sender<DaemonOptionVal>),
/// 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<Interface>),
Exit(Sender<DaemonStatus>),
}
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<IfKind>, usize),
EnableInterface(Vec<IfKind>),
DisableInterface(Vec<IfKind>),
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 "_<service_name>.<domain_name>." 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 = <Instance>.<Service>.<Domain>
/// The only `<Domain>` supported are "._tcp.local." and "._udp.local.".
///
/// Note: this function does not check for the length of the service name.
/// Instead, `register_service` method will check the length.
fn check_service_name(fullname: &str) -> Result<()> {
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<String, Sender<ServiceEvent>>,
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<String, (Sender<HostnameResolutionEvent>, Option<u64>)>,
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<Interface> {
my_ip_interfaces_inner(with_loopback, false)
}
fn my_ip_interfaces_inner(with_loopback: bool, with_apple_p2p: bool) -> Vec<Interface> {
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<SocketAddr>,
) -> MyResult<Vec<Vec<u8>>> {
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<SocketAddr>,
) -> MyResult<Vec<Vec<u8>>> {
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:
/// <instance>.<service_type>.<_udp|_tcp>.local.
/// Note: <instance> could contain '.' as well.
fn valid_instance_name(name: &str) -> bool {
name.split('.').count() >= 5
}
fn notify_monitors(monitors: &mut Vec<Sender<DaemonEvent>>, 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<DnsOutgoing> {
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<bool> {
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 `(<num>)` 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::<u32>() {
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 `-<num>` 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::<u32>() {
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<Reverse<u64>>,
now: u64,
) -> (DnsOutgoing, Vec<String>) {
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<Reverse<u64>>, 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<String>,
intf_name: &str,
dns_registry: &mut DnsRegistry,
monitors: &mut Vec<Sender<DaemonEvent>>,
) -> HashSet<String> {
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<IfAddr> {
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();
}
}
+2112
View File
@@ -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<IfAddr>,
/// 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":
/// <https://datatracker.ietf.org/doc/html/rfc6763#section-7.1>
sub_domain: Option<String>, // <subservice>._sub.<service>.<domain>
fullname: String, // <instance>.<service>.<domain>
server: String, // fully qualified name for service host
addresses: HashSet<IpAddr>,
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<u32, ServiceStatus>, // 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<IfKind>,
/// 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<String, String>`
/// - `Option<HashMap<String, String>>`
/// - 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<Ip: AsIpAddrs, P: IntoTxtProperties>(
ty_domain: &str,
my_name: &str,
host_name: &str,
ip: Ip,
port: u16,
properties: P,
) -> Result<Self> {
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<IfKind>) {
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<String> {
&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<Option<&[u8]>> {
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<IpAddr> {
&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<IpAddr> {
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<IpAddr> {
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<u8> {
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<ScopedIp> = 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<HashSet<IpAddr>>;
}
impl<T: AsIpAddrs> AsIpAddrs for &T {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
(*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<HashSet<IpAddr>> {
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<HashSet<IpAddr>> {
self.as_str().as_ip_addrs()
}
}
/// Support slice. Example: &["127.0.0.1", "127.0.0.2"]
impl<I: AsIpAddrs> AsIpAddrs for &[I] {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
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<HashSet<IpAddr>> {
Ok(HashSet::new())
}
}
impl AsIpAddrs for std::net::IpAddr {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
let mut ips = HashSet::new();
ips.insert(*self);
Ok(ips)
}
}
impl AsIpAddrs for Box<dyn AsIpAddrs> {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
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<TxtProperty>,
}
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<Item = &TxtProperty> {
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<Option<&[u8]>> {
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<String, String> {
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<String> = 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<Vec<u8>>,
}
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<K, V> 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<K, V> 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<String, String> {
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<HashMap<String, String>> {
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<TxtProperty> {
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<Item = &'a TxtProperty>) -> Vec<u8> {
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<TxtProperty> {
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<TxtProperty> {
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<DnsRecordBox>,
/// 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<String>,
/// 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<String, Probe>,
/// Already done probing, or no need to probe.
/// Keyed by DNS record name.
pub(crate) active: HashMap<String, Vec<DnsRecordBox>>,
/// timers of the newly added probes.
pub(crate) new_timers: Vec<u64>,
/// Mapping from original names to new names.
pub(crate) name_changes: HashMap<String, String>,
/// 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<String, u64>,
/// Same as [`Self::last_multicast_v4`] but for this interface's IPv6 group.
pub(crate) last_multicast_v6: HashMap<String, u64>,
}
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<T>(
&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::<DnsSrv>() {
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::<DnsSrv>() {
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<String, u64>,
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":
/// <https://datatracker.ietf.org/doc/html/rfc6763#section-7.1>
/// For example, "_printer._sub._http._tcp.local."
pub sub_ty_domain: Option<String>,
/// 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<ScopedIp>,
/// 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<IpAddr>,
}
impl ResolvedService {
/// Returns the observed response source when this value came from the
/// network cache.
#[inline]
pub const fn get_observed_source(&self) -> Option<IpAddr> {
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<String> {
&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<ScopedIp> {
&self.addresses
}
pub fn get_addresses_v4(&self) -> HashSet<Ipv4Addr> {
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<Option<&[u8]>> {
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<u8> = 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<u8> = 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<ScopedIp> = HashSet::from([ScopedIp::V4(scoped_v4_one_intf)]);
let set_new: HashSet<ScopedIp> = 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<dyn std::error::Error>> {
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
}
}
}
+148
View File
@@ -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
})
);
}
+2707
View File
@@ -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<IpAddr> = 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<Interface> = 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<Interface> = 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<u8> = 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<Interface> = 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<Interface> = 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<Interface> = 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<Interface> {
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();
}
+381
View File
@@ -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");
}