mdns-sd-0.13.3/.cargo_vcs_info.json0000644000000001360000000000100124650ustar { "git": { "sha1": "085a1039db3b9e149376b913ca4f7b0f8c2ec643" }, "path_in_vcs": "" }mdns-sd-0.13.3/.github/workflows/build.yml000064400000000000000000000022161046102023000164750ustar 00000000000000name: Build and Test on: push: branches: [main] pull_request: branches: [main] env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-20.04, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@1.70.0 with: components: rustfmt, clippy - name: Run rustfmt and fail if any warnings run: cargo fmt -- --check - name: Run docs and fail if any warnings env: RUSTDOCFLAGS: "-D warnings" run: cargo doc --all-features - name: Build with feature configuration run: cargo build --no-default-features --features async - name: Build run: cargo build - name: Run clippy and fail if any warnings run: cargo clippy -- -D warnings - name: Run tests with debugs if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest' run: RUST_LOG=debug cargo test --verbose - name: Run tests on Windows if: matrix.os == 'windows-latest' run: cargo test mdns-sd-0.13.3/.gitignore000064400000000000000000000006151046102023000132470ustar 00000000000000# IntelliJ IDEA /.idea/ # Visual Studio Code /.vscode/ # Generated by Cargo # will have compiled files and executables /target/ mdns-parser/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk mdns-sd-0.13.3/CHANGELOG.md000064400000000000000000000437461046102023000131040ustar 00000000000000# Version 0.13.3 (2025-03-01) ## Highlights * `TxtProperties`: Support `into_property_map_str`. * For querier: a new struct `ResolvedService` that can be created from `ServiceInfo`. * Support a service to publish using loopback interfaces via `enable_interface`. * Bugfixes. ## All changes * `3e6842f 2025-02-28` enable_interface: support loopback interface (#317) (keepsimple1) * `7e77a5e 2025-02-26` fix: remove related addresses in the cache when disabling an interface (#316) (keepsimple1) * `735fb22 2025-02-22` refactoring: move handle_poller_events into Zeroconf impl (#315) (keepsimple1) * `e886787 2025-02-20` ci: add doc check and fail if there are any warnings (#310) (CosminPerRam) * `9d92545 2025-02-18` refactoring: make e_fmt! available within the crate (#311) (keepsimple1) * `4b671d4 2025-02-14` ResolvedService: a new plain struct for attributes of a service (#302) (keepsimple1) * `349de66 2025-02-13` fix: include loopback addresses in filtering criteria (#306) (Minetake) * `026a745 2025-02-09` refactoring: property length check (#305) (keepsimple1) * `818f37c 2025-02-10` TXT record: docs to remind users of the maximum length of the attribute. (#304) (Lazy Panda) * `fba8025 2025-02-09` TxtProperties: new method to get a HashMap of properties (#303) (keepsimple1) # Version 0.13.2 (2025-02-02) This is a bugfix release. ## All changes * 4288190 check any match for address records in conflict handler (#294) (keepsimple1) * b51f67d unit test: fix a timing issue (#292) (keepsimple1) * 7afed98 bugfix: check data len for NSEC record (#291) (keepsimple1) # Version 0.13.1 (2024-12-16) This is a bugfix release. Fixed a bug where upper case service names failed to publish. ## All changes * 71647a1 test: cover upper case in service name (#288) (keepsimple1) * 6ff9b52 fix: service keys must be lowercase (#286) (Jesper L. Nielsen) # Version 0.13.0 (2024-12-15) There are no breaking changes in API. Bump the minor version due to the change of rustc version to Rust 1.70.0. ## Highlights * Use `mio` instead of `polling` to poll sockets. * New API `set_multicast_loop_v4` and `set_multicast_loop_v6` of `ServiceDaemon`. * All logging are updated to be `debug` or `trace` levels only. ## All changes * 489ef5a test: fix a flaky test (#283) (keepsimple1) * 1ddae63 feat: new API to set multicast loop for ServiceDaemon (#281) (keepsimple1) * fcd31f3 dependency: use mio to replace polling (#280) (keepsimple1) * 99483b7 reduce logging levels (#277) (keepsimple1) # Version 0.12.0 (2024-11-24) There are no breaking changes in API. Bump the minor version due to new features and the change of rustc version. ## Highlights * Support name probing and conflict resolution [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-8) * Support service liveness checking via `verify` API. [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-10.4) * rustc version changed to 1.65.0 * performance improvements and doc updates. ## All changes * 7f6c5e9 perf: avoid cloning in filtering ptr (#272) (CosminPerRam) * e185d6f refactoring: define an enum for DNS resource record types (#274) (keepsimple1) * d117f4f refactoring: move exec_command into Zeroconf (#273) (keepsimple1) * 39acd80 feat: replace remaining Box with type (#271) (CosminPerRam) * b50fe8c perf: optimize u8_slice_to_hex by replacing Vec with String (#270) (CosminPerRam) * db545b1 doc: some spelling fixes (#269) (CosminPerRam) * 7328f45 doc: add a table of RFC compliance details (#268) (keepsimple1) * 1ade666 feat: verify to support Cache Flush on Failure Indication (#267) (keepsimple1) * 8b63fd7 feat: support name probing and conflict resolution (#265) (keepsimple1) * 429ecde dev-test: enhance test case for ipv4 only auto addr (#263) (keepsimple1) * f902cf2 register service: apply interface selection for auto IP addr (#262) (keepsimple1) * 0381e30 dns_cache: address record should only flush on the same network (#261) (keepsimple1) # Version 0.11.5 (2024-09-28) This is a bugfix release. ## All changes * 2829d8e tests: fix remove addr test (#258) (keepsimple1) * 4f58e2f dns_parser: check against potential name compression loop (#257) (keepsimple1) # Version 0.11.4 (2024-09-10) Bugfixes. Added checks for corrupted RR data to prevent unnecessary panics. Thanks for new contributor @rise0chen ! Sorry that this release has a few merged small commits as I didn't know how to properly merge in a PR that targets a fetaure branch used in another PR, instead of `main` branch. ## All changes * e54485e add --verbose in CI test run (#254) (keepsimple1) * f0c4c27 remove fastrand dependency from dev-test (#252) (keepsimple1) * dff1596 Merge pull request #250 from keepsimple1/rdata-check (keepsimple1) * 659e684 fix cargo clippy warning (keepsimple1) * 90a2f12 Merge pull request #251 from rise0chen/rdata-check (keepsimple1) * 6d51f55 Merge branch 'rdata-check' into rdata-check (keepsimple1) * 1b2cf40 add a check for rr data len (keepsimple1) * a5de799 feat: test random data (rise0chen) * 40698a3 add test case and simplify DnsTxt::new (keepsimple1) * fc489bd refactoring error log (keepsimple1) * a3fad8e add a check for rr data len (keepsimple1) # Version 0.11.3 (2024-08-23) A release of bugfixes and refactorings. ## All changes * 3292110 DnsTxt debug print: make its text field human-readable (#247) (keepsimple1) (2024-08-21) * 5567c1f Send SearchStarted events with addrs as `ip (intf-name)` (#245) (hrzlgnm) (2024-08-22) * 9a91a53 cache flush: add the missing timer for updated expires (#244) (keepsimple1) (2024-08-19) * c1d7efa Change intf_socks to a map of (Interface, Socket) (#242) (keepsimple1) (2024-08-18) * 404100d refactor out a common method for DnsRecordExt (#241) (keepsimple1) (2024-08-18) * f055c78 Refresh A and AAAA records of active `.browse` queriers (#240) (hrzlgnm) (2024-08-17) * 0453030 Avoid redundant query, announcement and unregistration overhaul (#239) (hrzlgnm) (2024-08-16) # Version 0.11.2 (2024-08-06) Mostly a bugfix and refactoring release, with limited support added for: - Known Answer Suppression (RFC 6762 section 7.1 and 7.2): - single packet for querier and responder, - multi-packet for querier. ## All changes * 92eae74 add support for Known Answer Suppression part 2: multi-packet: querier side (#232) (keepsimple1) * ada3486 fix test integration_success: respond count or known answer suppression count (#237) (keepsimple1) * 8106d07 Skip link local addresses while checking for redundant announcements or query packets (#235) (hrzlgnm) * b1a173a check data length in read_u16 (#234) (keepsimple1) * d1c9157 Add sanity check for service type domain suffix in browse (#231) (keepsimple1) * 736bec6 enable DEBUG logging for a failed test in CI (#229) (keepsimple1) * fd00210 add logs in test to debug CI failure (#228) (keepsimple1) * 5e0f1d3 add support for Known Answer Suppression part 1 (#227) (keepsimple1) * 5ae18a6 refactoring: remove Send for DnsRecordBox (#226) (keepsimple1) * d7d4867 fix integration_success test (#223) (keepsimple1) * 6f34f1c move DnsCache into its own module (#221) (keepsimple1) * bcdc2f9 add welcome to our new contributor (#220) (keepsimple1) # Version 0.11.1 (2024-05-13) ## Highlights - Start to honor cache flush bit. - Improved cache refresh logic. - Code refactorings. - And a few bugfixes. ## All changes * 098f2df move unit tests into integration test (#218) * 80291ba refresh PTR records (#217) * 5eb74b5 refactoring: extract details from exec_command into own functions (#215) * 551ed4d Bugfix: AddressesRemoved missing actual addrs (#210) * 3c924f4 Bugfix: cache flush properly (#211) * ccdae2d Bugfix: logging feature cannot be disabled (#212) * 626f9fa refresh SRV records and send out ServiceRemoved for expired SRV (#180) * 06e2cf7 feat: merge match same arms (#209) * bf5cea3 perf: in adding answers, use static dispatch instead of dynamic dispatch (#207) * 19d2161 feat: extract match addr to type as a function (#205) * 5bdcdd6 feat: remove clone derive from counter (#208) * e7fc0e0 feat: replace box dns with declared type (#206) * 5732665 feat: apply nursery lints (#202) * 16cb5cd feat: honor cache flush (#201) Welcome our new contributor: @lyager ! Thanks! # Version 0.11.0 (2024-04-21) ## Breaking changes * Now `ServiceDaemon::register()` requires `hostname` to end with ".local." ## New features * Support resolving hostnames directly: `ServiceDaemon::resolve_hostname()` ## All changes * example code: refactor the query output prints and the register hostname (#189) * support multiple questions in send_query_vec (#194) * CI: fix a test waiting for IPv6 addr (#195) * Add support for resolving non-service hostnames (#192) * zeroconf: use min heap for timers (#196) * Fix flaky test (#198) * enable logging for examples and add doc for logging (#199) Welcome our new contributor: @oysteintveit-nordicsemi ! Thanks! # Version 0.10.5 (2024-03-24) ## Notes * Port 0 is now considered valid in ServiceInfo (#181) ## Changes * reduce SearchStopped notification send error to warn (#178) * refactoring: extract handle_poller_events() (#177) * Do not consider port 0 as a missing info (#181) * query TYPE_A and TYPE_AAAA via Command::Resolve (#185) * bump socket2 version (#174) * add NSEC record to debug resolve issue (#183) Welcome our new contributors: @hrzlgnm and @irvingoujAtDevolution ! Thanks! # Version 0.10.4 (2024-02-10) This is a bug fix release. ## Changes * Add sanity checks in DNS message decoding (#169) * fine-tune MAX_MSG_ABSOLUTE (#170) # Version 0.10.3 (2024-01-14) This is a bug fix release. ## Changes * netmask -> subnet (#164) Welcome our new contributor @amfaber ! Thanks! # Version 0.10.2 (2023-12-28) This is a bug fix release. ## Changes * use human-readable address in error log of send_packet (#155) * query for unresolved instances only when needed (#157) * Fix panic due to range out of bounds in txt record parsing (#159) * Sanity check for empty service type name (#160) * Added comment for updating service info by re-registering. Welcome our new contributor @Raphiiko ! Thanks! Happy new year 2024! # Version 0.10.1 (2023-12-2) This is a bug fix release. ## Changes * update flume to 0.11 (#152) * bugfix: signal event key is possible to overlap with socket poll ids (#153) # Version 0.10.0 (2023-11-28) ## Breaking changes * `ServiceDaemon::shutdown()` return type changed from `Result<()>` to `Result>` (#149) ## Other changes * Related to the breaking change, a client can receive `DaemonStatus` to be sure the daemon is shutdown. * A new enum `DaemonStatus` and a new API `ServiceDaemon::status()` are introduced. * Updated CI in GitHub Actions: replace `actions-rs` with `dtolnay/rust-toolchain`. # Version 0.9.3 This is a bugfix release. * apply interface selections when IP addresses change (#142) * Remove un-necessary panic (#144) * Always include subtype info if exists (#146) p.s. Happy Halloween! # Version 0.9.2 The release includes a bugfix, thanks to @Mornix ! * fix PTR expiration from preventing later service resolution (#140) * updated doc comments for `DnsCache::add_or_update`. # Version 0.9.1 There are no breaking changes. * support interface selection (#137) Added two new methods for `ServiceDaemon`: `enable_interface` and `disable_interface`, and some refactoring. # Version 0.9.0 * Ssupports IPv6 (#130) (Thanks to @izissise) * ServiceInfo: support get_addresses_v4 (#132) * bugfix: set address type correctly (#134) This is a breaking change, including: - Trait `AsIpv4Addrs` changes to `AsIpAddrs` to support both IPv4 and IPv6. - `ServiceInfo::new()` uses the new `AsIpAddrs` trait. - `ServiceInfo::get_addresses()` returns both IPv4 and IPv6 addresses, while a new convenience method `get_addresses_v4` returns IPv4 only. But in general, because the trait hides away details, the user code is likely keeping working without code changes. Improvements: * avoid redundant annoucement or query packets (#135) # Version 0.8.1 * Remove env_logger in dev-dependencies and lower MSRV to 1.60.0. (#128) # Version 0.8.0 No breaking changes in API. This release brings two potential user-visible changes: * use UDP socket to signal the daemon for commands. (#125) This change reduces CPU utilization of the daemon thread as well as its latency to the user commands. Internally it uses local UDP sockets to signal the daemon. * Added the link-local feature to if_addrs in Cargo.toml to enable link-local interfaces in Windows. (#126) This change makes link-local interfaces visible to users in Windows where they didn't show up previously. # Version 0.7.5 * Revert the changes in v0.7.4 and support link-local addrs alongside routable addrs. (#122) # Version 0.7.4 (deprecated) * Not to use link-local addrs if routable addrs exist (#117) # Version 0.7.3 ## Highlights - Internal refactoring: always use DnsCache to resolve Servive Instances. When processing incoming packets, we used to update the cache one record at a time and also build separate service info structs to resolve. Now we finish the cache updates first, and then resolve instances from the cache. - Added env_logger for the examples code and enhanced the examples as well. ## What's Changed * Support updating instances after they are resolved by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/104 * add optional "unregister" in example code by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/107 * Returns an error with logging for read_name invalid offset by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/109 * register example should keep running by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/110 * Refactoring DnsCache and how to resolve Service Instance by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/108 * add sanity check in reading a record data RDATA by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/111 * Enable logging for the examples by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/112 * register example: a simpler input for the service type by @keepsimple1 in https://github.com/keepsimple1/mdns-sd/pull/113 # Version 0.7.2 Highlights: - Implemented `Display` trait for `TxtProperty`: print using `key=value` format, where `value` is same as `.get_property_val_str()`. - Implemented `Debug` trait for `TxtProperty`: print using a struct format, where `value` prints as a string if it is UTF-8, or prints as hex if it is not UTF-8. # Version 0.7.1 Highlights: - A bug fix: remove duplicated keys in TXT records received. # Version 0.7.0 Breaking Changes: - Allow non-standard max length for a service name. The check for the length of a service name is moved to the daemon. If a service name is too long, there will be an error log and an error event sent to the monitors. - `ServiceInfo.get_property_val()` returns `Option>` instead of `Option<&str>`. Now a new `ServiceInfo.get_property_val_str()` returns `Option<&str>`. In other words, migrate to `get_property_val_str()` if you don't want to worry about non-UTF8 values. Highlights: - Allow non-standard max length for a service name: A new method `ServiceDaemon.set_service_name_len_max()` is added to support that. Only use it when you really need to. - Support non-UTF-8 value for TXT properties. - Support `no value` for a TXT property, i.e. boolean keys. - Added checks for ASCII keys in a TXT property. # Version 0.6.1 Highlights: - Fixs a bug: missing TXT records in received responses. # Version 0.6.0 Breaking Changes: - `ServiceInfo::new()` takes `IntoTxtProperties` trait instead of a `HashMap` of properties. It is also backward-compatiable: the trait is implemented for `HashMap` and `Option`. - `ServiceInfo::get_properties()` returns `&TxtProperties` instead of a `HashMap` of properties. It is also mostly backward-compatiable: support `iter()`, `get()` methods. Highlights: - TXT properties' names are now case insensitive. And the original user input order is kept. - A new method `ServiceInfo::enable_addr_auto()`: automatically fill in IP addresses for published services. - Detect IP changes. - A new `ServiceDaemon::monitor()` method to return a `Receiver` handle to monitor the daemon events, such as IP changes. # Version 0.5.10 - skip interfaces that failed to bind (#79) (re-apply fix in v0.5.6) # Version 0.5.9 - Ignore duplicate keys (#74) - update error msg for send_packet (#69) # Version 0.5.8 - call check_service_name before sending the cmd to the daemon. (#60) - Changed dependency on 'log' crate to be optional (#64) - configure mDNS daemon thread a name (#66) - log an error if socket read returns 0 and reset the socket (#67) # Version 0.5.7 - Allow service names with trailing '.' (#56) - query unresolved instances (#58) # Verison 0.5.6 - handle join_multicast_v4 error gracefully (#53) # Version 0.5.5 - track IPv4 interfaces with sockets to support multiple LANs (#48) # Version 0.5.4 - Fix a bug in resolving multiple IPs for a host. - Code reorg: separate modules out of lib.rs. - Listening socket joins multicast on all interfaces. # Version 0.5.3 - Support subtypes. - Bind every valid IPv4 interface for outgoing sockets. - Include Windows and macOS in GitHub Actions. # Version 0.5.2 - Add support for Windows platform. # Version 0.5.1 - Fix missing info in the license files. - Add docs.rs badge. - Make Error implement std::error::Error. # Version 0.5.0 - Allow multiple formats for host_ipv4 to create ServiceInfo. - A breaking change: change `ServiceInfo::new()` to return a `Result<>`. - Update `nix` dependency to version 0.24.1. # Version 0.4.3 - Fix a bug in stop-browse # Version 0.4.2 - New feature: support meta-query `_services._dns-sd._udp` per RFC 6763. # Version 0.4.1 - Update docs. # Version 0.4.0 - Replace `crossbeam-channel` with `flume`. # Version 0.3.0 - Add "get_metrics" in API. - Fixed a bug in cache refresh. - Fixed a bug in retransmission. # Version 0.2.2 - Add the first example code. Thanks @lu-zero! (#5) # Version 0.2.1 - mDNS daemon respond socket to be blocking for simpler send. # Version 0.2.0 - Public API internally to use the unblocking try_send() to replace send(). - Add `Again` in Error type to support retry. # Version 0.1.0 - Initial version mdns-sd-0.13.3/Cargo.lock0000644000000160450000000000100104460ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "env_logger" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "log", ] [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "flume" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", "spin", ] [[package]] name = "futures-core" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "futures-sink" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "if-addrs" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78a89907582615b19f6f0da1af18abf6ff08be259395669b834b057a7ee92d8" dependencies = [ "libc", "windows-sys", ] [[package]] name = "libc" version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "lock_api" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "mdns-sd" version = "0.13.3" dependencies = [ "env_logger", "fastrand", "flume", "humantime", "if-addrs", "log", "mio", "socket2", "test-log", "test-log-macros", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", "wasi", "windows-sys", ] [[package]] name = "proc-macro2" version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ "lock_api", ] [[package]] name = "syn" version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "test-log" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6159ab4116165c99fc88cce31f99fa2c9dbe08d3691cb38da02fc3b45f357d2b" dependencies = [ "env_logger", "test-log-macros", ] [[package]] name = "test-log-macros" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba277e77219e9eea169e8508942db1bf5d8a41ff2db9b20aab5a5aadc9fa25d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" mdns-sd-0.13.3/Cargo.toml0000644000000040510000000000100104630ustar # 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.70.0" name = "mdns-sd" version = "0.13.3" authors = ["keepsimple "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "mDNS Service Discovery library with no async runtime dependency" documentation = "https://docs.rs/mdns-sd" readme = "README.md" keywords = [ "mdns", "service-discovery", "zeroconf", "dns-sd", ] categories = ["network-programming"] license = "Apache-2.0 OR MIT" repository = "https://github.com/keepsimple1/mdns-sd" [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" [dependencies.fastrand] version = "2.1" [dependencies.flume] version = "0.11" default-features = false [dependencies.if-addrs] version = "0.13" features = ["link-local"] [dependencies.log] version = "0.4" optional = true [dependencies.mio] version = "1.0" features = [ "os-poll", "net", ] [dependencies.socket2] version = "0.5.5" features = ["all"] [dev-dependencies.env_logger] version = "= 0.10.2" features = ["humantime"] default-features = false [dev-dependencies.fastrand] version = "2.1" [dev-dependencies.humantime] version = "2.1" [dev-dependencies.test-log] version = "= 0.2.14" [dev-dependencies.test-log-macros] version = "= 0.2.14" [features] async = ["flume/async"] default = [ "async", "logging", ] logging = ["log"] mdns-sd-0.13.3/Cargo.toml.orig000064400000000000000000000021571046102023000141510ustar 00000000000000[package] name = "mdns-sd" version = "0.13.3" authors = ["keepsimple "] edition = "2018" rust-version = "1.70.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"] default = ["async", "logging"] [dependencies] fastrand = "2.1" flume = { version = "0.11", default-features = false } # channel between threads if-addrs = { version = "0.13", features = ["link-local"] } # get local IP addresses log = { version = "0.4", optional = true } # logging mio = { version = "1.0", features = ["os-poll", "net"] } # select/poll sockets socket2 = { version = "0.5.5", features = ["all"] } # socket APIs [dev-dependencies] env_logger = { version = "= 0.10.2", default-features = false, features= ["humantime"] } fastrand = "2.1" humantime = "2.1" test-log = "= 0.2.14" test-log-macros = "= 0.2.14" mdns-sd-0.13.3/LICENSE-APACHE000064400000000000000000000251511046102023000132050ustar 00000000000000 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. mdns-sd-0.13.3/LICENSE-MIT000064400000000000000000000021031046102023000127050ustar 00000000000000MIT 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. mdns-sd-0.13.3/README.md000064400000000000000000000100421046102023000125310ustar 00000000000000# 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] | ❌ | because we don't support Unicast yet. | | Unicast Responses | RFC 6762 [section 5.4][ref2] | ❌ | | 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()` | [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 ## 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. mdns-sd-0.13.3/examples/query.rs000064400000000000000000000040171046102023000146100ustar 00000000000000//! A mDNS query client. //! //! Run with: //! //! cargo run --example query //! //! Example: //! //! cargo run --example query _my-service._udp //! //! Note: there is no '.' at the end as the program adds ".local." //! automatically. //! //! Keeps listening for new events. use mdns_sd::{ServiceDaemon, ServiceEvent}; fn main() { env_logger::builder().format_timestamp_millis().init(); // Create a daemon let mdns = ServiceDaemon::new().expect("Failed to create daemon"); let mut service_type = match std::env::args().nth(1) { Some(arg) => arg, None => { print_usage(); return; } }; // Browse for a service type. service_type.push_str(".local."); 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.get_fullname(), info.get_hostname(), info.get_port(), ); for addr in info.get_addresses().iter() { println!(" Address: {}", addr); } for prop in info.get_properties().iter() { println!(" Property: {}", prop); } } other_event => { println!("At {:?}: {:?}", now.elapsed(), &other_event); } } } } fn print_usage() { println!("Usage: cargo run --example query "); println!("Example: "); println!("cargo run --example query _my-service._udp"); 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"); } mdns-sd-0.13.3/examples/register.rs000064400000000000000000000073731046102023000152770ustar 00000000000000//! Registers a mDNS service. //! //! Run with: //! //! cargo run --example register [options] //! //! Example: //! //! cargo run --example register _my-hello._udp instance1 host1 //! //! Options: //! "--unregister": automatically unregister after 2 seconds. //! "--disable-ipv6": not to use IPv6 interfaces. use mdns_sd::{DaemonEvent, IfKind, ServiceDaemon, ServiceInfo}; use std::{env, thread, time::Duration}; fn main() { // setup env_logger with more precise timestamp. let mut builder = env_logger::Builder::from_default_env(); builder.format_timestamp_millis().init(); // Simple command line options. let args: Vec = env::args().collect(); let mut should_unreg = false; let mut disable_ipv6 = false; for arg in args.iter() { if arg.as_str() == "--unregister" { should_unreg = true } else if arg.as_str() == "--disable-ipv6" { disable_ipv6 = true } } // Create a new mDNS daemon. let mdns = ServiceDaemon::new().expect("Could not create service daemon"); if disable_ipv6 { mdns.disable_interface(IfKind::IPv6).unwrap(); } let service_type = match args.get(1) { Some(arg) => format!("{}.local.", arg), None => { print_usage(); return; } }; let instance_name = match args.get(2) { Some(arg) => arg, None => { print_usage(); return; } }; let hostname = match args.get(3) { Some(arg) => arg, None => { print_usage(); return; } }; // With `enable_addr_auto()`, we can give empty addrs and let the lib find them. // If the caller knows specific addrs to use, then assign the addrs here. let my_addrs = ""; let service_hostname = format!("{}.local.", hostname); let port = 3456; // The key string in TXT properties is case insensitive. Only the first // (key, val) pair will take effect. let properties = [("PATH", "one"), ("Path", "two"), ("PaTh", "three")]; // Register a service. let service_info = ServiceInfo::new( &service_type, instance_name, &service_hostname, my_addrs, port, &properties[..], ) .expect("valid service info") .enable_addr_auto(); // Optionally, we can monitor the daemon events. let monitor = mdns.monitor().expect("Failed to monitor the daemon"); let service_fullname = service_info.get_fullname().to_string(); mdns.register(service_info) .expect("Failed to register mDNS service"); println!("Registered service {}.{}", &instance_name, &service_type); if should_unreg { let wait_in_secs = 2; println!("Sleeping {} seconds before unregister", wait_in_secs); thread::sleep(Duration::from_secs(wait_in_secs)); let receiver = mdns.unregister(&service_fullname).unwrap(); while let Ok(event) = receiver.recv() { println!("unregister result: {:?}", &event); } } else { // Monitor the daemon events. while let Ok(event) = monitor.recv() { println!("Daemon event: {:?}", &event); if let DaemonEvent::Error(e) = event { println!("Failed: {}", e); break; } } } } fn print_usage() { println!("Usage:"); println!("cargo run --example register [options]"); println!("Options:"); println!("--unregister: automatically unregister after 2 seconds"); println!("--disable-ipv6: not to use IPv6 interfaces."); println!(); println!("For example:"); println!("cargo run --example register _my-hello._udp instance1 host1"); } mdns-sd-0.13.3/src/dns_cache.rs000064400000000000000000000505771046102023000143370ustar 00000000000000//! A cache for DNS records. //! //! This is an internal implementation, not visible to the public API. #[cfg(feature = "logging")] use crate::log::{debug, trace}; use crate::{ dns_parser::{DnsAddress, DnsPointer, DnsRecordBox, DnsSrv, RRType}, service_info::{split_sub_domain, valid_ip_on_intf, valid_two_addrs_on_intf}, }; use if_addrs::Interface; use std::{ collections::{HashMap, HashSet}, net::IpAddr, time::SystemTime, }; /// A cache for all types of DNS records. pub(crate) struct DnsCache { /// DnsPointer records indexed by ty_domain ptr: HashMap>, /// DnsSrv records indexed by the fullname of an instance srv: HashMap>, /// DnsTxt records indexed by the fullname of an instance txt: HashMap>, /// DnsAddr records indexed by the hostname addr: HashMap>, /// A reverse lookup table from "instance fullname" to "subtype PTR name" subtype: HashMap, /// Negative responses: /// A map from "instance fullname" to DnsNSec. nsec: HashMap>, } impl DnsCache { pub(crate) fn new() -> Self { Self { ptr: HashMap::new(), srv: HashMap::new(), txt: HashMap::new(), addr: HashMap::new(), subtype: HashMap::new(), nsec: HashMap::new(), } } pub(crate) fn all_ptr(&self) -> &HashMap> { &self.ptr } pub(crate) fn get_ptr(&self, ty_domain: &str) -> Option<&Vec> { self.ptr.get(ty_domain) } pub(crate) fn get_srv(&self, fullname: &str) -> Option<&Vec> { self.srv.get(fullname) } pub(crate) fn get_txt(&self, fullname: &str) -> Option<&Vec> { self.txt.get(fullname) } pub(crate) fn get_addr(&self, hostname: &str) -> Option<&Vec> { self.addr.get(hostname) } /// A reverse lookup table from "instance fullname" to "subtype PTR name" pub(crate) fn get_subtype(&self, fullname: &str) -> Option<&String> { self.subtype.get(fullname) } /// Returns the list of instances that has `host` as its hostname. pub(crate) fn get_instances_on_host(&self, host: &str) -> Vec { self.srv .iter() .filter_map(|(instance, srv_list)| { if let Some(item) = srv_list.first() { if let Some(dns_srv) = item.any().downcast_ref::() { if dns_srv.host() == host { return Some(instance.clone()); } } } None }) .collect() } /// Returns the set of IP addresses for a hostname. pub(crate) fn get_addresses_for_host(&self, host: &str) -> HashSet { self.addr .get(host) .into_iter() .flatten() .filter_map(|record| { record .any() .downcast_ref::() .map(|addr| addr.address()) }) .collect() } /// Returns a list of resource records (name, rr_type) that need to be queried in order to /// verify the `instance`. /// /// If `expire_at` is not None, the resource records' expire time will be updated. pub(crate) fn service_verify_queries( &mut self, instance: &str, expire_at: Option, ) -> Vec<(String, RRType)> { let Some(srv_vec) = self.srv.get_mut(instance) else { return Vec::new(); }; let mut query_vec = vec![(instance.to_string(), RRType::SRV)]; for srv in srv_vec { if let Some(new_expire) = expire_at { srv.set_expire_sooner(new_expire); } let Some(srv_record) = srv.any().downcast_ref::() else { continue; }; // Will verify addresses for the hostname. query_vec.push((srv_record.host().to_string(), RRType::A)); query_vec.push((srv_record.host().to_string(), RRType::AAAA)); if let Some(new_expire) = expire_at { if let Some(addrs) = self.addr.get_mut(srv_record.host()) { for addr in addrs { addr.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: &Interface, incoming: DnsRecordBox, timers: &mut Vec, ) -> Option<(&DnsRecordBox, bool)> { let entry_name = incoming.get_name().to_string(); // If it is PTR with subtype, store a mapping from the instance fullname // to the subtype in this cache. if incoming.get_type() == RRType::PTR { let (_, subtype_opt) = split_sub_domain(&entry_name); if let Some(subtype) = subtype_opt { if let Some(ptr) = incoming.any().downcast_ref::() { if !self.subtype.contains_key(ptr.alias()) { self.subtype .insert(ptr.alias().to_string(), subtype.to_string()); } } } } // Check if address is valid on the interface. When IP_MULTICAST_LOOP is enabled, // a multicast packet would loopback to other interfaces of the same multicast group on Linux. if incoming.get_type() == RRType::A || incoming.get_type() == RRType::AAAA { if let Some(answer_addr) = incoming.any().downcast_ref::() { let addr = answer_addr.address(); if !valid_ip_on_intf(&addr, intf) { debug!( "add_or_update: answer addr {addr} not in the subnet of {}", intf.ip() ); return None; } } } // get the existing records for the type. 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).or_default(), RRType::NSEC => self.nsec.entry(entry_name).or_default(), _ => return None, }; if incoming.get_cache_flush() { let now = current_time_millis(); let class = incoming.get_class(); let rtype = incoming.get_type(); record_vec.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.get_class() && rtype == r.get_type() && now > r.get_created() + 1000 && r.get_expire() > now + 1000 { should_flush = true; // additional checks for address records. if rtype == RRType::A || rtype == RRType::AAAA { if let Some(addr) = r.any().downcast_ref::() { if let Some(addr_b) = incoming.any().downcast_ref::() { should_flush = valid_two_addrs_on_intf( &addr.address(), &addr_b.address(), intf, ); } } } } if should_flush { trace!("FLUSH one record: {:?}", &r); let new_expire = now + 1000; r.set_expire(new_expire); // Add a timer so the run loop will handle this expire. timers.push(new_expire); } }); } // update TTL for existing record or create a new record. let (idx, updated) = match record_vec .iter_mut() .enumerate() .find(|(_idx, r)| r.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.reset_ttl(incoming.as_ref()); (i, false) } None => { record_vec.insert(0, incoming); // 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.matches(record.as_ref()) { true => { found = true; false } false => true, }); } found } /// Iterates all ADDR records and remove ones that expired. /// Returns the expired ones in a map of names and addresses. pub(crate) fn evict_expired_addr(&mut self, now: u64) -> HashMap> { let mut removed = HashMap::new(); self.addr.retain(|_, records| { records.retain(|addr| { let expired = addr.get_record().is_expired(now); if expired { if let Some(addr_record) = addr.any().downcast_ref::() { trace!("evict expired ADDR: {:?}", addr_record); removed .entry(addr.get_name().to_string()) .or_insert_with(HashSet::new) .insert(addr_record.address()); } } !expired }); !records.is_empty() }); removed } /// Evicts expired PTR and SRV, TXT records for each ty_domain in the cache, and /// returns the set of expired instance names for each ty_domain. /// /// An instance in the returned set indicates its PTR and/or SRV record has expired. pub(crate) fn evict_expired_services(&mut self, now: u64) -> HashMap> { let mut expired_instances = HashMap::new(); // Check all ty_domain in the cache by following all PTR records, regardless // if the ty_domain is actively queried or not. for (ty_domain, ptr_records) in self.ptr.iter_mut() { for ptr in ptr_records.iter() { if let Some(dns_ptr) = ptr.any().downcast_ref::() { let instance_name = dns_ptr.alias(); // evict expired SRV records of this instance if let Some(srv_records) = self.srv.get_mut(instance_name) { srv_records.retain(|srv| { let expired = srv.get_record().is_expired(now); if expired { trace!("expired SRV: {}: {:?}", ty_domain, srv); expired_instances .entry(ty_domain.to_string()) .or_insert_with(HashSet::new) .insert(srv.get_name().to_string()); } !expired }); } // evict expired TXT records of this instance if let Some(txt_records) = self.txt.get_mut(instance_name) { txt_records.retain(|txt| !txt.get_record().is_expired(now)) } } } // evict expired PTR records ptr_records.retain(|x| { let expired = x.get_record().is_expired(now); if expired { if let Some(dns_ptr) = x.any().downcast_ref::() { trace!("expired PTR: domain:{ty_domain} record: {:?}", dns_ptr); expired_instances .entry(ty_domain.to_string()) .or_insert_with(HashSet::new) .insert(dns_ptr.alias().to_string()); } } !expired }); } expired_instances } /// Checks refresh due for PTR records of `ty_domain`. /// Returns all updated refresh time. pub(crate) fn refresh_due_ptr(&mut self, ty_domain: &str) -> HashSet { let now = current_time_millis(); // Check all PTR records for this ty_domain. self.ptr .get_mut(ty_domain) .into_iter() .flatten() .filter_map(|record| record.updated_refresh_time(now)) .collect() } /// Returns the set of SRV instance names that are due for refresh /// for a `ty_domain`. pub(crate) fn refresh_due_srv(&mut self, ty_domain: &str) -> (HashSet, HashSet) { let now = current_time_millis(); let instances: Vec<_> = self .ptr .get(ty_domain) .into_iter() .flatten() .filter(|record| !record.get_record().is_expired(now)) .filter_map(|record| { record .any() .downcast_ref::() .map(|ptr| ptr.alias()) }) .collect(); // Check SRV records. let mut refresh_due = HashSet::new(); let mut new_timers = HashSet::new(); for instance in instances { let refresh_timers: HashSet = self .srv .get_mut(instance) .into_iter() .flatten() .filter_map(|record| record.updated_refresh_time(now)) .collect(); if !refresh_timers.is_empty() { refresh_due.insert(instance.to_string()); new_timers.extend(refresh_timers); } } (refresh_due, new_timers) } /// Returns the set of `host`, where refreshing the A / AAAA records is due /// for a `ty_domain`. pub(crate) fn refresh_due_hosts(&mut self, ty_domain: &str) -> (HashSet, HashSet) { let now = current_time_millis(); let instances: Vec<_> = self .ptr .get(ty_domain) .into_iter() .flatten() .filter(|record| !record.get_record().is_expired(now)) .filter_map(|record| { record .any() .downcast_ref::() .map(|ptr| ptr.alias()) }) .collect(); // Collect hostnames we have browsers for by SRV records. let mut hostnames_browsed = HashSet::new(); for instance in instances { let hosts: HashSet = self .srv .get(instance) .into_iter() .flatten() .filter_map(|record| { record .any() .downcast_ref::() .map(|srv| srv.host().to_string()) }) .collect(); hostnames_browsed.extend(hosts); } let mut refresh_due = HashSet::new(); let mut new_timers = HashSet::new(); for hostname in hostnames_browsed { let refresh_timers: HashSet = self .addr .get_mut(&hostname) .into_iter() .flatten() .filter_map(|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, IpAddr)> { let now = current_time_millis(); self.addr .get_mut(hostname) .into_iter() .flatten() .filter_map(|record| { let rec = record.get_record_mut(); if rec.is_expired(now) || !rec.refresh_due(now) { return None; } rec.refresh_no_more(); Some(( hostname.to_owned(), record.any().downcast_ref::().unwrap().address(), )) }) .collect() } /// Returns a list of Known Answer for a given question of `name` with `qtype`. /// The timestamp `now` is passed in to check TTL. /// /// Reference: RFC 6762 section 7.1 pub(crate) fn get_known_answers<'a>( &'a self, name: &str, qtype: RRType, now: u64, ) -> Vec<&'a DnsRecordBox> { 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.get_record().is_unique() && !r.get_record().halflife_passed(now)) .collect() } pub(crate) fn remove_addrs_on_disabled_intf(&mut self, disabled_intf: &Interface) { for (host, records) in self.addr.iter_mut() { records.retain(|record| { let Some(dns_addr) = record.any().downcast_ref::() else { return false; // invalid address record. }; // Remove the record if it is on this interface. if valid_ip_on_intf(&dns_addr.address(), disabled_intf) { debug!( "removing ADDR on disabled intf: {:?} host {host}", dns_addr.address() ); false } else { true } }); } } } /// Returns UNIX time in millis 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 } mdns-sd-0.13.3/src/dns_parser.rs000064400000000000000000002064451046102023000145650ustar 00000000000000//! 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]. #[cfg(feature = "logging")] use crate::log::trace; use crate::error::{e_fmt, Error, Result}; use std::{ any::Any, cmp, collections::HashMap, convert::TryInto, fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, str, time::SystemTime, }; /// DNS resource record types, stored as `u16`. Can do `as u16` when needed. /// /// See [RFC 1035 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2) #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] #[non_exhaustive] #[repr(u16)] pub enum RRType { /// DNS record type for IPv4 address A = 1, /// DNS record type for Canonical Name CNAME = 5, /// DNS record type for Pointer PTR = 12, /// DNS record type for Host Info HINFO = 13, /// DNS record type for Text (properties) TXT = 16, /// DNS record type for IPv6 address AAAA = 28, /// DNS record type for Service SRV = 33, /// DNS record type for Negative Responses NSEC = 47, /// DNS record type for any records (wildcard) ANY = 255, } impl RRType { /// Converts `u16` into `RRType` if possible. pub const fn from_u16(value: u16) -> Option { match value { 1 => Some(RRType::A), 5 => Some(RRType::CNAME), 12 => Some(RRType::PTR), 13 => Some(RRType::HINFO), 16 => Some(RRType::TXT), 28 => Some(RRType::AAAA), 33 => Some(RRType::SRV), 47 => Some(RRType::NSEC), 255 => Some(RRType::ANY), _ => None, } } } impl fmt::Display for RRType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RRType::A => write!(f, "TYPE_A"), RRType::CNAME => write!(f, "TYPE_CNAME"), RRType::PTR => write!(f, "TYPE_PTR"), RRType::HINFO => write!(f, "TYPE_HINFO"), RRType::TXT => write!(f, "TYPE_TXT"), RRType::AAAA => write!(f, "TYPE_AAAA"), RRType::SRV => write!(f, "TYPE_SRV"), RRType::NSEC => write!(f, "TYPE_NSEC"), RRType::ANY => write!(f, "TYPE_ANY"), } } } /// The class value for the Internet. pub const CLASS_IN: u16 = 1; pub const CLASS_MASK: u16 = 0x7FFF; /// Cache-flush bit: the most significant bit of the rrclass field of the resource record. pub const CLASS_CACHE_FLUSH: u16 = 0x8000; /// Max size of UDP datagram payload. /// /// It is calculated as: 9000 bytes - IP header 20 bytes - UDP header 8 bytes. /// Reference: [RFC6762 section 17](https://datatracker.ietf.org/doc/html/rfc6762#section-17) pub const MAX_MSG_ABSOLUTE: usize = 8972; const MSG_HEADER_LEN: usize = 12; // Definitions for DNS message header "flags" field // // The "flags" field is 16-bit long, in this format: // (RFC 1035 section 4.1.1) // // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // |QR| Opcode |AA|TC|RD|RA| Z | RCODE | // pub const FLAGS_QR_MASK: u16 = 0x8000; // mask for query/response bit /// Flag bit to indicate a query pub const FLAGS_QR_QUERY: u16 = 0x0000; /// Flag bit to indicate a response pub const FLAGS_QR_RESPONSE: u16 = 0x8000; /// Flag bit for Authoritative Answer pub const FLAGS_AA: u16 = 0x0400; /// mask for TC(Truncated) bit /// /// 2024-08-10: currently this flag is only supported on the querier side, /// not supported on the responder side. I.e. the responder only /// handles the first packet and ignore this bit. Since the /// additional packets have 0 questions, the processing of them /// is no-op. /// In practice, this means the responder supports Known-Answer /// only with single packet, not multi-packet. The querier supports /// both single packet and multi-packet. pub const FLAGS_TC: u16 = 0x0200; /// A convenience type alias for DNS record trait objects. pub type DnsRecordBox = Box; impl Clone for DnsRecordBox { fn clone(&self) -> Self { self.clone_box() } } const U16_SIZE: usize = 2; /// Returns `RRType` for a given IP address. #[inline] pub const fn ip_address_rr_type(address: &IpAddr) -> RRType { match address { IpAddr::V4(_) => RRType::A, IpAddr::V6(_) => RRType::AAAA, } } #[derive(Eq, PartialEq, Debug, Clone)] pub struct DnsEntry { pub(crate) name: String, // always lower case. pub(crate) ty: RRType, class: u16, cache_flush: bool, } impl DnsEntry { const fn new(name: String, ty: RRType, class: u16) -> Self { Self { name, ty, class: class & CLASS_MASK, cache_flush: (class & CLASS_CACHE_FLUSH) != 0, } } } /// Common methods for all DNS entries: questions and resource records. pub trait DnsEntryExt: fmt::Debug { fn entry_name(&self) -> &str; fn entry_type(&self) -> RRType; } /// A DNS question entry #[derive(Debug)] pub struct DnsQuestion { pub(crate) entry: DnsEntry, } impl DnsEntryExt for DnsQuestion { fn entry_name(&self) -> &str { &self.entry.name } fn entry_type(&self) -> RRType { self.entry.ty } } /// A DNS Resource Record - like a DNS entry, but has a TTL. /// RFC: https://www.rfc-editor.org/rfc/rfc1035#section-3.2.1 /// https://www.rfc-editor.org/rfc/rfc1035#section-4.1.3 #[derive(Debug, Clone)] pub struct DnsRecord { pub(crate) entry: DnsEntry, ttl: u32, // in seconds, 0 means this record should not be cached created: u64, // UNIX time in millis expires: u64, // expires at this UNIX time in millis /// Support re-query an instance before its PTR record expires. /// See https://datatracker.ietf.org/doc/html/rfc6762#section-5.2 refresh: u64, // UNIX time in millis /// If conflict resolution decides to change the name, this is the new one. new_name: Option, } impl DnsRecord { fn new(name: &str, ty: RRType, class: u16, ttl: u32) -> Self { let created = current_time_millis(); // From RFC 6762 section 5.2: // "... The querier should plan to issue a query at 80% of the record // lifetime, and then if no answer is received, at 85%, 90%, and 95%." let refresh = get_expiration_time(created, ttl, 80); let expires = get_expiration_time(created, ttl, 100); Self { entry: DnsEntry::new(name.to_string(), ty, class), ttl, created, expires, refresh, new_name: None, } } pub const fn get_ttl(&self) -> u32 { self.ttl } pub const fn get_expire_time(&self) -> u64 { self.expires } pub const fn get_refresh_time(&self) -> u64 { self.refresh } pub const fn is_expired(&self, now: u64) -> bool { now >= self.expires } 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.refresh = get_expiration_time(self.created, self.ttl, 80); self.expires = get_expiration_time(self.created, self.ttl, 100); } /// 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; fn write(&self, packet: &mut DnsOutPacket); 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); } } /// Given `now`, if the record is due to refresh, this method updates the refresh time /// and returns the new refresh time. Otherwise, returns None. fn updated_refresh_time(&mut self, now: u64) -> Option { if self.get_record_mut().refresh_maybe(now) { Some(self.get_record().get_refresh_time()) } else { None } } /// Returns true if another record has matched content, /// and if its TTL is at least half of this record's. fn suppressed_by_answer(&self, other: &dyn DnsRecordExt) -> bool { self.matches(other) && (other.get_record().ttl > self.get_record().ttl / 2) } /// Required by RFC 6762 Section 7.1: Known-Answer Suppression. fn suppressed_by(&self, msg: &DnsIncoming) -> bool { for answer in msg.answers.iter() { if self.suppressed_by_answer(answer.as_ref()) { return true; } } false } fn clone_box(&self) -> DnsRecordBox; } /// Resource Record for IPv4 address or IPv6 address. #[derive(Debug, Clone)] pub struct DnsAddress { pub(crate) record: DnsRecord, address: IpAddr, } impl DnsAddress { pub fn new(name: &str, ty: RRType, class: u16, ttl: u32, address: IpAddr) -> Self { let record = DnsRecord::new(name, ty, class, ttl); Self { record, address } } pub fn address(&self) -> IpAddr { self.address } } 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) { match self.address { IpAddr::V4(addr) => packet.write_bytes(addr.octets().as_ref()), IpAddr::V6(addr) => packet.write_bytes(addr.octets().as_ref()), }; } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_a) = other.any().downcast_ref::() { return self.address == other_a.address && self.record.entry == other_a.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_a) = other.any().downcast_ref::() { return self.address == other_a.address; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { if let Some(other_a) = other.any().downcast_ref::() { self.address.cmp(&other_a.address) } else { cmp::Ordering::Greater } } fn rdata_print(&self) -> String { format!("{}", self.address) } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } /// 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) { packet.write_name(&self.alias); } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_ptr) = other.any().downcast_ref::() { return self.alias == other_ptr.alias && self.record.entry == other_ptr.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_ptr) = other.any().downcast_ref::() { return self.alias == other_ptr.alias; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { if let Some(other_ptr) = other.any().downcast_ref::() { self.alias.cmp(&other_ptr.alias) } else { cmp::Ordering::Greater } } fn rdata_print(&self) -> String { self.alias.clone() } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } /// 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) { packet.write_short(self.priority); packet.write_short(self.weight); packet.write_short(self.port); packet.write_name(&self.host); } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_svc) = other.any().downcast_ref::() { return self.host == other_svc.host && self.port == other_svc.port && self.weight == other_svc.weight && self.priority == other_svc.priority && self.record.entry == other_svc.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_srv) = other.any().downcast_ref::() { return self.host == other_srv.host && self.port == other_srv.port && self.weight == other_srv.weight && self.priority == other_srv.priority; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { let Some(other_srv) = other.any().downcast_ref::() else { return cmp::Ordering::Greater; }; // 1. compare `priority` match self .priority .to_be_bytes() .cmp(&other_srv.priority.to_be_bytes()) { cmp::Ordering::Equal => { // 2. compare `weight` match self .weight .to_be_bytes() .cmp(&other_srv.weight.to_be_bytes()) { cmp::Ordering::Equal => { // 3. compare `port`. match self.port.to_be_bytes().cmp(&other_srv.port.to_be_bytes()) { cmp::Ordering::Equal => self.host.cmp(&other_srv.host), not_equal => not_equal, } } not_equal => not_equal, } } not_equal => not_equal, } } fn rdata_print(&self) -> String { format!( "priority: {}, weight: {}, port: {}, host: {}", self.priority, self.weight, self.port, self.host ) } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } /// Resource Record for a DNS TXT record. /// /// From [RFC 6763 section 6]: /// /// The format of each constituent string within the DNS TXT record is a /// single length byte, followed by 0-255 bytes of text data. /// /// DNS-SD uses DNS TXT records to store arbitrary key/value pairs /// conveying additional information about the named service. Each /// key/value pair is encoded as its own constituent string within the /// DNS TXT record, in the form "key=value" (without the quotation /// marks). Everything up to the first '=' character is the key (Section /// 6.4). Everything after the first '=' character to the end of the /// string (including subsequent '=' characters, if any) is the value #[derive(Clone)] pub struct DnsTxt { pub(crate) record: DnsRecord, text: Vec, } impl DnsTxt { pub fn new(name: &str, class: u16, ttl: u32, text: Vec) -> Self { let record = DnsRecord::new(name, RRType::TXT, class, ttl); Self { record, text } } pub fn text(&self) -> &[u8] { &self.text } } impl DnsRecordExt for DnsTxt { fn get_record(&self) -> &DnsRecord { &self.record } fn get_record_mut(&mut self) -> &mut DnsRecord { &mut self.record } fn write(&self, packet: &mut DnsOutPacket) { packet.write_bytes(&self.text); } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_txt) = other.any().downcast_ref::() { return self.text == other_txt.text && self.record.entry == other_txt.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_txt) = other.any().downcast_ref::() { return self.text == other_txt.text; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { if let Some(other_txt) = other.any().downcast_ref::() { self.text.cmp(&other_txt.text) } else { cmp::Ordering::Greater } } fn rdata_print(&self) -> String { format!("{:?}", decode_txt(&self.text)) } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } impl fmt::Debug for DnsTxt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let properties = decode_txt(&self.text); write!( f, "DnsTxt {{ record: {:?}, text: {:?} }}", self.record, properties ) } } // Convert from DNS TXT record content to key/value pairs fn decode_txt(txt: &[u8]) -> Vec { let mut properties = Vec::new(); let mut offset = 0; while offset < txt.len() { let length = txt[offset] as usize; if length == 0 { break; // reached the end } offset += 1; // move over the length byte let offset_end = offset + length; if offset_end > txt.len() { trace!("ERROR: DNS TXT: size given for property is out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len()); break; // Skipping the rest of the record content, as the size for this property would already be out of range. } let kv_bytes = &txt[offset..offset_end]; // split key and val using the first `=` let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else( || (kv_bytes.to_vec(), None), |idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())), ); // Make sure the key can be stored in UTF-8. match String::from_utf8(k) { Ok(k_string) => { properties.push(TxtProperty { key: k_string, val: v, }); } Err(e) => trace!("ERROR: convert to String from key: {}", e), } offset += length; } properties } /// Represents a property in a TXT record. #[derive(Clone, PartialEq, Eq)] pub struct TxtProperty { /// The name of the property. The original cases are kept. key: String, /// RFC 6763 says values are bytes, not necessarily UTF-8. /// It is also possible that there is no value, in which case /// the key is a boolean key. val: Option>, } impl TxtProperty { /// Returns the value of a property as str. pub fn val_str(&self) -> &str { self.val .as_ref() .map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default()) } } /// Supports constructing from a tuple. impl From<&(K, V)> for TxtProperty where K: ToString, V: ToString, { fn from(prop: &(K, V)) -> Self { Self { key: prop.0.to_string(), val: Some(prop.1.to_string().into_bytes()), } } } impl From<(K, V)> for TxtProperty where K: ToString, V: AsRef<[u8]>, { fn from(prop: (K, V)) -> Self { Self { key: prop.0.to_string(), val: Some(prop.1.as_ref().into()), } } } /// Support a property that has no value. impl From<&str> for TxtProperty { fn from(key: &str) -> Self { Self { key: key.to_string(), val: None, } } } impl fmt::Display for TxtProperty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}={}", self.key, self.val_str()) } } /// Mimic the default debug output for a struct, with a twist: /// - If self.var is UTF-8, will output it as a string in double quotes. /// - If self.var is not UTF-8, will output its bytes as in hex. impl fmt::Debug for TxtProperty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let val_string = self.val.as_ref().map_or_else( || "None".to_string(), |v| { std::str::from_utf8(&v[..]).map_or_else( |_| format!("Some({})", u8_slice_to_hex(&v[..])), |s| format!("Some(\"{}\")", s), ) }, ); write!( f, "TxtProperty {{key: \"{}\", val: {}}}", &self.key, &val_string, ) } } const HEX_TABLE: [char; 16] = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', ]; /// Create a hex string from `slice`, with a "0x" prefix. /// /// For example, [1u8, 2u8] -> "0x0102" fn u8_slice_to_hex(slice: &[u8]) -> String { let mut hex = String::with_capacity(slice.len() * 2 + 2); hex.push_str("0x"); for b in slice { hex.push(HEX_TABLE[(b >> 4) as usize]); hex.push(HEX_TABLE[(b & 0x0F) as usize]); } hex } /// A DNS host information record #[derive(Debug, Clone)] struct DnsHostInfo { record: DnsRecord, cpu: String, os: String, } impl DnsHostInfo { fn new(name: &str, ty: RRType, class: u16, ttl: u32, cpu: String, os: String) -> Self { let record = DnsRecord::new(name, ty, class, ttl); Self { record, cpu, os } } } impl DnsRecordExt for DnsHostInfo { fn get_record(&self) -> &DnsRecord { &self.record } fn get_record_mut(&mut self) -> &mut DnsRecord { &mut self.record } fn write(&self, packet: &mut DnsOutPacket) { println!("writing HInfo: cpu {} os {}", &self.cpu, &self.os); packet.write_bytes(self.cpu.as_bytes()); packet.write_bytes(self.os.as_bytes()); } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_hinfo) = other.any().downcast_ref::() { return self.cpu == other_hinfo.cpu && self.os == other_hinfo.os && self.record.entry == other_hinfo.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_hinfo) = other.any().downcast_ref::() { return self.cpu == other_hinfo.cpu && self.os == other_hinfo.os; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { if let Some(other_hinfo) = other.any().downcast_ref::() { match self.cpu.cmp(&other_hinfo.cpu) { cmp::Ordering::Equal => self.os.cmp(&other_hinfo.os), ordering => ordering, } } else { cmp::Ordering::Greater } } fn rdata_print(&self) -> String { format!("cpu: {}, os: {}", self.cpu, self.os) } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } /// Resource Record for negative responses /// /// [RFC4034 section 4.1](https://datatracker.ietf.org/doc/html/rfc4034#section-4.1) /// and /// [RFC6762 section 6.1](https://datatracker.ietf.org/doc/html/rfc6762#section-6.1) #[derive(Debug, Clone)] pub struct DnsNSec { record: DnsRecord, next_domain: String, type_bitmap: Vec, } impl DnsNSec { pub fn new( name: &str, class: u16, ttl: u32, next_domain: String, type_bitmap: Vec, ) -> Self { let record = DnsRecord::new(name, RRType::NSEC, class, ttl); Self { record, next_domain, type_bitmap, } } /// Returns the types marked by `type_bitmap` pub fn _types(&self) -> Vec { // From RFC 4034: 4.1.2 The Type Bit Maps Field // https://datatracker.ietf.org/doc/html/rfc4034#section-4.1.2 // // Each bitmap encodes the low-order 8 bits of RR types within the // window block, in network bit order. The first bit is bit 0. For // window block 0, bit 1 corresponds to RR type 1 (A), bit 2 corresponds // to RR type 2 (NS), and so forth. let mut bit_num = 0; let mut results = Vec::new(); for byte in self.type_bitmap.iter() { let mut bit_mask: u8 = 0x80; // for bit 0 in network bit order // check every bit in this byte, one by one. for _ in 0..8 { if (byte & bit_mask) != 0 { results.push(bit_num); } bit_num += 1; bit_mask >>= 1; // mask for the next bit } } results } } impl DnsRecordExt for DnsNSec { fn get_record(&self) -> &DnsRecord { &self.record } fn get_record_mut(&mut self) -> &mut DnsRecord { &mut self.record } fn write(&self, packet: &mut DnsOutPacket) { packet.write_bytes(self.next_domain.as_bytes()); packet.write_bytes(&self.type_bitmap); } fn any(&self) -> &dyn Any { self } fn matches(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_record) = other.any().downcast_ref::() { return self.next_domain == other_record.next_domain && self.type_bitmap == other_record.type_bitmap && self.record.entry == other_record.record.entry; } false } fn rrdata_match(&self, other: &dyn DnsRecordExt) -> bool { if let Some(other_record) = other.any().downcast_ref::() { return self.next_domain == other_record.next_domain && self.type_bitmap == other_record.type_bitmap; } false } fn compare_rdata(&self, other: &dyn DnsRecordExt) -> cmp::Ordering { if let Some(other_nsec) = other.any().downcast_ref::() { match self.next_domain.cmp(&other_nsec.next_domain) { cmp::Ordering::Equal => self.type_bitmap.cmp(&other_nsec.type_bitmap), ordering => ordering, } } else { cmp::Ordering::Greater } } fn rdata_print(&self) -> String { format!( "next_domain: {}, type_bitmap len: {}", self.next_domain, self.type_bitmap.len() ) } fn clone_box(&self) -> DnsRecordBox { Box::new(self.clone()) } } #[derive(PartialEq)] enum PacketState { Init = 0, Finished = 1, } /// A single packet for outgoing DNS message. pub struct DnsOutPacket { /// All bytes in `data` concatenated is the actual packet on the wire. data: Vec>, /// Current logical size of the packet. It starts with the size of the mandatory header. size: usize, /// An internal state, not defined by DNS. state: PacketState, /// k: name, v: offset names: HashMap, } impl DnsOutPacket { fn new() -> Self { Self { data: Vec::new(), size: MSG_HEADER_LEN, // Header is mandatory. state: PacketState::Init, names: HashMap::new(), } } pub fn size(&self) -> usize { self.size } pub fn to_bytes(&self) -> Vec { self.data.concat() } fn write_question(&mut self, question: &DnsQuestion) { self.write_name(&question.entry.name); self.write_short(question.entry.ty as u16); self.write_short(question.entry.class); } /// Writes a record (answer, authoritative answer, additional) /// Returns false if the packet exceeds the max size with this record, nothing is written to the packet. /// otherwise returns true. fn write_record(&mut self, record_ext: &dyn DnsRecordExt, now: u64) -> bool { let start_data_length = self.data.len(); 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)); } let index = self.data.len(); // Adjust size for the short we will write before this record self.size += 2; record_ext.write(self); self.size -= 2; let length: usize = self.data[index..].iter().map(|x| x.len()).sum(); self.insert_short(index, length as u16); if self.size > MAX_MSG_ABSOLUTE { self.data.truncate(start_data_length); self.size = start_size; self.state = PacketState::Finished; return false; } true } pub(crate) fn insert_short(&mut self, index: usize, value: u16) { self.data.insert(index, value.to_be_bytes().to_vec()); self.size += 2; } // 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. fn write_name(&mut self, name: &str) { // ignore the ending "." if exists let end = name.len(); let end = if end > 0 && &name[end - 1..] == "." { end - 1 } else { end }; let mut here = 0; while here < end { const POINTER_MASK: u16 = 0xC000; let remaining = &name[here..end]; // Check if 'remaining' already appeared in this message match self.names.get(remaining) { Some(offset) => { let pointer = *offset | POINTER_MASK; self.write_short(pointer); // println!( // "written pointer {} ({}) for {}", // pointer, // pointer ^ POINTER_MASK, // remaining // ); break; } None => { // Remember the remaining parts so we can point to it self.names.insert(remaining.to_string(), self.size as u16); // println!("set offset {} for {}", self.size, remaining); // Find the current label to write into the packet let stop = remaining.find('.').map_or(end, |i| here + i); let label = &name[here..stop]; self.write_utf8(label); here = stop + 1; // move past the current label } } if here >= end { self.write_byte(0); // name ends with 0 if not using a pointer } } } fn write_utf8(&mut self, utf: &str) { assert!(utf.len() < 64); self.write_byte(utf.len() as u8); self.write_bytes(utf.as_bytes()); } fn write_bytes(&mut self, s: &[u8]) { self.data.push(s.to_vec()); self.size += s.len(); } fn write_u32(&mut self, int: u32) { self.data.push(int.to_be_bytes().to_vec()); self.size += 4; } fn write_short(&mut self, short: u16) { self.data.push(short.to_be_bytes().to_vec()); self.size += 2; } fn write_byte(&mut self, byte: u8) { self.data.push(vec![byte]); self.size += 1; } /// 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, q_count: u16, a_count: u16, auth_count: u16, addi_count: u16, ) { self.insert_short(0, addi_count); self.insert_short(0, auth_count); self.insert_short(0, a_count); self.insert_short(0, q_count); self.insert_short(0, flags); self.insert_short(0, id); // Adjust the size as it was already initialized to include the header. self.size -= MSG_HEADER_LEN; self.state = PacketState::Finished; } } /// Representation of one outgoing DNS message that could be sent in one or more packet(s). pub struct DnsOutgoing { flags: u16, id: u16, multicast: bool, questions: Vec, answers: Vec<(DnsRecordBox, u64)>, authorities: Vec, additionals: Vec, known_answer_count: i64, // for internal maintenance only } impl DnsOutgoing { pub fn new(flags: u16) -> Self { Self { flags, id: 0, multicast: true, questions: Vec::new(), answers: Vec::new(), authorities: Vec::new(), additionals: Vec::new(), known_answer_count: 0, } } pub fn questions(&self) -> &[DnsQuestion] { &self.questions } 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; } pub const fn is_query(&self) -> bool { (self.flags & FLAGS_QR_MASK) == FLAGS_QR_QUERY } const fn is_response(&self) -> bool { (self.flags & FLAGS_QR_MASK) == FLAGS_QR_RESPONSE } // 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(Box::new(answer)); } /// 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); } /// 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((Box::new(answer), now)); return true; } false } 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); } /// Returns a list of actual DNS packet data to be sent on the wire. pub fn to_data_on_wire(&self) -> Vec> { let packet_list = self.to_packets(); packet_list.iter().map(|p| p.data.concat()).collect() } /// Encode self into one or more packets. pub fn to_packets(&self) -> Vec { let mut packet_list = Vec::new(); let mut packet = DnsOutPacket::new(); let mut question_count = self.questions.len() as u16; let mut answer_count = 0; let mut auth_count = 0; let mut addi_count = 0; let id = if self.multicast { 0 } else { self.id }; for question in self.questions.iter() { packet.write_question(question); } for (answer, time) in self.answers.iter() { if packet.write_record(answer.as_ref(), *time) { answer_count += 1; } } for auth in self.authorities.iter() { auth_count += u16::from(packet.write_record(auth.as_ref(), 0)); } for addi in self.additionals.iter() { if packet.write_record(addi.as_ref(), 0) { addi_count += 1; continue; } // No more processing for response packets. if self.is_response() { break; } // For query, the current packet exceeds its max size due to known answers, // need to truncate. // finish the current packet first. packet.write_header( id, self.flags | FLAGS_TC, question_count, answer_count, auth_count, addi_count, ); packet_list.push(packet); // create a new packet and reset counts. packet = DnsOutPacket::new(); packet.write_record(addi.as_ref(), 0); question_count = 0; answer_count = 0; auth_count = 0; addi_count = 1; } packet.write_header( id, self.flags, question_count, answer_count, auth_count, addi_count, ); packet_list.push(packet); packet_list } } /// An incoming DNS message. It could be a query or a response. #[derive(Debug)] pub struct DnsIncoming { offset: usize, data: Vec, questions: Vec, answers: Vec, authorities: Vec, additional: Vec, id: u16, flags: u16, num_questions: u16, num_answers: u16, num_authorities: u16, num_additionals: u16, } impl DnsIncoming { pub fn new(data: Vec) -> Result { let mut incoming = Self { offset: 0, data, questions: Vec::new(), answers: Vec::new(), authorities: Vec::new(), additional: Vec::new(), id: 0, flags: 0, num_questions: 0, num_answers: 0, num_authorities: 0, num_additionals: 0, }; /* 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 +---------------------+ */ incoming.read_header()?; incoming.read_questions()?; incoming.read_answers()?; incoming.read_authorities()?; incoming.read_additional()?; Ok(incoming) } pub fn id(&self) -> u16 { self.id } pub fn questions(&self) -> &[DnsQuestion] { &self.questions } pub fn answers(&self) -> &[DnsRecordBox] { &self.answers } pub fn authorities(&self) -> &[DnsRecordBox] { &self.authorities } pub fn additionals(&self) -> &[DnsRecordBox] { &self.additional } pub fn answers_mut(&mut self) -> &mut Vec { &mut self.answers } pub fn authorities_mut(&mut self) -> &mut Vec { &mut self.authorities } pub fn additionals_mut(&mut self) -> &mut Vec { &mut self.additional } pub fn all_records(self) -> impl Iterator { self.answers .into_iter() .chain(self.authorities) .chain(self.additional) } pub fn num_additionals(&self) -> u16 { self.num_additionals } pub fn num_authorities(&self) -> u16 { self.num_authorities } pub fn num_questions(&self) -> u16 { self.num_questions } pub const fn is_query(&self) -> bool { (self.flags & FLAGS_QR_MASK) == FLAGS_QR_QUERY } pub const fn is_response(&self) -> bool { (self.flags & FLAGS_QR_MASK) == FLAGS_QR_RESPONSE } fn read_header(&mut self) -> Result<()> { if self.data.len() < MSG_HEADER_LEN { return Err(e_fmt!( "DNS incoming: header is too short: {} bytes", self.data.len() )); } let data = &self.data[0..]; self.id = u16_from_be_slice(&data[..2]); self.flags = u16_from_be_slice(&data[2..4]); self.num_questions = u16_from_be_slice(&data[4..6]); self.num_answers = u16_from_be_slice(&data[6..8]); self.num_authorities = u16_from_be_slice(&data[8..10]); self.num_additionals = u16_from_be_slice(&data[10..12]); self.offset = MSG_HEADER_LEN; trace!( "read_header: id {}, {} questions {} answers {} authorities {} additionals", self.id, self.num_questions, self.num_answers, self.num_authorities, self.num_additionals ); Ok(()) } fn read_questions(&mut self) -> Result<()> { trace!("read_questions: {}", &self.num_questions); for i in 0..self.num_questions { let name = self.read_name()?; let data = &self.data[self.offset..]; if data.len() < 4 { return Err(Error::Msg(format!( "DNS incoming: question idx {} too short: {}", i, data.len() ))); } let ty = u16_from_be_slice(&data[..2]); let class = u16_from_be_slice(&data[2..4]); self.offset += 4; let Some(rr_type) = RRType::from_u16(ty) else { return Err(Error::Msg(format!( "DNS incoming: question idx {} qtype unknown: {}", i, ty ))); }; self.questions.push(DnsQuestion { entry: DnsEntry::new(name, rr_type, class), }); } Ok(()) } fn read_answers(&mut self) -> Result<()> { self.answers = self.read_rr_records(self.num_answers)?; Ok(()) } fn read_authorities(&mut self) -> Result<()> { self.authorities = self.read_rr_records(self.num_authorities)?; Ok(()) } fn read_additional(&mut self) -> Result<()> { self.additional = self.read_rr_records(self.num_additionals)?; Ok(()) } /// Decodes a sequence of RR records (in answers, authorities and additionals). fn read_rr_records(&mut self, count: u16) -> Result> { trace!("read_rr_records: {}", count); let mut rr_records = Vec::new(); // RFC 1035: https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.1 // // All RRs have the same top level format shown below: // 1 1 1 1 1 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // | | // / / // / NAME / // | | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // | TYPE | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // | CLASS | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // | TTL | // | | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // | RDLENGTH | // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| // / RDATA / // / / // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ // Muse have at least TYPE, CLASS, TTL, RDLENGTH fields: 10 bytes. const RR_HEADER_REMAIN: usize = 10; for _ in 0..count { let name = self.read_name()?; let slice = &self.data[self.offset..]; if slice.len() < RR_HEADER_REMAIN { return Err(Error::Msg(format!( "read_others: RR '{}' is too short after name: {} bytes", &name, slice.len() ))); } let ty = u16_from_be_slice(&slice[..2]); let class = u16_from_be_slice(&slice[2..4]); let mut ttl = u32_from_be_slice(&slice[4..8]); if ttl == 0 && self.is_response() { // RFC 6762 section 10.1: // "...Queriers receiving a Multicast DNS response with a TTL of zero SHOULD // NOT immediately delete the record from the cache, but instead record // a TTL of 1 and then delete the record one second later." // See https://datatracker.ietf.org/doc/html/rfc6762#section-10.1 ttl = 1; } let rdata_len = u16_from_be_slice(&slice[8..10]) as usize; self.offset += RR_HEADER_REMAIN; let next_offset = self.offset + rdata_len; // Sanity check for RDATA length. if next_offset > self.data.len() { return Err(Error::Msg(format!( "RR {name} RDATA length {rdata_len} is invalid: remain data len: {}", self.data.len() - self.offset ))); } // decode RDATA based on the record type. let rec: Option = match RRType::from_u16(ty) { None => None, Some(rr_type) => match rr_type { RRType::CNAME | RRType::PTR => Some(Box::new(DnsPointer::new( &name, rr_type, class, ttl, self.read_name()?, ))), RRType::TXT => Some(Box::new(DnsTxt::new( &name, class, ttl, self.read_vec(rdata_len), ))), RRType::SRV => Some(Box::new(DnsSrv::new( &name, class, ttl, self.read_u16()?, self.read_u16()?, self.read_u16()?, self.read_name()?, ))), RRType::HINFO => Some(Box::new(DnsHostInfo::new( &name, rr_type, class, ttl, self.read_char_string(), self.read_char_string(), ))), RRType::A => Some(Box::new(DnsAddress::new( &name, rr_type, class, ttl, self.read_ipv4().into(), ))), RRType::AAAA => Some(Box::new(DnsAddress::new( &name, rr_type, class, ttl, self.read_ipv6().into(), ))), RRType::NSEC => Some(Box::new(DnsNSec::new( &name, class, ttl, self.read_name()?, self.read_type_bitmap()?, ))), _ => None, }, }; if let Some(record) = rec { trace!("read_rr_records: {:?}", &record); rr_records.push(record); } else { trace!("Unsupported DNS record type: {} name: {}", ty, &name); self.offset += rdata_len; } // sanity check. if self.offset != next_offset { return Err(Error::Msg(format!( "read_rr_records: decode offset error for RData type {} offset: {} expected offset: {}", ty, self.offset, next_offset, ))); } } Ok(rr_records) } fn read_char_string(&mut self) -> String { let length = self.data[self.offset]; self.offset += 1; self.read_string(length as usize) } fn read_u16(&mut self) -> Result { let slice = &self.data[self.offset..]; if slice.len() < U16_SIZE { return Err(Error::Msg(format!( "read_u16: slice len is only {}", slice.len() ))); } let num = u16_from_be_slice(&slice[..U16_SIZE]); self.offset += U16_SIZE; Ok(num) } /// Reads the "Type Bit Map" block for a DNS NSEC record. fn read_type_bitmap(&mut self) -> Result> { // From RFC 6762: 6.1. Negative Responses // https://datatracker.ietf.org/doc/html/rfc6762#section-6.1 // o The Type Bit Map block number is 0. // o The Type Bit Map block length byte is a value in the range 1-32. // o The Type Bit Map data is 1-32 bytes, as indicated by length // byte. // Sanity check: at least 2 bytes to read. if self.data.len() < self.offset + 2 { return Err(Error::Msg(format!( "DnsIncoming is too short: {} at NSEC Type Bit Map offset {}", self.data.len(), self.offset ))); } let block_num = self.data[self.offset]; self.offset += 1; if block_num != 0 { return Err(Error::Msg(format!( "NSEC block number is not 0: {}", block_num ))); } let block_len = self.data[self.offset] as usize; if !(1..=32).contains(&block_len) { return Err(Error::Msg(format!( "NSEC block length must be in the range 1-32: {}", block_len ))); } self.offset += 1; let end = self.offset + block_len; if end > self.data.len() { return Err(Error::Msg(format!( "NSEC block overflow: {} over RData len {}", end, self.data.len() ))); } let bitmap = self.data[self.offset..end].to_vec(); self.offset += block_len; Ok(bitmap) } fn read_vec(&mut self, length: usize) -> Vec { let v = self.data[self.offset..self.offset + length].to_vec(); self.offset += length; v } fn read_ipv4(&mut self) -> Ipv4Addr { let bytes: [u8; 4] = (&self.data)[self.offset..self.offset + 4] .try_into() .unwrap(); self.offset += bytes.len(); Ipv4Addr::from(bytes) } fn read_ipv6(&mut self) -> Ipv6Addr { let bytes: [u8; 16] = (&self.data)[self.offset..self.offset + 16] .try_into() .unwrap(); self.offset += bytes.len(); Ipv6Addr::from(bytes) } fn read_string(&mut self, length: usize) -> String { let s = str::from_utf8(&self.data[self.offset..self.offset + length]).unwrap(); self.offset += length; s.to_string() } /// Reads a domain name at the current location of `self.data`. /// /// See https://datatracker.ietf.org/doc/html/rfc1035#section-3.1 for /// domain name encoding. fn read_name(&mut self) -> Result { let data = &self.data[..]; let start_offset = self.offset; let mut offset = start_offset; let mut name = "".to_string(); let mut at_end = false; // 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_name: offset: {} data len {}. DnsIncoming: {:?}", offset, data.len(), self ))); } let length = data[offset]; // From RFC1035: // "...Since every domain name ends with the null label of // the root, a domain name is terminated by a length byte of zero." if length == 0 { if !at_end { self.offset = offset + 1; } break; // 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_name: ending {} exceeds data length {}", ending, data.len() ))); } name += str::from_utf8(&data[offset..ending]) .map_err(|e| Error::Msg(format!("read_name: from_utf8: {}", e)))?; name += "."; offset += length as usize; } 0xC0 => { // Message compression. // See https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4 let slice = &data[offset..]; if slice.len() < U16_SIZE { return Err(Error::Msg(format!( "read_name: u16 slice len is only {}", slice.len() ))); } let pointer = (u16_from_be_slice(slice) ^ 0xC000) as usize; if pointer >= start_offset { // Error: could trigger an infinite loop. return Err(Error::Msg(format!( "Invalid name compression: pointer {} must be less than the start offset {}", &pointer, &start_offset ))); } // A pointer marks the end of a domain name. if !at_end { self.offset = offset + U16_SIZE; at_end = true; } offset = pointer; } _ => { return Err(Error::Msg(format!( "Bad name with invalid length: 0x{:x} offset {}, data (so far): {:x?}", length, offset, &data[..offset] ))); } }; } Ok(name) } } /// Returns UNIX time in millis fn current_time_millis() -> u64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("failed to get current UNIX time") .as_millis() as u64 } 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 * percent * 10) as u64 } mdns-sd-0.13.3/src/error.rs000064400000000000000000000016771046102023000135560ustar 00000000000000use std::fmt; /// A basic error type from this library. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Error { /// Like a classic EAGAIN. The receiver should retry. Again, /// 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"), } } } impl std::error::Error for Error {} /// One and only `Result` type from this library crate. pub type Result = core::result::Result; /// A simple macro to report all kinds of errors. macro_rules! e_fmt { ($($arg:tt)+) => { Error::Msg(format!($($arg)+)) }; } pub(crate) use e_fmt; mdns-sd-0.13.3/src/lib.rs000064400000000000000000000136041046102023000131640ustar 00000000000000//! A small and safe library for Multicast DNS-SD (Service Discovery). //! //! This library creates one new thread to run a mDNS daemon, and exposes //! its API that interacts with the daemon via a //! [`flume`](https://crates.io/crates/flume) channel. The channel supports //! both `recv()` and `recv_async()`. //! //! For example, a client querying (browsing) a service behaves like this: //!```text //! Client mDNS daemon thread //! | | starts its run-loop. //! | --- Browse --> | //! | | detects services //! | | finds service instance A //! | <-- Found A -- | //! | ... | resolves service A //! | <-- Resolved A -- | //! | ... | //!``` //! All commands in the public API are sent to the daemon using the unblocking `try_send()` //! so that the caller can use it with both sync and async code, with no dependency on any //! particular async runtimes. //! //! # Usage //! //! The user starts with creating a daemon by calling [`ServiceDaemon::new()`]. //! Then as a mDNS querier, the user would call [`browse`](`ServiceDaemon::browse`) to //! search for services, and/or as a mDNS responder, call [`register`](`ServiceDaemon::register`) //! to publish (i.e. announce) its own service. And, the daemon type can be cloned and passed //! around between threads. //! //! The user can also call [`resolve_hostname`](`ServiceDaemon::resolve_hostname`) to //! resolve a hostname to IP addresses using mDNS, regardless if the host publishes a service name. //! //! ## Example: a client querying for a service type. //! //! ```rust //! use mdns_sd::{ServiceDaemon, ServiceEvent}; //! //! // Create a daemon //! let mdns = ServiceDaemon::new().expect("Failed to create daemon"); //! //! // Browse for a service type. //! let service_type = "_mdns-sd-my-test._udp.local."; //! let receiver = mdns.browse(service_type).expect("Failed to browse"); //! //! // Receive the browse events in sync or async. Here is //! // an example of using a thread. Users can call `receiver.recv_async().await` //! // if running in async environment. //! std::thread::spawn(move || { //! while let Ok(event) = receiver.recv() { //! match event { //! ServiceEvent::ServiceResolved(info) => { //! println!("Resolved a new service: {}", info.get_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. //! //! ```rust //! use mdns_sd::{ServiceDaemon, ServiceInfo}; //! use std::collections::HashMap; //! //! // Create a daemon //! let mdns = ServiceDaemon::new().expect("Failed to create daemon"); //! //! // Create a service info. //! 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(); //! ``` //! //! # 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; pub use dns_parser::RRType; pub use error::{Error, Result}; pub use service_daemon::{ DaemonEvent, DaemonStatus, DnsNameChange, HostnameResolutionEvent, IfKind, Metrics, ServiceDaemon, ServiceEvent, UnregisterStatus, SERVICE_NAME_LEN_MAX_DEFAULT, VERIFY_TIMEOUT_DEFAULT, }; pub use service_info::{ AsIpAddrs, IntoTxtProperties, ResolvedService, ServiceInfo, TxtProperties, TxtProperty, }; /// A handler to receive messages from [ServiceDaemon]. Re-export from `flume` crate. pub use flume::Receiver; mdns-sd-0.13.3/src/service_daemon.rs000064400000000000000000004323101046102023000154000ustar 00000000000000//! Service daemon for mDNS Service Discovery. // How DNS-based Service Discovery works in a nutshell: // // (excerpt from RFC 6763) // .... that a particular service instance can be // described using a DNS SRV [RFC2782] and DNS TXT [RFC1035] record. // The SRV record has a name of the form ".." // and gives the target host and port where the service instance can be // reached. The DNS TXT record of the same name gives additional // information about this instance, in a structured form using key/value // pairs, described in Section 6. A client discovers the list of // available instances of a given service type using a query for a DNS // PTR [RFC1035] record with a name of the form ".", // which returns a set of zero or more names, which are the names of the // aforementioned DNS SRV/TXT record pairs. // // Some naming conventions in this source code: // // `ty_domain` refers to service type together with domain name, i.e. .. // Every consists of two labels: service itself and "_udp." or "_tcp". // See RFC 6763 section 7 Service Names. // for example: `_my-service._udp.local.` // // `fullname` refers to a full Service Instance Name, i.e. .. // for example: `my_home._my-service._udp.local.` // // In mDNS and DNS, the basic data structure is "Resource Record" (RR), where // in Service Discovery, the basic data structure is "Service Info". One Service Info // corresponds to a set of DNS Resource Records. #[cfg(feature = "logging")] use crate::log::{debug, trace}; use crate::{ dns_cache::{current_time_millis, DnsCache}, dns_parser::{ ip_address_rr_type, DnsAddress, DnsEntryExt, DnsIncoming, DnsOutgoing, DnsPointer, DnsRecordBox, DnsRecordExt, DnsSrv, DnsTxt, RRType, CLASS_CACHE_FLUSH, CLASS_IN, FLAGS_AA, FLAGS_QR_QUERY, FLAGS_QR_RESPONSE, MAX_MSG_ABSOLUTE, }, error::{e_fmt, Error, Result}, service_info::{ split_sub_domain, valid_ip_on_intf, DnsRegistry, Probe, ServiceInfo, ServiceStatus, }, Receiver, }; use flume::{bounded, Sender, TrySendError}; use if_addrs::{IfAddr, Interface}; use mio::{net::UdpSocket as MioUdpSocket, Poll}; use socket2::Socket; use std::{ cmp::{self, Reverse}, collections::{BinaryHeap, HashMap, HashSet}, fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket}, str, thread, time::Duration, vec, }; /// 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 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); 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; /// 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, CacheRefreshSRV, CacheRefreshAddr, KnownAnswerSuppression, } 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::CacheRefreshSRV => write!(f, "cache-refresh-srv"), Self::CacheRefreshAddr => write!(f, "cache-refresh-addr"), Self::KnownAnswerSuppression => write!(f, "known-answer-suppression"), } } } /// The metrics is a HashMap of (name_key, i64_value). /// The main purpose is to help monitoring the mDNS packet traffic. pub type Metrics = HashMap; const SIGNAL_SOCK_EVENT_KEY: usize = usize::MAX - 1; // avoid to overlap with zc.poll_ids /// A daemon thread for mDNS /// /// This struct provides a handle and an API to the daemon. It is cloneable. #[derive(Clone)] pub struct ServiceDaemon { /// Sender handle of the channel to the daemon. sender: Sender, /// Send to this addr to signal that a `Command` is coming. /// /// The daemon listens on this addr together with other mDNS sockets, /// to avoid busy polling the flume channel. If there is a way to poll /// the channel and mDNS sockets together, then this can be removed. signal_addr: SocketAddr, } impl ServiceDaemon { /// Creates a new daemon and spawns a thread to run the daemon. /// /// The daemon (re)uses the default mDNS port 5353. To keep it simple, we don't /// ask callers to set the port. pub fn new() -> Result { // Use port 0 to allow the system assign a random available port, // no need for a pre-defined port number. let signal_addr = SocketAddrV4::new(LOOPBACK_V4, 0); let signal_sock = UdpSocket::bind(signal_addr) .map_err(|e| e_fmt!("failed to create signal_sock for daemon: {}", e))?; // Get the socket with the OS chosen port let signal_addr = signal_sock .local_addr() .map_err(|e| e_fmt!("failed to get signal sock addr: {}", e))?; // Must be nonblocking so we can listen to it together with mDNS sockets. signal_sock .set_nonblocking(true) .map_err(|e| e_fmt!("failed to set nonblocking for signal socket: {}", e))?; let poller = Poll::new().map_err(|e| e_fmt!("failed to create mio Poll: {e}"))?; let (sender, receiver) = bounded(100); // Spawn the daemon thread let mio_sock = MioUdpSocket::from_std(signal_sock); thread::Builder::new() .name("mDNS_daemon".to_string()) .spawn(move || Self::daemon_thread(mio_sock, poller, receiver)) .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, e => e_fmt!("flume::channel::send failed: {}", e), })?; // 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. pub fn browse(&self, service_type: &str) -> Result> { check_domain_suffix(service_type)?; let (resp_s, resp_r) = bounded(10); self.send_cmd(Command::Browse(service_type.to_string(), 1, resp_s))?; Ok(resp_r) } /// Stops searching for a specific service type. /// /// When an error is returned, the caller should retry only when /// the error is `Error::Again`, otherwise should log and move on. 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. pub fn resolve_hostname( &self, hostname: &str, timeout: Option, ) -> Result> { check_hostname(hostname)?; let (resp_s, resp_r) = bounded(10); self.send_cmd(Command::ResolveHostname( hostname.to_string(), 1, resp_s, timeout, ))?; Ok(resp_r) } /// Stops querying for the ip addresses of a hostname. /// /// When an error is returned, the caller should retry only when /// the error is `Error::Again`, otherwise should log and move on. 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. 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)) } /// 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. /// /// When an error is returned, the caller should retry only when /// the error is `Error::Again`, otherwise should log and move on. pub fn unregister(&self, fullname: &str) -> Result> { let (resp_s, resp_r) = bounded(1); self.send_cmd(Command::Unregister(fullname.to_lowercase(), resp_s))?; Ok(resp_r) } /// Starts to monitor events from the daemon. /// /// Returns a channel [`Receiver`] of [`DaemonEvent`]. pub fn monitor(&self) -> Result> { let (resp_s, resp_r) = bounded(100); self.send_cmd(Command::Monitor(resp_s))?; Ok(resp_r) } /// Shuts down the daemon thread and returns a channel to receive the status. /// /// When an error is returned, the caller should retry only when /// the error is `Error::Again`, otherwise should log and move on. pub fn shutdown(&self) -> Result> { let (resp_s, resp_r) = bounded(1); self.send_cmd(Command::Exit(resp_s))?; Ok(resp_r) } /// Returns the status of the daemon. /// /// When an error is returned, the caller should retry only when /// the error is `Error::Again`, otherwise should consider the daemon /// stopped working and move on. pub fn status(&self) -> Result> { let (resp_s, resp_r) = bounded(1); if self.sender.is_disconnected() { resp_s .send(DaemonStatus::Shutdown) .map_err(|e| e_fmt!("failed to send daemon status to the client: {}", e))?; } else { self.send_cmd(Command::GetStatus(resp_s))?; } Ok(resp_r) } /// Returns a channel receiver for the metrics, e.g. input/output counters. /// /// The metrics returned is a snapshot. Hence the caller should call /// this method repeatedly if they want to monitor the metrics continuously. pub fn get_metrics(&self) -> Result> { let (resp_s, resp_r) = bounded(1); self.send_cmd(Command::GetMetrics(resp_s))?; Ok(resp_r) } /// Change the max length allowed for a service name. /// /// As RFC 6763 defines a length max for a service name, a user should not call /// this method unless they have to. See [`SERVICE_NAME_LEN_MAX_DEFAULT`]. /// /// `len_max` is capped at an internal limit, which is currently 30. pub fn set_service_name_len_max(&self, len_max: u8) -> Result<()> { const SERVICE_NAME_LEN_MAX_LIMIT: u8 = 30; // Double the default length max. if len_max > SERVICE_NAME_LEN_MAX_LIMIT { return Err(Error::Msg(format!( "service name length max {} is too large", len_max ))); } self.send_cmd(Command::SetOption(DaemonOption::ServiceNameLenMax(len_max))) } /// Include interfaces that match `if_kind` for this service daemon. /// /// For example: /// ```ignore /// daemon.enable_interface("en0")?; /// ``` pub fn enable_interface(&self, if_kind: impl IntoIfKindVec) -> Result<()> { let if_kind_vec = if_kind.into_vec(); self.send_cmd(Command::SetOption(DaemonOption::EnableInterface( if_kind_vec.kinds, ))) } /// Ignore/exclude interfaces that match `if_kind` for this daemon. /// /// For example: /// ```ignore /// daemon.disable_interface(IfKind::IPv6)?; /// ``` pub fn disable_interface(&self, if_kind: impl IntoIfKindVec) -> Result<()> { let if_kind_vec = if_kind.into_vec(); self.send_cmd(Command::SetOption(DaemonOption::DisableInterface( if_kind_vec.kinds, ))) } /// Enable or disable the loopback for locally sent multicast packets in IPv4. /// /// By default, multicast loop is enabled for IPv4. When disabled, a querier will not /// receive announcements from a responder on the same host. /// /// Reference: /// /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than /// the UNIX version of the IP_MULTICAST_LOOP option: /// /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." /// /// Which means, in order NOT to receive localhost announcements, you want to call /// this API on the querier side on Windows, but on the responder side on Unix. pub fn set_multicast_loop_v4(&self, on: bool) -> Result<()> { self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV4(on))) } /// Enable or disable the loopback for locally sent multicast packets in IPv6. /// /// By default, multicast loop is enabled for IPv6. When disabled, a querier will not /// receive announcements from a responder on the same host. /// /// Reference: /// /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than /// the UNIX version of the IP_MULTICAST_LOOP option: /// /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." /// /// Which means, in order NOT to receive localhost announcements, you want to call /// this API on the querier side on Windows, but on the responder side on Unix. pub fn set_multicast_loop_v6(&self, on: bool) -> Result<()> { self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV6(on))) } /// Proactively confirms whether a service instance still valid. /// /// This call will issue queries for a service instance's SRV record and Address records. /// /// For `timeout`, most users should use [VERIFY_TIMEOUT_DEFAULT] /// unless there is a reason not to follow RFC. /// /// If no response is received within `timeout`, the current resource /// records will be flushed, and if needed, `ServiceRemoved` event will be /// sent to active queriers. /// /// Reference: [RFC 6762](https://datatracker.ietf.org/doc/html/rfc6762#section-10.4) 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) { let zc = Zeroconf::new(signal_sock, poller); if let Some(cmd) = Self::run(zc, 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); } } } } /// 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 zc: Zeroconf, receiver: Receiver) -> Option { // Add the daemon's signal socket to the poller. if let Err(e) = zc.poller.registry().register( &mut zc.signal_sock, mio::Token(SIGNAL_SOCK_EVENT_KEY), mio::Interest::READABLE, ) { debug!("failed to add signal socket to the poller: {}", e); return None; } // Add mDNS sockets to the poller. for (intf, sock) in zc.intf_socks.iter_mut() { let key = Zeroconf::add_poll_impl(&mut zc.poll_ids, &mut zc.poll_id_count, intf.clone()); if let Err(e) = zc.poller .registry() .register(sock, mio::Token(key), mio::Interest::READABLE) { debug!("add socket of {:?} to poller: {e}", intf); return None; } } // Setup timer for IP checks. const IP_CHECK_INTERVAL_MILLIS: u64 = 30_000; let mut next_ip_check = current_time_millis() + IP_CHECK_INTERVAL_MILLIS; zc.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 = zc.peek_earliest_timer(); let timeout = earliest_timer.map(|timer| { // If `timer` already passed, set `timeout` to be 1ms. let millis = if timer > now { timer - now } else { 1 }; Duration::from_millis(millis) }); // Process incoming packets, command events and optional timeout. events.clear(); match zc.poller.poll(&mut events, timeout) { Ok(_) => zc.handle_poller_events(&events), Err(e) => debug!("failed to select from sockets: {}", e), } let now = current_time_millis(); // Remove the timer if already passed. if let Some(timer) = earliest_timer { if now >= timer { zc.pop_earliest_timer(); } } // Remove hostname resolvers with expired timeouts. for hostname in zc .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( &zc.hostname_resolvers, &hostname, HostnameResolutionEvent::SearchTimeout(hostname.to_owned()), ); call_hostname_resolution_listener( &zc.hostname_resolvers, &hostname, HostnameResolutionEvent::SearchStopped(hostname.to_owned()), ); zc.hostname_resolvers.remove(&hostname); } // process commands from the command channel while let Ok(command) = receiver.try_recv() { if matches!(command, Command::Exit(_)) { zc.status = DaemonStatus::Shutdown; return Some(command); } zc.exec_command(command, false); } // check for repeated commands and run them if their time is up. let mut i = 0; while i < zc.retransmissions.len() { if now >= zc.retransmissions[i].next_time { let rerun = zc.retransmissions.remove(i); zc.exec_command(rerun.command, true); } else { i += 1; } } // Refresh cached service records with active queriers zc.refresh_active_services(); // Refresh cached A/AAAA records with active queriers let mut query_count = 0; for (hostname, _sender) in zc.hostname_resolvers.iter() { for (hostname, ip_addr) in zc.cache.refresh_due_hostname_resolutions(hostname).iter() { zc.send_query(hostname, ip_address_rr_type(ip_addr)); query_count += 1; } } zc.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 = zc.cache.evict_expired_services(now); zc.notify_service_removal(expired_services); // Notify hostname listeners about the expired records. let expired_addrs = zc.cache.evict_expired_addr(now); for (hostname, addrs) in expired_addrs { call_hostname_resolution_listener( &zc.hostname_resolvers, &hostname, HostnameResolutionEvent::AddressesRemoved(hostname.clone(), addrs), ); let instances = zc.cache.get_instances_on_host(&hostname); let instance_set: HashSet = instances.into_iter().collect(); zc.resolve_updated_instances(&instance_set); } // Send out probing queries. zc.probing_handler(); // check IP changes. if now > next_ip_check { next_ip_check = now + IP_CHECK_INTERVAL_MILLIS; zc.check_ip_changes(); zc.add_timer(next_ip_check); } } } } /// Creates a new UDP socket that uses `intf` to send and recv multicast. fn new_socket_bind(intf: &Interface, should_loop: bool) -> Result { // Use the same socket for receiving and sending multicast packets. // Such socket has to bind to INADDR_ANY or IN6ADDR_ANY. let intf_ip = &intf.ip(); match intf_ip { IpAddr::V4(ip) => { let addr = SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), MDNS_PORT); let sock = new_socket(addr.into(), true)?; // Join mDNS group to receive packets. sock.join_multicast_v4(&GROUP_ADDR_V4, ip) .map_err(|e| e_fmt!("join multicast group on addr {}: {}", intf_ip, e))?; // Set IP_MULTICAST_IF to send packets. sock.set_multicast_if_v4(ip) .map_err(|e| e_fmt!("set multicast_if on addr {}: {}", ip, e))?; 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(); for packet in test_packets { sock.send_to(&packet, &multicast_addr) .map_err(|e| e_fmt!("send multicast packet on addr {}: {}", ip, e))?; } Ok(MioUdpSocket::from_std(UdpSocket::from(sock))) } 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)?; // Join mDNS group to receive packets. sock.join_multicast_v6(&GROUP_ADDR_V6, intf.index.unwrap_or(0)) .map_err(|e| e_fmt!("join multicast group on addr {}: {}", ip, e))?; // Set IPV6_MULTICAST_IF to send packets. sock.set_multicast_if_v6(intf.index.unwrap_or(0)) .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)". Ok(MioUdpSocket::from_std(UdpSocket::from(sock))) } } } /// Creates a new UDP socket to bind to `port` with REUSEPORT option. /// `non_block` indicates whether to set O_NONBLOCK for the socket. fn new_socket(addr: SocketAddr, non_block: bool) -> Result { let domain = match addr { SocketAddr::V4(_) => socket2::Domain::IPV4, SocketAddr::V6(_) => socket2::Domain::IPV6, }; let fd = Socket::new(domain, socket2::Type::DGRAM, None) .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)] // this is currently restricted to Unix's in socket2 fd.set_reuse_port(true) .map_err(|e| e_fmt!("set ReusePort failed: {}", 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, } /// Enum to represent the IP version. #[derive(Debug, Eq, Hash, PartialEq)] enum IpVersion { V4, V6, } /// A struct to track multicast send status for a network interface. #[derive(Debug, Eq, Hash, PartialEq)] struct MulticastSendTracker { intf_index: u32, ip_version: IpVersion, } /// Returns the multicast send tracker if the interface index is valid fn multicast_send_tracker(intf: &Interface) -> Option { match intf.index { Some(index) => { let ip_ver = match intf.addr { IfAddr::V4(_) => IpVersion::V4, IfAddr::V6(_) => IpVersion::V6, }; Some(MulticastSendTracker { intf_index: index, ip_version: ip_ver, }) } None => None, } } /// Specify kinds of interfaces. It is used to enable or to disable interfaces in the daemon. /// /// Note that for ergonomic reasons, `From<&str>` and `From` are implemented. #[derive(Debug, Clone)] #[non_exhaustive] pub enum IfKind { /// All interfaces. All, /// All IPv4 interfaces. IPv4, /// All IPv6 interfaces. IPv6, /// By the interface name, for example "en0" Name(String), /// By an IPv4 or IPv6 address. Addr(IpAddr), /// 127.0.0.1 (or anything in 127.0.0.0/8), disabled by default. /// /// Use [ServiceDaemon::enable_interface] to support registering services on loopback interfaces, /// which is required by some use cases (e.g., OSCQuery) that publish via mDNS. LoopbackV4, /// ::1/128, disabled by default. LoopbackV6, } impl IfKind { /// Checks if `intf` matches with this interface kind. 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(), } } } /// The first use case of specifying an interface was to /// use an interface name. Hence adding this for ergonomic reasons. impl From<&str> for IfKind { fn from(val: &str) -> Self { Self::Name(val.to_string()) } } impl From<&String> for IfKind { fn from(val: &String) -> Self { Self::Name(val.to_string()) } } /// Still for ergonomic reasons. impl From for IfKind { fn from(val: IpAddr) -> Self { Self::Addr(val) } } /// A list of `IfKind` that can be used to match interfaces. pub struct IfKindVec { kinds: Vec, } /// A trait that converts a type into a Vec of `IfKind`. pub trait IntoIfKindVec { fn into_vec(self) -> IfKindVec; } impl> IntoIfKindVec for T { fn into_vec(self) -> IfKindVec { let if_kind: IfKind = self.into(); IfKindVec { kinds: vec![if_kind], } } } impl> IntoIfKindVec for Vec { fn into_vec(self) -> IfKindVec { let kinds: Vec = self.into_iter().map(|x| x.into()).collect(); IfKindVec { kinds } } } /// Selection of interfaces. struct IfSelection { /// The interfaces to be selected. if_kind: IfKind, /// Whether the `if_kind` should be enabled or not. selected: bool, } /// A struct holding the state. It was inspired by `zeroconf` package in Python. struct Zeroconf { /// Local interfaces with sockets to recv/send on these interfaces. intf_socks: HashMap, /// Map poll id to Interface. poll_ids: HashMap, /// Next poll id value poll_id_count: usize, /// Local registered services, keyed by service full names. my_services: HashMap, /// Received DNS records. cache: DnsCache, /// Registered service records. dns_registry_map: HashMap, /// Active "Browse" commands. service_queriers: HashMap>, // /// Active "ResolveHostname" commands. /// /// The timestamps are set at the future timestamp when the command should timeout. hostname_resolvers: HashMap, Option)>, // /// All repeating transmissions. retransmissions: Vec, counters: Metrics, /// Waits for incoming packets. poller: Poll, /// Channels to notify events. monitors: Vec>, /// Options service_name_len_max: u8, /// All interface selections called to the daemon. if_selections: Vec, /// Socket for signaling. signal_sock: MioUdpSocket, /// Timestamps marking where we need another iteration of the run loop, /// to react to events like retransmissions, cache refreshes, interface IP address changes, etc. /// /// When the run loop goes through a single iteration, it will /// set its timeout to the earliest timer in this list. timers: BinaryHeap>, status: DaemonStatus, /// Service instances that are pending for resolving SRV and TXT. pending_resolves: HashSet, /// Service instances that are already resolved. resolved: HashSet, multicast_loop_v4: bool, multicast_loop_v6: bool, } impl Zeroconf { fn new(signal_sock: MioUdpSocket, poller: Poll) -> Self { // Get interfaces. let my_ifaddrs = my_ip_interfaces(false); // 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 intf_socks = HashMap::new(); let mut dns_registry_map = HashMap::new(); for intf in my_ifaddrs { let sock = match new_socket_bind(&intf, true) { Ok(s) => s, Err(e) => { trace!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); continue; } }; dns_registry_map.insert(intf.clone(), DnsRegistry::new()); intf_socks.insert(intf, sock); } let monitors = Vec::new(); let service_name_len_max = SERVICE_NAME_LEN_MAX_DEFAULT; let timers = BinaryHeap::new(); // Disable loopback by default. let if_selections = vec![ IfSelection { if_kind: IfKind::LoopbackV4, selected: false, }, IfSelection { if_kind: IfKind::LoopbackV6, selected: false, }, ]; let status = DaemonStatus::Running; Self { intf_socks, poll_ids: HashMap::new(), poll_id_count: 0, my_services: HashMap::new(), cache: DnsCache::new(), dns_registry_map, hostname_resolvers: HashMap::new(), service_queriers: HashMap::new(), retransmissions: Vec::new(), counters: HashMap::new(), poller, monitors, service_name_len_max, if_selections, signal_sock, timers, status, pending_resolves: HashSet::new(), resolved: HashSet::new(), multicast_loop_v4: true, multicast_loop_v6: true, } } fn process_set_option(&mut self, daemon_opt: DaemonOption) { match daemon_opt { DaemonOption::ServiceNameLenMax(length) => self.service_name_len_max = length, DaemonOption::EnableInterface(if_kind) => self.enable_interface(if_kind), DaemonOption::DisableInterface(if_kind) => self.disable_interface(if_kind), DaemonOption::MulticastLoopV4(on) => self.set_multicast_loop_v4(on), DaemonOption::MulticastLoopV6(on) => self.set_multicast_loop_v6(on), } } fn enable_interface(&mut self, kinds: Vec) { for if_kind in kinds { self.if_selections.push(IfSelection { if_kind, selected: true, }); } self.apply_intf_selections(my_ip_interfaces(true)); } fn disable_interface(&mut self, kinds: Vec) { for if_kind in kinds { self.if_selections.push(IfSelection { if_kind, selected: false, }); } self.apply_intf_selections(my_ip_interfaces(true)); } fn set_multicast_loop_v4(&mut self, on: bool) { for (_, sock) in self.intf_socks.iter_mut() { if let Err(e) = sock.set_multicast_loop_v4(on) { debug!("failed to set multicast loop v4: {e}"); } } } fn set_multicast_loop_v6(&mut self, on: bool) { for (_, sock) in self.intf_socks.iter_mut() { if let Err(e) = sock.set_multicast_loop_v6(on) { debug!("failed to set multicast loop v6: {e}"); } } } 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); } } } /// Insert a new interface into the poll map and return key fn add_poll(&mut self, intf: Interface) -> usize { Self::add_poll_impl(&mut self.poll_ids, &mut self.poll_id_count, intf) } /// Insert a new interface into the poll map and return its key. /// /// This exists to satisfy the borrow checker fn add_poll_impl( poll_ids: &mut HashMap, poll_id_count: &mut usize, intf: Interface, ) -> usize { let key = *poll_id_count; *poll_id_count += 1; let _ = (*poll_ids).insert(key, intf); key } fn add_timer(&mut self, next_time: u64) { self.timers.push(Reverse(next_time)); } fn peek_earliest_timer(&self) -> Option { self.timers.peek().map(|Reverse(v)| *v) } fn pop_earliest_timer(&mut self) -> Option { self.timers.pop().map(|Reverse(v)| v) } /// Apply all selections to `interfaces` and return the selected addresses. fn selected_addrs(&self, interfaces: Vec) -> HashSet { let intf_count = interfaces.len(); let mut intf_selections = vec![true; intf_count]; // apply if_selections for selection in self.if_selections.iter() { // Mark the interfaces for this selection. for i in 0..intf_count { if selection.if_kind.matches(&interfaces[i]) { intf_selections[i] = selection.selected; } } } let mut selected_addrs = HashSet::new(); for i in 0..intf_count { if intf_selections[i] { selected_addrs.insert(interfaces[i].addr.ip()); } } selected_addrs } /// Apply all selections to `interfaces`. /// /// For any interface, add it if selected but not bound yet, /// delete it if not selected but still bound. fn apply_intf_selections(&mut self, interfaces: Vec) { // By default, we enable all interfaces. let intf_count = interfaces.len(); let mut intf_selections = vec![true; intf_count]; // apply if_selections for selection in self.if_selections.iter() { // Mark the interfaces for this selection. for i in 0..intf_count { if selection.if_kind.matches(&interfaces[i]) { intf_selections[i] = selection.selected; } } } // Update `intf_socks` based on the selections. for (idx, intf) in interfaces.into_iter().enumerate() { if intf_selections[idx] { // Add the interface if !self.intf_socks.contains_key(&intf) { debug!("apply_intf_selections: add {:?}", &intf.ip()); self.add_new_interface(intf); } } else { // Remove the interface if let Some(mut sock) = self.intf_socks.remove(&intf) { match self.poller.registry().deregister(&mut sock) { Ok(()) => debug!("apply_intf_selections: deregister {:?}", &intf.ip()), Err(e) => debug!("apply_intf_selections: poller.delete {:?}: {}", &intf, e), } // Remove from poll_ids self.poll_ids.retain(|_, v| v != &intf); // Remove cache records for this interface. self.cache.remove_addrs_on_disabled_intf(&intf); } } } } /// Check for IP changes and update intf_socks as needed. fn check_ip_changes(&mut self) { // Get the current interfaces. let my_ifaddrs = my_ip_interfaces(true); let poll_ids = &mut self.poll_ids; let poller = &mut self.poller; // Remove unused sockets in the poller. let deleted_addrs = self .intf_socks .iter_mut() .filter_map(|(intf, sock)| { if !my_ifaddrs.contains(intf) { if let Err(e) = poller.registry().deregister(sock) { debug!("check_ip_changes: poller.delete {:?}: {}", intf, e); } // Remove from poll_ids poll_ids.retain(|_, v| v != intf); Some(intf.ip()) } else { None } }) .collect::>(); // Remove deleted addrs from my services that enabled `addr_auto`. for ip in deleted_addrs.iter() { self.del_addr_in_my_services(ip); self.notify_monitors(DaemonEvent::IpDel(*ip)); } // Keep the interfaces only if they still exist. self.intf_socks.retain(|intf, _| my_ifaddrs.contains(intf)); // Add newly found interfaces only if in our selections. self.apply_intf_selections(my_ifaddrs); } fn add_new_interface(&mut self, intf: Interface) { // Bind the new interface. let new_ip = intf.ip(); let should_loop = if new_ip.is_ipv4() { self.multicast_loop_v4 } else { self.multicast_loop_v6 }; let mut sock = match new_socket_bind(&intf, should_loop) { Ok(s) => s, Err(e) => { debug!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); return; } }; // Add the new interface into the poller. let key = self.add_poll(intf.clone()); if let Err(e) = self.poller .registry() .register(&mut sock, mio::Token(key), mio::Interest::READABLE) { debug!("check_ip_changes: poller add ip {}: {}", new_ip, e); return; } debug!("add new interface {}: {new_ip}", intf.name); let dns_registry = match self.dns_registry_map.get_mut(&intf) { Some(registry) => registry, None => self .dns_registry_map .entry(intf.clone()) .or_insert_with(DnsRegistry::new), }; for (_, service_info) in self.my_services.iter_mut() { if service_info.is_addr_auto() { service_info.insert_ipaddr(new_ip); if announce_service_on_intf(dns_registry, service_info, &intf, &sock) { debug!( "Announce service {} on {}", service_info.get_fullname(), intf.ip() ); service_info.set_status(&intf, ServiceStatus::Announced); } else { for timer in dns_registry.new_timers.drain(..) { self.timers.push(Reverse(timer)); } service_info.set_status(&intf, ServiceStatus::Probing); } } } self.intf_socks.insert(intf, sock); // Notify the monitors. self.notify_monitors(DaemonEvent::IpAdd(new_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) { debug!("check_service_name_length: {}", &e); self.notify_monitors(DaemonEvent::Error(e)); return; } if info.is_addr_auto() { let selected_addrs = self.selected_addrs(my_ip_interfaces(true)); for addr in selected_addrs { info.insert_ipaddr(addr); } } debug!("register service {:?}", &info); let outgoing_addrs = self.send_unsolicited_response(&mut info); if !outgoing_addrs.is_empty() { self.notify_monitors(DaemonEvent::Announce( info.get_fullname().to_string(), format!("{:?}", &outgoing_addrs), )); } // The key has to be lower case letter as DNS record name is case insensitive. // The info will have the original name. let service_fullname = info.get_fullname().to_lowercase(); self.my_services.insert(service_fullname, info); } /// Sends out announcement of `info` on every valid interface. /// Returns the list of interface IPs that sent out the announcement. fn send_unsolicited_response(&mut self, info: &mut ServiceInfo) -> Vec { let mut outgoing_addrs = Vec::new(); // Send the announcement on one interface per ip version. let mut multicast_sent_trackers = HashSet::new(); let mut outgoing_intfs = Vec::new(); for (intf, sock) in self.intf_socks.iter() { if let Some(tracker) = multicast_send_tracker(intf) { if multicast_sent_trackers.contains(&tracker) { continue; // No need to send again on the same interface with same ip version. } } let dns_registry = match self.dns_registry_map.get_mut(intf) { Some(registry) => registry, None => self .dns_registry_map .entry(intf.clone()) .or_insert_with(DnsRegistry::new), }; if announce_service_on_intf(dns_registry, info, intf, sock) { if let Some(tracker) = multicast_send_tracker(intf) { multicast_sent_trackers.insert(tracker); } outgoing_addrs.push(intf.ip()); outgoing_intfs.push(intf.clone()); debug!("Announce service {} on {}", info.get_fullname(), intf.ip()); info.set_status(intf, ServiceStatus::Announced); } else { for timer in dns_registry.new_timers.drain(..) { self.timers.push(Reverse(timer)); } info.set_status(intf, ServiceStatus::Probing); } } // 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() + 1000; for intf in outgoing_intfs { self.add_retransmission( next_time, Command::RegisterResend(info.get_fullname().to_string(), intf), ); } outgoing_addrs } /// Send probings or finish them if expired. Notify waiting services. fn probing_handler(&mut self) { let now = current_time_millis(); for (intf, sock) in self.intf_socks.iter() { let Some(dns_registry) = self.dns_registry_map.get_mut(intf) else { continue; }; let mut expired_probe_names = 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_probe_names.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 self.timers.push(Reverse(probe.next_send)); } } } // send probing. if !out.questions().is_empty() { debug!("sending out probing of {} questions", out.questions().len()); send_dns_outgoing(&out, intf, sock); } let mut waiting_services = HashSet::new(); for name in expired_probe_names { 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(), }; notify_monitors(&mut self.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); } } // wake up services waiting. for service_name in waiting_services { debug!( "try to announce service {service_name} on intf {}", intf.ip() ); // service names are lowercase if let Some(info) = self.my_services.get_mut(&service_name.to_lowercase()) { if info.get_status(intf) == ServiceStatus::Announced { debug!("service {} already announced", info.get_fullname()); continue; } if announce_service_on_intf(dns_registry, info, intf, sock) { let next_time = now + 1000; let command = Command::RegisterResend(info.get_fullname().to_string(), intf.clone()); self.retransmissions.push(ReRun { next_time, command }); self.timers.push(Reverse(next_time)); let fullname = match dns_registry.name_changes.get(&service_name) { Some(new_name) => new_name.to_string(), None => service_name.to_string(), }; let mut hostname = info.get_hostname(); if let Some(new_name) = dns_registry.name_changes.get(hostname) { hostname = new_name; } debug!("wake up: announce service {} on {}", fullname, intf.ip()); notify_monitors( &mut self.monitors, DaemonEvent::Announce(fullname, format!("{}:{}", hostname, &intf.ip())), ); info.set_status(intf, ServiceStatus::Announced); } } } } } fn unregister_service( &self, info: &ServiceInfo, intf: &Interface, sock: &MioUdpSocket, ) -> Vec { 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, info.get_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, info.get_fullname().to_string(), ), 0, ); } out.add_answer_at_time( DnsSrv::new( info.get_fullname(), CLASS_IN | CLASS_CACHE_FLUSH, 0, info.get_priority(), info.get_weight(), info.get_port(), info.get_hostname().to_string(), ), 0, ); out.add_answer_at_time( DnsTxt::new( info.get_fullname(), CLASS_IN | CLASS_CACHE_FLUSH, 0, info.generate_txt(), ), 0, ); for address in info.get_addrs_on_intf(intf) { out.add_answer_at_time( DnsAddress::new( info.get_hostname(), ip_address_rr_type(&address), CLASS_IN | CLASS_CACHE_FLUSH, 0, address, ), 0, ); } // `out` data is non-empty, hence we can do this. send_dns_outgoing(&out, intf, sock).remove(0) } /// Binds a channel `listener` to querying mDNS hostnames. /// /// If there is already a `listener`, it will be updated, i.e. overwritten. fn add_hostname_resolver( &mut self, hostname: String, listener: Sender, timeout: Option, ) { let real_timeout = timeout.map(|t| current_time_millis() + t); self.hostname_resolvers .insert(hostname, (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 out a list of `questions` (i.e. DNS questions) via multicast. fn send_query_vec(&self, questions: &[(&str, RRType)]) { trace!("Sending query questions: {:?}", questions); 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); let mut new_record = record.clone(); new_record.get_record_mut().update_ttl(now); out.add_answer_box(new_record); } } // Send the query on one interface per ip version. let mut multicast_sent_trackers = HashSet::new(); for (intf, sock) in self.intf_socks.iter() { if let Some(tracker) = multicast_send_tracker(intf) { if multicast_sent_trackers.contains(&tracker) { continue; // no need to send query the same interface with same ip version. } multicast_sent_trackers.insert(tracker); } send_dns_outgoing(&out, intf, sock); } } /// Reads from the socket of `ip`. /// /// Returns false if failed to receive a packet, /// otherwise returns true. fn handle_read(&mut self, intf: &Interface) -> bool { let sock = match self.intf_socks.get_mut(intf) { Some(if_sock) => if_sock, None => return false, }; let mut buf = vec![0u8; MAX_MSG_ABSOLUTE]; // Read the next mDNS UDP datagram. // // If the datagram is larger than `buf`, excess bytes may or may not // be truncated by the socket layer depending on the platform's libc. // In any case, such large datagram will not be decoded properly and // this function should return false but should not crash. let sz = match sock.recv(&mut buf) { Ok(sz) => sz, Err(e) => { if e.kind() != std::io::ErrorKind::WouldBlock { debug!("listening socket read failed: {}", e); } return false; } }; trace!("received {} bytes at IP: {}", sz, intf.ip()); // If sz is 0, it means sock reached End-of-File. if sz == 0 { debug!("socket {:?} was likely shutdown", &sock); if let Err(e) = self.poller.registry().deregister(sock) { debug!("failed to remove sock {:?} from poller: {}", sock, &e); } // Replace the closed socket with a new one. let should_loop = if intf.ip().is_ipv4() { self.multicast_loop_v4 } else { self.multicast_loop_v6 }; match new_socket_bind(intf, should_loop) { Ok(new_sock) => { trace!("reset socket for IP {}", intf.ip()); self.intf_socks.insert(intf.clone(), new_sock); } Err(e) => debug!("re-bind a socket to {:?}: {}", intf, e), } return false; } buf.truncate(sz); // reduce potential processing errors match DnsIncoming::new(buf) { Ok(msg) => { if msg.is_query() { self.handle_query(msg, intf); } else if msg.is_response() { self.handle_response(msg, intf); } 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.any().downcast_ref::() { if self.cache.get_addr(srv.host()).is_none() { self.send_query_vec(&[(srv.host(), RRType::A), (srv.host(), RRType::AAAA)]); return true; } } } } else { self.send_query(instance, RRType::ANY); return true; } false } /// Checks if `ty_domain` has records in the cache. If yes, sends the /// cached records via `sender`. fn query_cache_for_service(&mut self, ty_domain: &str, sender: &Sender) { let mut resolved: HashSet = HashSet::new(); let mut unresolved: HashSet = HashSet::new(); if let Some(records) = self.cache.get_ptr(ty_domain) { for record in records.iter() { if let Some(ptr) = record.any().downcast_ref::() { let info = match self.create_service_info_from_cache(ty_domain, ptr.alias()) { Ok(ok) => ok, Err(err) => { debug!("Error while creating service info from cache: {}", err); continue; } }; match sender.send(ServiceEvent::ServiceFound( ty_domain.to_string(), ptr.alias().to_string(), )) { Ok(()) => debug!("send service found {}", ptr.alias()), Err(e) => { debug!("failed to send service found: {}", e); continue; } } if info.is_ready() { resolved.insert(ptr.alias().to_string()); match sender.send(ServiceEvent::ServiceResolved(info)) { Ok(()) => debug!("sent service resolved: {}", ptr.alias()), Err(e) => debug!("failed to send service resolved: {}", e), } } else { unresolved.insert(ptr.alias().to_string()); } } } } for instance in resolved.drain() { self.pending_resolves.remove(&instance); self.resolved.insert(instance); } for instance in unresolved.drain() { self.add_pending_resolve(instance); } } /// Checks if `hostname` has records in the cache. If yes, sends the /// cached records via `sender`. fn query_cache_for_hostname( &mut self, hostname: &str, sender: Sender, ) { let addresses = self.cache.get_addresses_for_host(hostname); if !addresses.is_empty() { match sender.send(HostnameResolutionEvent::AddressesFound( hostname.to_string(), 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); } } fn create_service_info_from_cache( &self, ty_domain: &str, fullname: &str, ) -> Result { let my_name = { let name = fullname.trim_end_matches(split_sub_domain(ty_domain).0); name.strip_suffix('.').unwrap_or(name).to_string() }; let now = current_time_millis(); let mut info = ServiceInfo::new(ty_domain, &my_name, "", (), 0, None)?; // 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 info.get_subtype().is_none() { info.set_subtype(subtype.clone()); } } // resolve SRV record if let Some(records) = self.cache.get_srv(fullname) { if let Some(answer) = records.first() { if let Some(dns_srv) = answer.any().downcast_ref::() { info.set_hostname(dns_srv.host().to_string()); info.set_port(dns_srv.port()); } } } // resolve TXT record if let Some(records) = self.cache.get_txt(fullname) { if let Some(record) = records.first() { if let Some(dns_txt) = record.any().downcast_ref::() { info.set_properties_from_txt(dns_txt.text()); } } } // resolve A and AAAA records if let Some(records) = self.cache.get_addr(info.get_hostname()) { for answer in records.iter() { if let Some(dns_a) = answer.any().downcast_ref::() { if dns_a.get_record().is_expired(now) { trace!("Addr expired: {}", dns_a.address()); } else { info.insert_ipaddr(dns_a.address()); } } } } Ok(info) } 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. let intf = match self.poll_ids.get(&ev.token().0) { Some(interface) => interface.clone(), None => { debug!("Ip for event key {} not found", ev.token().0); break; } }; while self.handle_read(&intf) {} // we continue to monitor this socket. if let Some(sock) = self.intf_socks.get_mut(&intf) { if let Err(e) = self.poller .registry() .reregister(sock, ev.token(), mio::Interest::READABLE) { debug!("modify poller for interface {:?}: {}", &intf, e); break; } } } } /// Deal with incoming response packets. All answers /// are held in the cache, and listeners are notified. fn handle_response(&mut self, mut msg: DnsIncoming, intf: &Interface) { trace!( "handle_response: {} answers {} authorities {} additionals", msg.answers().len(), &msg.authorities().len(), &msg.num_additionals() ); let now = current_time_millis(); // remove records that are expired. let mut record_predicate = |record: &DnsRecordBox| { if !record.get_record().is_expired(now) { return true; } debug!("record is expired, removing it from cache."); if self.cache.remove(record) { // for PTR records, send event to listeners if let Some(dns_ptr) = record.any().downcast_ref::() { call_service_listener( &self.service_queriers, dns_ptr.get_name(), ServiceEvent::ServiceRemoved( dns_ptr.get_name().to_string(), dns_ptr.alias().to_string(), ), ); } } false }; msg.answers_mut().retain(&mut record_predicate); msg.authorities_mut().retain(&mut record_predicate); msg.additionals_mut().retain(&mut record_predicate); // check possible conflicts and handle them. self.conflict_handler(&msg, intf); /// 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(); for record in msg.all_records() { match self.cache.add_or_update(intf, record, &mut timers) { Some((dns_record, true)) => { timers.push(dns_record.get_record().get_expire_time()); timers.push(dns_record.get_record().get_refresh_time()); let ty = dns_record.get_type(); let name = dns_record.get_name(); if ty == RRType::PTR { if self.service_queriers.contains_key(name) { timers.push(dns_record.get_record().get_refresh_time()); } // send ServiceFound if let Some(dns_ptr) = dns_record.any().downcast_ref::() { 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.get_record().get_expire_time()); timers.push(dns_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. changes .iter() .filter(|change| change.ty == RRType::A || change.ty == RRType::AAAA) .map(|change| change.name.clone()) .collect::>() .iter() .map(|hostname| (hostname, self.cache.get_addresses_for_host(hostname))) .for_each(|(hostname, addresses)| { call_hostname_resolution_listener( &self.hostname_resolvers, hostname, HostnameResolutionEvent::AddressesFound(hostname.to_string(), 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, intf: &Interface) { let Some(dns_registry) = self.dns_registry_map.get_mut(intf) else { return; }; for answer in msg.answers().iter() { let mut new_records = Vec::new(); let name = answer.get_name(); let Some(probe) = dns_registry.probing.get_mut(name) else { continue; }; // check against possible multicast forwarding if answer.get_type() == RRType::A || answer.get_type() == RRType::AAAA { if let Some(answer_addr) = answer.any().downcast_ref::() { if !valid_ip_on_intf(&answer_addr.address(), intf) { debug!( "conflict handler: answer addr {:?} not in the subnet of {:?}", answer_addr, intf ); 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) { self.timers.push(Reverse(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) }); self.timers.push(Reverse(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) { let mut resolved: HashSet = HashSet::new(); let mut unresolved: HashSet = HashSet::new(); let mut removed_instances = HashMap::new(); 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 record in records.iter() { if let Some(dns_ptr) = record.any().downcast_ref::() { if updated_instances.contains(dns_ptr.alias()) { if let Ok(info) = self.create_service_info_from_cache(ty_domain, dns_ptr.alias()) { if info.is_ready() { debug!("call queriers to resolve {}", dns_ptr.alias()); resolved.insert(dns_ptr.alias().to_string()); call_service_listener( &self.service_queriers, ty_domain, ServiceEvent::ServiceResolved(info), ); } else { if self.resolved.remove(dns_ptr.alias()) { removed_instances .entry(ty_domain.to_string()) .or_insert_with(HashSet::new) .insert(dns_ptr.alias().to_string()); } unresolved.insert(dns_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); } self.notify_service_removal(removed_instances); } /// Handle incoming query packets, figure out whether and what to respond. fn handle_query(&mut self, msg: DnsIncoming, intf: &Interface) { let sock = match self.intf_socks.get(intf) { Some(sock) => sock, None => return, }; let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE | FLAGS_AA); // Special meta-query "_services._dns-sd._udp.". // See https://datatracker.ietf.org/doc/html/rfc6763#section-9 const META_QUERY: &str = "_services._dns-sd._udp.local."; let Some(dns_registry) = self.dns_registry_map.get_mut(intf) else { debug!("missing dns registry for intf {}", intf.ip()); return; }; for question in msg.questions().iter() { trace!("query question: {:?}", &question); let qtype = question.entry_type(); if qtype == RRType::PTR { for service in self.my_services.values() { if service.get_status(intf) != ServiceStatus::Announced { continue; } if question.entry_name() == service.get_type() || service .get_subtype() .as_ref() .is_some_and(|v| v == question.entry_name()) { add_answer_with_additionals(&mut out, &msg, service, intf, dns_registry); } else if question.entry_name() == META_QUERY { let ptr_added = out.add_answer( &msg, DnsPointer::new( question.entry_name(), RRType::PTR, CLASS_IN, service.get_other_ttl(), service.get_type().to_string(), ), ); if !ptr_added { 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 { let probe_name = question.entry_name(); if let Some(probe) = dns_registry.probing.get_mut(probe_name) { let now = current_time_millis(); // Only do tiebreaking if probe already started. // This check also helps avoid redo tiebreaking if start time // was postponed. if probe.start_time < now { let incoming_records: 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. */ match probe.tiebreaking(&incoming_records) { cmp::Ordering::Less => { debug!( "tiebreaking '{}': LOST, will wait for one second", probe_name ); probe.start_time = now + 1000; // wait and restart. probe.next_send = now + 1000; } ordering => { debug!("tiebreaking '{}': {:?}", probe_name, ordering); } } } } } if qtype == RRType::A || qtype == RRType::AAAA || qtype == RRType::ANY { for service in self.my_services.values() { if service.get_status(intf) != ServiceStatus::Announced { continue; } let service_hostname = match dns_registry.name_changes.get(service.get_hostname()) { Some(new_name) => new_name, None => service.get_hostname(), }; if service_hostname.to_lowercase() == question.entry_name().to_lowercase() { let intf_addrs = service.get_addrs_on_intf(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 ); return; } for address in intf_addrs { out.add_answer( &msg, DnsAddress::new( question.entry_name(), ip_address_rr_type(&address), CLASS_IN | CLASS_CACHE_FLUSH, service.get_host_ttl(), address, ), ); } } } } let query_name = question.entry_name().to_lowercase(); let service_opt = self .my_services .iter() .find(|(k, _v)| { let service_name = match dns_registry.name_changes.get(k.as_str()) { Some(new_name) => new_name, None => k, }; service_name == &query_name }) .map(|(_, v)| v); let Some(service) = service_opt else { continue; }; if service.get_status(intf) != ServiceStatus::Announced { continue; } if qtype == RRType::SRV || qtype == RRType::ANY { out.add_answer( &msg, DnsSrv::new( question.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( question.entry_name(), CLASS_IN | CLASS_CACHE_FLUSH, service.get_host_ttl(), service.generate_txt(), ), ); } if qtype == RRType::SRV { let intf_addrs = service.get_addrs_on_intf(intf); if intf_addrs.is_empty() { debug!( "Cannot find valid addrs for TYPE_SRV response on intf {:?}", &intf ); return; } 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, )); } } } } if !out.answers_count() > 0 { out.set_id(msg.id()); send_dns_outgoing(&out, intf, sock); self.increase_counter(Counter::Respond, 1); self.notify_monitors(DaemonEvent::Respond(intf.ip())); } self.increase_counter(Counter::KnownAnswerSuppression, out.known_answer_count()); } /// 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); } } } 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. fn notify_service_removal(&self, expired: HashMap>) { for (ty_domain, sender) in self.service_queriers.iter() { if let Some(instances) = expired.get(ty_domain) { for instance_name in instances { let event = ServiceEvent::ServiceRemoved( ty_domain.to_string(), instance_name.to_string(), ); match sender.send(event) { Ok(()) => debug!("notify_service_removal: sent ServiceRemoved to listener of {ty_domain}: {instance_name}"), Err(e) => debug!("Failed to send event: {}", e), } } } } } /// The entry point that executes all commands received by the daemon. /// /// `repeating`: whether this is a retransmission. fn exec_command(&mut self, command: Command, repeating: bool) { match command { Command::Browse(ty, next_delay, listener) => { self.exec_command_browse(repeating, ty, next_delay, 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.addr); self.exec_command_register_resend(fullname, intf); } Command::Unregister(fullname, resp_s) => { trace!("unregister service {} repeat {}", &fullname, &repeating); self.exec_command_unregister(repeating, fullname, resp_s); } Command::UnregisterResend(packet, ip) => { self.exec_command_unregister_resend(packet, ip); } Command::StopBrowse(ty_domain) => self.exec_command_stop_browse(ty_domain), Command::StopResolveHostname(hostname) => { self.exec_command_stop_resolve_hostname(hostname) } Command::Resolve(instance, try_count) => self.exec_command_resolve(instance, try_count), Command::GetMetrics(resp_s) => match resp_s.send(self.counters.clone()) { Ok(()) => trace!("Sent metrics to the client"), Err(e) => debug!("Failed to send metrics: {}", e), }, 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::Verify(instance_fullname, timeout) => { self.exec_command_verify(instance_fullname, timeout, repeating); } _ => { debug!("unexpected command: {:?}", &command); } } } fn exec_command_browse( &mut self, repeating: bool, ty: String, next_delay: u32, listener: Sender, ) { let pretty_addrs: Vec = self .intf_socks .keys() .map(|itf| format!("{} ({})", itf.ip(), 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; } 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); } self.send_query(&ty, RRType::PTR); self.increase_counter(Counter::Browse, 1); let next_time = current_time_millis() + (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, listener)); } fn exec_command_resolve_hostname( &mut self, repeating: bool, hostname: String, next_delay: u32, listener: Sender, timeout: Option, ) { let addr_list: Vec<_> = self.intf_socks.keys().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; } 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()); } self.send_query_vec(&[(&hostname, RRType::A), (&hostname, RRType::AAAA)]); self.increase_counter(Counter::ResolveHostname, 1); let now = current_time_millis(); let next_time = now + u64::from(next_delay) * 1000; let max_delay = 60 * 60; let delay = cmp::min(next_delay * 2, max_delay); // Only add retransmission if it does not exceed the hostname resolver timeout, if any. if self .hostname_resolvers .get(&hostname) .and_then(|(_sender, timeout)| *timeout) .map(|timeout| next_time < timeout) .unwrap_or(true) { self.add_retransmission( next_time, Command::ResolveHostname(hostname, delay, listener, None), ); } } fn exec_command_resolve(&mut self, instance: String, try_count: u16) { let pending_query = self.query_unresolved(&instance); let max_try = 3; if pending_query && try_count < max_try { // Note that if the current try already succeeds, the next retransmission // will be no-op as the cache has been updated. let next_time = current_time_millis() + RESOLVE_WAIT_IN_MILLIS; self.add_retransmission(next_time, Command::Resolve(instance, try_count + 1)); } } fn exec_command_unregister( &mut self, repeating: bool, fullname: String, resp_s: Sender, ) { let response = match self.my_services.remove_entry(&fullname) { None => { debug!("unregister: cannot find such service {}", &fullname); UnregisterStatus::NotFound } Some((_k, info)) => { let mut timers = Vec::new(); // Send one unregister per interface and ip version let mut multicast_sent_trackers = HashSet::new(); for (intf, sock) in self.intf_socks.iter() { if let Some(tracker) = multicast_send_tracker(intf) { if multicast_sent_trackers.contains(&tracker) { continue; // no need to send unregister the same interface with same ip version. } multicast_sent_trackers.insert(tracker); } let packet = self.unregister_service(&info, intf, sock); // 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, intf.clone()), }); 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, intf: Interface) { if let Some(sock) = self.intf_socks.get(&intf) { debug!("UnregisterResend from {}", &intf.ip()); multicast_on_intf(&packet[..], &intf, sock); 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; } // 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, intf: Interface) { let Some(info) = self.my_services.get_mut(&fullname) else { trace!("announce: cannot find such service {}", &fullname); return; }; let Some(dns_registry) = self.dns_registry_map.get_mut(&intf) else { return; }; let Some(sock) = self.intf_socks.get(&intf) else { return; }; if announce_service_on_intf(dns_registry, info, &intf, sock) { let mut hostname = info.get_hostname(); if let Some(new_name) = dns_registry.name_changes.get(hostname) { hostname = new_name; } let service_name = match dns_registry.name_changes.get(&fullname) { Some(new_name) => new_name.to_string(), None => fullname, }; debug!("resend: announce service {} on {}", service_name, intf.ip()); notify_monitors( &mut self.monitors, DaemonEvent::Announce(service_name, format!("{}:{}", hostname, &intf.ip())), ); info.set_status(&intf, ServiceStatus::Announced); } else { debug!("register-resend should not fail"); } self.increase_counter(Counter::RegisterResend, 1); } 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(ty_domain); for instance in instances.iter() { trace!("sending refresh query for SRV: {}", instance); self.send_query(instance, RRType::SRV); 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::CacheRefreshSRV, query_srv_count); self.increase_counter(Counter::CacheRefreshAddr, query_addr_count); } } /// All possible events sent to the client from the daemon /// regarding service discovery. #[derive(Debug)] pub enum ServiceEvent { /// Started searching for a service type. SearchStarted(String), /// Found a specific (service_type, fullname). ServiceFound(String, String), /// Resolved a service instance with detailed info. ServiceResolved(ServiceInfo), /// 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(Debug)] #[non_exhaustive] pub enum HostnameResolutionEvent { /// Started searching for the ip address of a hostname. SearchStarted(String), /// One or more addresses for a hostname has been found. AddressesFound(String, HashSet), /// One or more addresses for a hostname has been removed. AddressesRemoved(String, HashSet), /// The search for the ip address of a hostname has timed out. SearchTimeout(String), /// Stopped searching for the ip address of a hostname. SearchStopped(String), } /// Some notable events from the daemon besides [`ServiceEvent`]. /// These events are expected to happen infrequently. #[derive(Clone, Debug)] #[non_exhaustive] pub enum DaemonEvent { /// Daemon unsolicitly announced a service from an interface. Announce(String, String), /// Daemon encountered an error. Error(Error), /// Daemon detected a new IP address from the host. IpAdd(IpAddr), /// Daemon detected a IP address removed from the host. IpDel(IpAddr), /// Daemon resolved a name conflict by changing one of its names. /// see [DnsNameChange] for more details. NameChange(DnsNameChange), /// Send out a multicast response via an IP address. Respond(IpAddr), } /// 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, Sender), /// Resolve a hostname to IP addresses. ResolveHostname(String, u32, Sender, Option), // (hostname, next_time_delay_in_seconds, sender, timeout_in_milliseconds) /// Register a service Register(ServiceInfo), /// Unregister a service Unregister(String, Sender), // (fullname) /// Announce again a service to local network RegisterResend(String, Interface), // (fullname) /// Resend unregister packet. UnregisterResend(Vec, Interface), // (packet content) /// Stop browsing a service type StopBrowse(String), // (ty_domain) /// Stop resolving a hostname StopResolveHostname(String), // (hostname) /// Send query to resolve a service instance. /// This is used when a PTR record exists but SRV & TXT records are missing. Resolve(String, u16), // (service_instance_fullname, try_count) /// Read the current values of the counters GetMetrics(Sender), /// Get the current status of the daemon. GetStatus(Sender), /// Monitor noticable events in the daemon. Monitor(Sender), SetOption(DaemonOption), /// 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), Exit(Sender), } impl fmt::Display for Command { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Browse(_, _, _) => write!(f, "Command Browse"), Self::ResolveHostname(_, _, _, _) => write!(f, "Command ResolveHostname"), Self::Exit(_) => write!(f, "Command Exit"), Self::GetStatus(_) => write!(f, "Command GetStatus"), Self::GetMetrics(_) => write!(f, "Command GetMetrics"), Self::Monitor(_) => write!(f, "Command Monitor"), Self::Register(_) => write!(f, "Command Register"), Self::RegisterResend(_, _) => write!(f, "Command RegisterResend"), Self::SetOption(_) => write!(f, "Command SetOption"), Self::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"), } } } #[derive(Debug)] enum DaemonOption { ServiceNameLenMax(u8), EnableInterface(Vec), DisableInterface(Vec), MulticastLoopV4(bool), MulticastLoopV6(bool), } /// The length of Service Domain name supported in this lib. const DOMAIN_LEN: usize = "._tcp.local.".len(); /// Validate the length of "service_name" in a "_.." string. fn check_service_name_length(ty_domain: &str, limit: u8) -> Result<()> { if ty_domain.len() <= DOMAIN_LEN + 1 { // service name cannot be empty or only '_'. return Err(e_fmt!("Service type name cannot be empty: {}", ty_domain)); } let service_name_len = ty_domain.len() - DOMAIN_LEN - 1; // exclude the leading `_` if service_name_len > limit as usize { return Err(e_fmt!("Service name length must be <= {} bytes", limit)); } Ok(()) } /// Checks if `name` ends with a valid domain: '._tcp.local.' or '._udp.local.' fn check_domain_suffix(name: &str) -> Result<()> { if !(name.ends_with("._tcp.local.") || name.ends_with("._udp.local.")) { return Err(e_fmt!( "mDNS service {} must end with '._tcp.local.' or '._udp.local.'", name )); } Ok(()) } /// Validate the service name in a fully qualified name. /// /// A Full Name = .. /// The only `` supported are "._tcp.local." and "._udp.local.". /// /// Note: this function does not check for the length of the service name. /// Instead, `register_service` method will check the length. fn check_service_name(fullname: &str) -> Result<()> { check_domain_suffix(fullname)?; let remaining: Vec<&str> = fullname[..fullname.len() - DOMAIN_LEN].split('.').collect(); let name = remaining.last().ok_or_else(|| e_fmt!("No service name"))?; if &name[0..1] != "_" { return Err(e_fmt!("Service name must start with '_'")); } let name = &name[1..]; if name.contains("--") { return Err(e_fmt!("Service name must not contain '--'")); } if name.starts_with('-') || name.ends_with('-') { return Err(e_fmt!("Service name (%s) may not start or end with '-'")); } let ascii_count = name.chars().filter(|c| c.is_ascii_alphabetic()).count(); if ascii_count < 1 { return Err(e_fmt!( "Service name must contain at least one letter (eg: 'A-Za-z')" )); } Ok(()) } /// Validate a hostname. fn check_hostname(hostname: &str) -> Result<()> { if !hostname.ends_with(".local.") { return Err(e_fmt!("Hostname must end with '.local.': {hostname}")); } if hostname == ".local." { return Err(e_fmt!( "The part of the hostname before '.local.' cannot be empty" )); } if hostname.len() > 255 { return Err(e_fmt!("Hostname length must be <= 255 bytes")); } Ok(()) } fn call_service_listener( listeners_map: &HashMap>, ty_domain: &str, event: ServiceEvent, ) { if let Some(listener) = listeners_map.get(ty_domain) { match listener.send(event) { Ok(()) => trace!("Sent event to listener successfully"), Err(e) => debug!("Failed to send event: {}", e), } } } fn call_hostname_resolution_listener( listeners_map: &HashMap, Option)>, hostname: &str, event: HostnameResolutionEvent, ) { if let Some(listener) = listeners_map.get(hostname).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. /// Loopback interfaces are excluded. fn my_ip_interfaces(with_loopback: bool) -> Vec { if_addrs::get_if_addrs() .unwrap_or_default() .into_iter() .filter(|i| !i.is_loopback() || with_loopback) .collect() } /// Send an outgoing mDNS query or response, and returns the packet bytes. fn send_dns_outgoing(out: &DnsOutgoing, intf: &Interface, sock: &MioUdpSocket) -> Vec> { let qtype = if out.is_query() { "query" } else { "response" }; trace!( "send outgoing {}: {} questions {} answers {} authorities {} additional", qtype, out.questions().len(), out.answers_count(), out.authorities().len(), out.additionals().len() ); let packet_list = out.to_data_on_wire(); for packet in packet_list.iter() { multicast_on_intf(packet, intf, sock); } packet_list } /// Sends a multicast packet, and returns the packet bytes. fn multicast_on_intf(packet: &[u8], intf: &Interface, socket: &MioUdpSocket) { if packet.len() > MAX_MSG_ABSOLUTE { debug!("Drop over-sized packet ({})", packet.len()); return; } let addr: SocketAddr = match intf.addr { if_addrs::IfAddr::V4(_) => SocketAddrV4::new(GROUP_ADDR_V4, MDNS_PORT).into(), if_addrs::IfAddr::V6(_) => { let mut sock = SocketAddrV6::new(GROUP_ADDR_V6, MDNS_PORT, 0, 0); sock.set_scope_id(intf.index.unwrap_or(0)); // Choose iface for multicast sock.into() } }; send_packet(packet, addr, intf, socket); } /// Sends out `packet` to `addr` on the socket in `intf_sock`. fn send_packet(packet: &[u8], addr: SocketAddr, intf: &Interface, sock: &MioUdpSocket) { match sock.send_to(packet, addr) { Ok(sz) => trace!("sent out {} bytes on interface {:?}", sz, intf), Err(e) => debug!("Failed to send to {} via {:?}: {}", addr, &intf, e), } } /// Returns true if `name` is a valid instance name of format: /// ..<_udp|_tcp>.local. /// Note: could contain '.' as well. fn valid_instance_name(name: &str) -> bool { name.split('.').count() >= 5 } fn notify_monitors(monitors: &mut Vec>, event: DaemonEvent) { monitors.retain(|sender| { if let Err(e) = sender.try_send(event.clone()) { debug!("notify_monitors: try_send: {}", &e); if matches!(e, TrySendError::Disconnected(_)) { return false; // This monitor is dropped. } } true }); } /// Check if all unique records passed "probing", and if yes, create a packet /// to announce the service. fn prepare_announce( info: &ServiceInfo, intf: &Interface, dns_registry: &mut DnsRegistry, ) -> Option { let intf_addrs = info.get_addrs_on_intf(intf); if intf_addrs.is_empty() { trace!("No valid addrs to add on intf {:?}", &intf); return None; } // check if we changed our name due to conflicts. let service_fullname = match dns_registry.name_changes.get(info.get_fullname()) { Some(new_name) => new_name, None => info.get_fullname(), }; debug!( "prepare to announce service {service_fullname} on {}: {}", &intf.name, &intf.ip() ); 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 = match dns_registry.name_changes.get(info.get_hostname()) { Some(new_name) => new_name.to_string(), None => 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, ); 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. fn announce_service_on_intf( dns_registry: &mut DnsRegistry, info: &ServiceInfo, intf: &Interface, sock: &MioUdpSocket, ) -> bool { if let Some(out) = prepare_announce(info, intf, dns_registry) { send_dns_outgoing(&out, intf, sock); return true; } 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!("{} (2)", first_part); // check if there is already has `()` suffix. if let Some(paren_pos) = first_part.rfind(" (") { // Check if there's a closing parenthesis if let Some(end_paren) = first_part[paren_pos..].find(')') { let absolute_end_pos = paren_pos + end_paren; // Only process if the closing parenthesis is the last character if absolute_end_pos == first_part.len() - 1 { let num_start = paren_pos + 2; // Skip " (" // Try to parse the number between parentheses if let Ok(number) = first_part[num_start..absolute_end_pos].parse::() { let base_name = &first_part[..paren_pos]; new_name = format!("{} ({})", base_name, number + 1) } } } } *first_part = &new_name; parts.join(".") } /// Returns a new name based on the `original` to avoid conflicts. /// If the name already contains a hyphenated number, increments that number. /// /// Examples: /// - `foo.local.` becomes `foo-2.local.` /// - `foo-2.local.` becomes `foo-3.local.` /// - `foo` becomes `foo-2` fn hostname_change(original: &str) -> String { let mut parts: Vec<_> = original.split('.').collect(); let Some(first_part) = parts.get_mut(0) else { return format!("{original}-2"); }; let mut new_name = format!("{}-2", first_part); // check if there is already a `-` suffix if let Some(hyphen_pos) = first_part.rfind('-') { // Try to parse everything after the hyphen as a number if let Ok(number) = first_part[hyphen_pos + 1..].parse::() { let base_name = &first_part[..hyphen_pos]; new_name = format!("{}-{}", base_name, number + 1); } } *first_part = &new_name; parts.join(".") } fn add_answer_with_additionals( out: &mut DnsOutgoing, msg: &DnsIncoming, service: &ServiceInfo, intf: &Interface, dns_registry: &DnsRegistry, ) { let intf_addrs = service.get_addrs_on_intf(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 = match dns_registry.name_changes.get(service.get_fullname()) { Some(new_name) => new_name, None => service.get_fullname(), }; let hostname = match dns_registry.name_changes.get(service.get_hostname()) { Some(new_name) => new_name, None => service.get_hostname(), }; let ptr_added = out.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); out.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. out.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(), )); out.add_additional_answer(DnsTxt::new( service_fullname, CLASS_IN | CLASS_CACHE_FLUSH, service.get_host_ttl(), service.generate_txt(), )); for address in intf_addrs { out.add_additional_answer(DnsAddress::new( hostname, ip_address_rr_type(&address), CLASS_IN | CLASS_CACHE_FLUSH, service.get_host_ttl(), address, )); } } #[cfg(test)] mod tests { use super::{ check_domain_suffix, check_service_name_length, hostname_change, my_ip_interfaces, name_change, new_socket_bind, send_dns_outgoing, valid_instance_name, HostnameResolutionEvent, ServiceDaemon, ServiceEvent, ServiceInfo, GROUP_ADDR_V4, MDNS_PORT, }; use crate::{ dns_parser::{DnsOutgoing, DnsPointer, RRType, CLASS_IN, FLAGS_AA, FLAGS_QR_RESPONSE}, service_daemon::check_hostname, }; use std::{ net::{SocketAddr, SocketAddrV4}, time::Duration, }; use test_log::test; #[test] fn test_socketaddr_print() { let addr: SocketAddr = SocketAddrV4::new(GROUP_ADDR_V4, MDNS_PORT).into(); let print = format!("{}", addr); assert_eq!(print, "224.0.0.251:5353"); } #[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_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.get_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(&packet_buffer, &intf, &sock); } 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.get_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) { match event { ServiceEvent::ServiceResolved(info) => { 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(); // 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) { match event { ServiceEvent::ServiceRemoved(service_type, full_name) => { 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 = my_ip_interfaces(false) .iter() .find(|iface| iface.ip().is_ipv4()) .map(|iface| iface.ip()) .unwrap(); let mut my_service = ServiceInfo::new( "_host_res_test._tcp.local.", "my_instance", hostname, &service_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) { match event { ServiceEvent::ServiceResolved(info) => { resolved = true; println!("Resolved a service of {}", &info.get_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 refresh_counter = metrics["cache-refresh-ptr"]; assert_eq!(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."); } } mdns-sd-0.13.3/src/service_info.rs000064400000000000000000001310351046102023000150700ustar 00000000000000//! Define `ServiceInfo` to represent a service and its operations. #[cfg(feature = "logging")] use crate::log::debug; use crate::{ dns_parser::{DnsRecordBox, DnsRecordExt, DnsSrv, RRType}, Error, Result, }; use if_addrs::{IfAddr, Interface}; use std::{ cmp, collections::{HashMap, HashSet}, convert::TryInto, fmt, net::{IpAddr, Ipv4Addr}, str::FromStr, }; /// 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 /// 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 { ty_domain: String, // . /// See RFC6763 section 7.1 about "Subtypes": /// sub_domain: Option, // ._sub.. fullname: String, // .. server: String, // fully qualified name for service host addresses: HashSet, port: u16, host_ttl: u32, // used for SRV and Address records other_ttl: u32, // used for PTR and TXT records priority: u16, weight: u16, txt_properties: TxtProperties, addr_auto: bool, // Let the system update addresses automatically. status: HashMap, /// Whether we need to probe names before announcing this service. requires_probe: 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.". /// /// `my_name` is the instance name, without the service type suffix. /// /// `host_name` is the "host" in the context of DNS. It is used as the "name" /// in the address records (i.e. TYPE_A and TYPE_AAAA records). It means that /// for the same hostname in the same local network, the service resolves in /// the same addresses. Be sure to check it if you see unexpected addresses resolved. /// /// `properties` can be `None` or key/value string pairs, in a type that /// implements [`IntoTxtProperties`] trait. It supports: /// - `HashMap` /// - `Option>` /// - slice of tuple: `&[(K, V)]` where `K` and `V` are [`std::string::ToString`]. /// /// Note: The maximum length of a single property string is `255`, Property that exceed the length are truncated. /// > `len(key + value) < u8::MAX` /// /// `ip` can be one or more IP addresses, in a type that implements /// [`AsIpAddrs`] trait. It supports: /// /// - Single IPv4: `"192.168.0.1"` /// - Single IPv6: `"2001:0db8::7334"` /// - Multiple IPv4 separated by comma: `"192.168.0.1,192.168.0.2"` /// - Multiple IPv6 separated by comma: `"2001:0db8::7334,2001:0db8::7335"` /// - A slice of IPv4: `&["192.168.0.1", "192.168.0.2"]` /// - A slice of IPv6: `&["2001:0db8::7334", "2001:0db8::7335"]` /// - A mix of IPv4 and IPv6: `"192.168.0.1,2001:0db8::7334"` /// - All the above formats with [IpAddr] or `String` instead of `&str`. /// /// The host TTL and other TTL are set to default values. pub fn new( ty_domain: &str, my_name: &str, host_name: &str, ip: Ip, port: u16, properties: P, ) -> Result { let (ty_domain, sub_domain) = split_sub_domain(ty_domain); let fullname = format!("{}.{}", my_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() ))); } } 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, }; 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; } /// Returns whether this service info requires name probing for potential name conflicts. /// /// By default, it returns true for every service info. pub const fn requires_probe(&self) -> bool { self.requires_probe } /// Returns the service type including the domain label. /// /// For example: "_my-service._udp.local.". #[inline] pub fn get_type(&self) -> &str { &self.ty_domain } /// Returns the service subtype including the domain label, /// if subtype has been defined. /// /// For example: "_printer._sub._http._tcp.local.". #[inline] pub const fn get_subtype(&self) -> &Option { &self.sub_domain } /// Returns a reference of the service fullname. /// /// This is useful, for example, in unregister. #[inline] pub fn get_fullname(&self) -> &str { &self.fullname } /// Returns the properties from TXT records. #[inline] pub const fn get_properties(&self) -> &TxtProperties { &self.txt_properties } /// Returns a property for a given `key`, where `key` is /// case insensitive. /// /// Returns `None` if `key` does not exist. pub fn get_property(&self, key: &str) -> Option<&TxtProperty> { self.txt_properties.get(key) } /// Returns a property value for a given `key`, where `key` is /// case insensitive. /// /// Returns `None` if `key` does not exist. pub fn get_property_val(&self, key: &str) -> Option> { self.txt_properties.get_property_val(key) } /// Returns a property value string for a given `key`, where `key` is /// case insensitive. /// /// Returns `None` if `key` does not exist. pub fn get_property_val_str(&self, key: &str) -> Option<&str> { self.txt_properties.get_property_val_str(key) } /// Returns the service's hostname. #[inline] pub fn get_hostname(&self) -> &str { &self.server } /// Returns the service's port. #[inline] pub const fn get_port(&self) -> u16 { self.port } /// Returns the service's addresses #[inline] pub const fn get_addresses(&self) -> &HashSet { &self.addresses } /// Returns the service's IPv4 addresses only. pub fn get_addresses_v4(&self) -> HashSet<&Ipv4Addr> { let mut ipv4_addresses = HashSet::new(); for ip in &self.addresses { if let IpAddr::V4(ipv4) = ip { ipv4_addresses.insert(ipv4); } } ipv4_addresses } /// Returns the service's TTL used for SRV and Address records. #[inline] pub const fn get_host_ttl(&self) -> u32 { self.host_ttl } /// Returns the service's TTL used for PTR and TXT records. #[inline] pub const fn get_other_ttl(&self) -> u32 { self.other_ttl } /// Returns the service's priority used in SRV records. #[inline] pub const fn get_priority(&self) -> u16 { self.priority } /// Returns the service's weight used in SRV records. #[inline] pub const fn get_weight(&self) -> u16 { self.weight } /// Returns a list of addresses that are in the same LAN as /// the interface `intf`. pub(crate) fn get_addrs_on_intf(&self, intf: &Interface) -> Vec { self.addresses .iter() .filter(|a| valid_ip_on_intf(a, intf)) .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, addr: IpAddr) { self.addresses.insert(addr); } pub(crate) fn remove_ipaddr(&mut self, addr: &IpAddr) { self.addresses.remove(addr); } pub(crate) fn generate_txt(&self) -> Vec { encode_txt(self.get_properties().iter()) } pub(crate) fn set_port(&mut self, port: u16) { self.port = port; } pub(crate) fn set_hostname(&mut self, hostname: String) { self.server = normalize_hostname(hostname); } /// Returns true if properties are updated. pub(crate) fn set_properties_from_txt(&mut self, txt: &[u8]) -> bool { let properties = decode_txt_unique(txt); if self.txt_properties.properties != properties { self.txt_properties = TxtProperties { properties }; true } else { false } } pub(crate) fn set_subtype(&mut self, subtype: String) { self.sub_domain = Some(subtype); } /// host_ttl is for SRV and address records /// currently only used for testing. pub(crate) fn _set_host_ttl(&mut self, ttl: u32) { self.host_ttl = ttl; } /// other_ttl is for PTR and TXT records. pub(crate) fn _set_other_ttl(&mut self, ttl: u32) { self.other_ttl = ttl; } pub(crate) fn set_status(&mut self, intf: &Interface, status: ServiceStatus) { match self.status.get_mut(intf) { Some(service_status) => { *service_status = status; } None => { self.status.entry(intf.clone()).or_insert(status); } } } pub(crate) fn get_status(&self, intf: &Interface) -> 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 { ResolvedService { ty_domain: self.ty_domain, sub_ty_domain: self.sub_domain, fullname: self.fullname, host: self.server, port: self.port, addresses: self.addresses, txt_properties: self.txt_properties, } } } /// Removes potentially duplicated ".local." at the end of "hostname". fn normalize_hostname(mut hostname: String) -> String { if hostname.ends_with(".local.local.") { let new_len = hostname.len() - "local.".len(); hostname.truncate(new_len); } hostname } /// This trait allows for parsing an input into a set of one or multiple [`Ipv4Addr`]. pub trait AsIpAddrs { fn as_ip_addrs(&self) -> Result>; } impl AsIpAddrs for &T { fn as_ip_addrs(&self) -> Result> { (*self).as_ip_addrs() } } /// Supports one address or multiple addresses separated by `,`. /// For example: "127.0.0.1,127.0.0.2". /// /// If the string is empty, will return an empty set. impl AsIpAddrs for &str { fn as_ip_addrs(&self) -> Result> { let mut addrs = HashSet::new(); if !self.is_empty() { let iter = self.split(',').map(str::trim).map(IpAddr::from_str); for addr in iter { let addr = addr.map_err(|err| Error::ParseIpAddr(err.to_string()))?; addrs.insert(addr); } } Ok(addrs) } } impl AsIpAddrs for String { fn as_ip_addrs(&self) -> Result> { self.as_str().as_ip_addrs() } } /// Support slice. Example: &["127.0.0.1", "127.0.0.2"] impl AsIpAddrs for &[I] { fn as_ip_addrs(&self) -> Result> { let mut addrs = HashSet::new(); for result in self.iter().map(I::as_ip_addrs) { addrs.extend(result?); } Ok(addrs) } } /// Optimization for zero sized/empty values, as `()` will never take up any space or evaluate to /// anything, helpful in contexts where we just want an empty value. impl AsIpAddrs for () { fn as_ip_addrs(&self) -> Result> { Ok(HashSet::new()) } } impl AsIpAddrs for std::net::IpAddr { fn as_ip_addrs(&self) -> Result> { let mut ips = HashSet::new(); ips.insert(*self); Ok(ips) } } /// 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)] pub struct TxtProperties { // Use `Vec` instead of `HashMap` to keep the order of insertions. properties: Vec, } impl TxtProperties { /// Returns an iterator for all properties. pub fn iter(&self) -> impl Iterator { self.properties.iter() } /// Returns the number of properties. pub fn len(&self) -> usize { self.properties.len() } /// Returns if the properties are empty. pub fn is_empty(&self) -> bool { self.properties.is_empty() } /// Returns a property for a given `key`, where `key` is /// case insensitive. pub fn get(&self, key: &str) -> Option<&TxtProperty> { let key = key.to_lowercase(); self.properties .iter() .find(|&prop| prop.key.to_lowercase() == key) } /// Returns a property value for a given `key`, where `key` is /// case insensitive. /// /// Returns `None` if `key` does not exist. /// Returns `Some(Option<&u8>)` for its value. pub fn get_property_val(&self, key: &str) -> Option> { self.get(key).map(|x| x.val()) } /// Returns a property value string for a given `key`, where `key` is /// case insensitive. /// /// Returns `None` if `key` does not exist. /// Returns `Some("")` if its value is `None` or is empty. pub fn get_property_val_str(&self, key: &str) -> Option<&str> { self.get(key).map(|x| x.val_str()) } /// Consumes properties and returns a hashmap, where the keys are the properties keys. /// /// If a property value is empty, return an empty string (because RFC 6763 allows empty values). /// If a property value is non-empty but not valid UTF-8, skip the property and log a message. pub fn into_property_map_str(self) -> HashMap { self.properties .into_iter() .filter_map(|property| { let val_string = property.val.map_or(Some(String::new()), |val| { String::from_utf8(val) .map_err(|e| { debug!("Property value contains invalid UTF-8: {e}"); }) .ok() })?; Some((property.key, val_string)) }) .collect() } } impl fmt::Display for TxtProperties { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let delimiter = ", "; let props: Vec = self.properties.iter().map(|p| p.to_string()).collect(); write!(f, "({})", props.join(delimiter)) } } /// Represents a property in a TXT record. #[derive(Clone, PartialEq, Eq)] pub struct TxtProperty { /// The name of the property. The original cases are kept. key: String, /// RFC 6763 says values are bytes, not necessarily UTF-8. /// It is also possible that there is no value, in which case /// the key is a boolean key. val: Option>, } impl TxtProperty { /// Returns the key of a property. pub fn key(&self) -> &str { &self.key } /// Returns the value of a property, which could be `None`. /// /// To obtain a `&str` of the value, use `val_str()` instead. pub fn val(&self) -> Option<&[u8]> { self.val.as_deref() } /// Returns the value of a property as str. pub fn val_str(&self) -> &str { self.val .as_ref() .map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default()) } } /// Supports constructing from a tuple. impl From<&(K, V)> for TxtProperty where K: ToString, V: ToString, { fn from(prop: &(K, V)) -> Self { Self { key: prop.0.to_string(), val: Some(prop.1.to_string().into_bytes()), } } } impl From<(K, V)> for TxtProperty where K: ToString, V: AsRef<[u8]>, { fn from(prop: (K, V)) -> Self { Self { key: prop.0.to_string(), val: Some(prop.1.as_ref().into()), } } } /// Support a property that has no value. impl From<&str> for TxtProperty { fn from(key: &str) -> Self { Self { key: key.to_string(), val: None, } } } impl fmt::Display for TxtProperty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}={}", self.key, self.val_str()) } } /// Mimic the default debug output for a struct, with a twist: /// - If self.var is UTF-8, will output it as a string in double quotes. /// - If self.var is not UTF-8, will output its bytes as in hex. impl fmt::Debug for TxtProperty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let val_string = self.val.as_ref().map_or_else( || "None".to_string(), |v| { std::str::from_utf8(&v[..]).map_or_else( |_| format!("Some({})", u8_slice_to_hex(&v[..])), |s| format!("Some(\"{}\")", s), ) }, ); write!( f, "TxtProperty {{key: \"{}\", val: {}}}", &self.key, &val_string, ) } } const HEX_TABLE: [char; 16] = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', ]; /// Create a hex string from `slice`, with a "0x" prefix. /// /// For example, [1u8, 2u8] -> "0x0102" fn u8_slice_to_hex(slice: &[u8]) -> String { let mut hex = String::with_capacity(slice.len() * 2 + 2); hex.push_str("0x"); for b in slice { hex.push(HEX_TABLE[(b >> 4) as usize]); hex.push(HEX_TABLE[(b & 0x0F) as usize]); } hex } /// This trait allows for converting inputs into [`TxtProperties`]. pub trait IntoTxtProperties { fn into_txt_properties(self) -> TxtProperties; } impl IntoTxtProperties for HashMap { fn into_txt_properties(mut self) -> TxtProperties { let properties = self .drain() .map(|(key, val)| TxtProperty { key, val: Some(val.into_bytes()), }) .collect(); TxtProperties { properties } } } /// Mainly for backward compatibility. impl IntoTxtProperties for Option> { fn into_txt_properties(self) -> TxtProperties { self.map_or_else( || TxtProperties { properties: Vec::new(), }, |h| h.into_txt_properties(), ) } } /// Support Vec like `[("k1", "v1"), ("k2", "v2")]`. impl<'a, T: 'a> IntoTxtProperties for &'a [T] where TxtProperty: From<&'a T>, { fn into_txt_properties(self) -> TxtProperties { let mut properties = Vec::new(); let mut keys = HashSet::new(); for t in self.iter() { let prop = TxtProperty::from(t); let key = prop.key.to_lowercase(); if keys.insert(key) { // Only push a new entry if the key did not exist. // // RFC 6763: https://www.rfc-editor.org/rfc/rfc6763#section-6.4 // // "If a client receives a TXT record containing the same key more than // once, then the client MUST silently ignore all but the first // occurrence of that attribute. " properties.push(prop); } } TxtProperties { properties } } } impl IntoTxtProperties for Vec { fn into_txt_properties(self) -> TxtProperties { TxtProperties { properties: self } } } // Convert from properties key/value pairs to DNS TXT record content fn encode_txt<'a>(properties: impl Iterator) -> Vec { let mut bytes = Vec::new(); for prop in properties { let mut s = prop.key.clone().into_bytes(); if let Some(v) = &prop.val { s.extend(b"="); s.extend(v); } // Property that exceed the length are truncated let sz: u8 = s.len().try_into().unwrap_or_else(|_| { debug!("Property {} is too long, truncating to 255 bytes", prop.key); s.resize(u8::MAX as usize, 0); u8::MAX }); // TXT uses (Length,Value) format for each property, // i.e. the first byte is the length. bytes.push(sz); bytes.extend(s); } if bytes.is_empty() { bytes.push(0); } bytes } // Convert from DNS TXT record content to key/value pairs pub(crate) fn decode_txt(txt: &[u8]) -> Vec { let mut properties = Vec::new(); let mut offset = 0; while offset < txt.len() { let length = txt[offset] as usize; if length == 0 { break; // reached the end } offset += 1; // move over the length byte let offset_end = offset + length; if offset_end > txt.len() { debug!("DNS TXT record contains invalid data: Size given for property would be out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len()); break; // Skipping the rest of the record content, as the size for this property would already be out of range. } let kv_bytes = &txt[offset..offset_end]; // split key and val using the first `=` let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else( || (kv_bytes.to_vec(), None), |idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())), ); // Make sure the key can be stored in UTF-8. match String::from_utf8(k) { Ok(k_string) => { properties.push(TxtProperty { key: k_string, val: v, }); } Err(e) => debug!("failed to convert to String from key: {}", e), } offset += length; } properties } fn decode_txt_unique(txt: &[u8]) -> Vec { let mut properties = decode_txt(txt); // Remove duplicated keys and retain only the first appearance // of each key. let mut keys = HashSet::new(); properties.retain(|p| { let key = p.key().to_lowercase(); keys.insert(key) // returns True if key is new. }); properties } /// Returns true if `addr` is in the same network of `intf`. pub(crate) fn valid_ip_on_intf(addr: &IpAddr, intf: &Interface) -> bool { match (addr, &intf.addr) { (IpAddr::V4(addr), IfAddr::V4(intf)) => { let netmask = u32::from(intf.netmask); let intf_net = u32::from(intf.ip) & netmask; let addr_net = u32::from(*addr) & netmask; addr_net == intf_net } (IpAddr::V6(addr), IfAddr::V6(intf)) => { let netmask = u128::from(intf.netmask); let intf_net = u128::from(intf.ip) & netmask; let addr_net = u128::from(*addr) & netmask; addr_net == intf_net } _ => false, } } /// Returns true if `addr_a` and `addr_b` are in the same network as `intf`. pub fn valid_two_addrs_on_intf(addr_a: &IpAddr, addr_b: &IpAddr, intf: &Interface) -> bool { match (addr_a, addr_b, &intf.addr) { (IpAddr::V4(ipv4_a), IpAddr::V4(ipv4_b), IfAddr::V4(intf)) => { let netmask = u32::from(intf.netmask); let intf_net = u32::from(intf.ip) & netmask; let net_a = u32::from(*ipv4_a) & netmask; let net_b = u32::from(*ipv4_b) & netmask; net_a == intf_net && net_b == intf_net } (IpAddr::V6(ipv6_a), IpAddr::V6(ipv6_b), IfAddr::V6(intf)) => { let netmask = u128::from(intf.netmask); let intf_net = u128::from(intf.ip) & netmask; let net_a = u128::from(*ipv6_a) & netmask; let net_b = u128::from(*ipv6_b) & netmask; net_a == intf_net && net_b == intf_net } _ => false, } } /// A probing for a particular name. #[derive(Debug)] pub(crate) struct Probe { /// All records probing for the same name. pub(crate) records: Vec, /// The fullnames of services that are probing these records. /// These are the original service names, will not change per conflicts. pub(crate) waiting_services: HashSet, /// The time (T) to send the first query . pub(crate) start_time: u64, /// The time to send the next (including the first) query. pub(crate) next_send: u64, } impl Probe { pub(crate) fn new(start_time: u64) -> Self { // RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.1: // // "250 ms after the first query, the host should send a second; then, // 250 ms after that, a third. If, by 250 ms after the third probe, no // conflicting Multicast DNS responses have been received, the host may // move to the next step, announcing. " let next_send = start_time; Self { records: Vec::new(), waiting_services: HashSet::new(), start_time, next_send, } } /// Add a new record with the same probing name in a sorted order. pub(crate) fn insert_record(&mut self, record: DnsRecordBox) { /* RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2.1 " The records are sorted using the same lexicographical order as described above, that is, if the record classes differ, the record with the lower class number comes first. If the classes are the same but the rrtypes differ, the record with the lower rrtype number comes first." */ let insert_position = self .records .binary_search_by( |existing| match existing.get_class().cmp(&record.get_class()) { std::cmp::Ordering::Equal => existing.get_type().cmp(&record.get_type()), other => other, }, ) .unwrap_or_else(|pos| pos); self.records.insert(insert_position, record); } /// Compares with `incoming` records. Returns `Less` if we yield. pub(crate) fn tiebreaking(&self, incoming: &[&DnsRecordBox]) -> cmp::Ordering { /* RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2 " 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 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 => return other, } } self.records.len().cmp(&incoming.len()) } 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 records. /* When a host is probing for a group of related records with the same name (e.g., the SRV and TXT record describing a DNS-SD service), only a single question need be placed in the Question Section, since query type "ANY" (255) is used, which will elicit answers for all records with that name. However, for tiebreaking to work correctly in all cases, the Authority Section must contain *all* the records and proposed rdata being probed for uniqueness. */ pub(crate) probing: HashMap, /// Already done probing, or no need to probe. pub(crate) active: HashMap>, /// timers of the newly added probes. pub(crate) new_timers: Vec, /// Mapping from original names to new names. pub(crate) name_changes: HashMap, } impl DnsRegistry { pub(crate) fn new() -> Self { Self { probing: HashMap::new(), active: HashMap::new(), new_timers: Vec::new(), name_changes: HashMap::new(), } } pub(crate) fn is_probing_done( &mut self, answer: &T, service_name: &str, start_time: u64, ) -> bool where T: DnsRecordExt + Send + 'static, { if let Some(active_records) = self.active.get(answer.get_name()) { for record in active_records.iter() { if answer.matches(record.as_ref()) { debug!( "found active record {} {}", answer.get_type(), answer.get_name(), ); return true; } } } let probe = self .probing .entry(answer.get_name().to_string()) .or_insert_with(|| { debug!("new probe of {}", answer.get_name()); Probe::new(start_time) }); self.new_timers.push(probe.next_send); for record in probe.records.iter() { if answer.matches(record.as_ref()) { debug!( "found existing record {} in probe of '{}'", answer.get_type(), answer.get_name(), ); probe.waiting_services.insert(service_name.to_string()); return false; // Found existing probe for the same record. } } debug!( "insert record {} into probe of {}", answer.get_type(), answer.get_name(), ); probe.insert_record(answer.clone_box()); probe.waiting_services.insert(service_name.to_string()); false } /// check all records in "probing" and "active": /// if the record is SRV, and hostname is set to original, remove it. /// and create a new SRV with "host" set to "new_name" and put into "probing". pub(crate) fn update_hostname( &mut self, original: &str, new_name: &str, probe_time: u64, ) -> bool { let mut found_records = Vec::new(); let mut new_timer_added = false; for (_name, probe) in self.probing.iter_mut() { probe.records.retain(|record| { if record.get_type() == RRType::SRV { if let Some(srv) = record.any().downcast_ref::() { if srv.host() == original { let mut new_record = srv.clone(); new_record.set_host(new_name.to_string()); found_records.push(new_record); return false; } } } true }); } for (_name, records) in self.active.iter_mut() { records.retain(|record| { if record.get_type() == RRType::SRV { if let Some(srv) = record.any().downcast_ref::() { if srv.host() == original { let mut new_record = srv.clone(); new_record.set_host(new_name.to_string()); found_records.push(new_record); return false; } } } true }); } for record in found_records { let probe = match self.probing.get_mut(record.get_name()) { Some(p) => { p.start_time = probe_time; // restart this probe. p } None => { let new_probe = self .probing .entry(record.get_name().to_string()) .or_insert_with(|| Probe::new(probe_time)); new_timer_added = true; new_probe } }; debug!( "insert record {} with new hostname {new_name} into probe for: {}", record.get_type(), record.get_name() ); probe.insert_record(Box::new(record)); } new_timer_added } } /// 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) } } /// Represents a resolved service as a plain data struct. /// This is from a client (i.e. querier) point of view. #[non_exhaustive] pub struct ResolvedService { /// Service type and domain. For example, "_http._tcp.local." pub ty_domain: String, /// Optional service subtype and domain. /// /// See RFC6763 section 7.1 about "Subtypes": /// /// For example, "_printer._sub._http._tcp.local." pub sub_ty_domain: Option, /// Full name of the service. For example, "my-service._http._tcp.local." pub fullname: String, /// Host name of the service. For example, "my-server1.local." pub host: String, /// Port of the service. I.e. TCP or UDP port. pub port: u16, /// Addresses of the service. IPv4 or IPv6 addresses. pub addresses: HashSet, /// Properties of the service, decoded from TXT record. pub txt_properties: TxtProperties, } impl ResolvedService { /// 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 } } #[cfg(test)] mod tests { use super::{ decode_txt, encode_txt, u8_slice_to_hex, valid_two_addrs_on_intf, ServiceInfo, TxtProperty, }; use if_addrs::{IfAddr, Ifv4Addr, Ifv6Addr, Interface}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[test] fn test_txt_encode_decode() { let properties = vec![ TxtProperty::from(&("key1", "value1")), TxtProperty::from(&("key2", "value2")), ]; // test encode let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!( encoded.len(), "key1=value1".len() + "key2=value2".len() + property_count ); assert_eq!(encoded[0] as usize, "key1=value1".len()); // test decode let decoded = decode_txt(&encoded); assert!(properties[..] == decoded[..]); // test empty value let properties = vec![TxtProperty::from(&("key3", ""))]; let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!(encoded.len(), "key3=".len() + property_count); let decoded = decode_txt(&encoded); assert_eq!(properties, decoded); // test non-string value let binary_val: Vec = vec![123, 234, 0]; let binary_len = binary_val.len(); let properties = vec![TxtProperty::from(("key4", binary_val))]; let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!(encoded.len(), "key4=".len() + binary_len + property_count); let decoded = decode_txt(&encoded); assert_eq!(properties, decoded); // test value that contains '=' let properties = vec![TxtProperty::from(("key5", "val=5"))]; let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!( encoded.len(), "key5=".len() + "val=5".len() + property_count ); let decoded = decode_txt(&encoded); assert_eq!(properties, decoded); // test a property that has no value. let properties = vec![TxtProperty::from("key6")]; let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!(encoded.len(), "key6".len() + property_count); let decoded = decode_txt(&encoded); assert_eq!(properties, decoded); // test very long property. let properties = vec![TxtProperty::from( String::from_utf8(vec![0x30; 1024]).unwrap().as_str(), // A long string of 0 char )]; let property_count = properties.len(); let encoded = encode_txt(properties.iter()); assert_eq!(encoded.len(), 255 + property_count); let decoded = decode_txt(&encoded); assert_eq!( vec![TxtProperty::from( String::from_utf8(vec![0x30; 255]).unwrap().as_str() )], decoded ); } #[test] fn test_set_properties_from_txt() { // Three duplicated keys. let properties = vec![ TxtProperty::from(&("one", "1")), TxtProperty::from(&("ONE", "2")), TxtProperty::from(&("One", "3")), ]; let encoded = encode_txt(properties.iter()); // Simple decode does not remove duplicated keys. let decoded = decode_txt(&encoded); assert_eq!(decoded.len(), 3); // ServiceInfo removes duplicated keys and keeps only the first one. let mut service_info = ServiceInfo::new("_test._tcp", "prop_test", "localhost", "", 1234, None).unwrap(); service_info.set_properties_from_txt(&encoded); assert_eq!(service_info.get_properties().len(), 1); // Verify the only one property. let prop = service_info.get_properties().iter().next().unwrap(); assert_eq!(prop.key, "one"); assert_eq!(prop.val_str(), "1"); } #[test] fn test_u8_slice_to_hex() { let bytes = [0x01u8, 0x02u8, 0x03u8]; let hex = u8_slice_to_hex(&bytes); assert_eq!(hex.as_str(), "0x010203"); let slice = "abcdefghijklmnopqrstuvwxyz"; let hex = u8_slice_to_hex(slice.as_bytes()); assert_eq!(hex.len(), slice.len() * 2 + 2); assert_eq!( hex.as_str(), "0x6162636465666768696a6b6c6d6e6f707172737475767778797a" ); } #[test] fn test_txt_property_debug() { // Test UTF-8 property value. let prop_1 = TxtProperty { key: "key1".to_string(), val: Some("val1".to_string().into()), }; let prop_1_debug = format!("{:?}", &prop_1); assert_eq!( prop_1_debug, "TxtProperty {key: \"key1\", val: Some(\"val1\")}" ); // Test non-UTF-8 property value. let prop_2 = TxtProperty { key: "key2".to_string(), val: Some(vec![150u8, 151u8, 152u8]), }; let prop_2_debug = format!("{:?}", &prop_2); assert_eq!( prop_2_debug, "TxtProperty {key: \"key2\", val: Some(0x969798)}" ); } #[test] fn test_txt_decode_property_size_out_of_bounds() { // Construct a TXT record with an invalid property length that would be out of bounds. let encoded: Vec = vec![ 0x0b, // Length 11 b'k', b'e', b'y', b'1', b'=', b'v', b'a', b'l', b'u', b'e', b'1', // key1=value1 (Length 11) 0x10, // Length 16 (Would be out of bounds) b'k', b'e', b'y', b'2', b'=', b'v', b'a', b'l', b'u', b'e', b'2', // key2=value2 (Length 11) ]; // Decode the record content let decoded = decode_txt(&encoded); // We expect the out of bounds length for the second property to have caused the rest of the record content to be skipped. // Test that we only parsed the first property. assert_eq!(decoded.len(), 1); // Test that the key of the property we parsed is "key1" assert_eq!(decoded[0].key, "key1"); } #[test] fn test_valid_two_addrs_on_intf() { // test IPv4 let ipv4_netmask = Ipv4Addr::new(192, 168, 1, 0); let ipv4_intf_addr = IfAddr::V4(Ifv4Addr { ip: Ipv4Addr::new(192, 168, 1, 10), netmask: ipv4_netmask, prefixlen: 24, broadcast: None, }); let ipv4_intf = Interface { name: "e0".to_string(), addr: ipv4_intf_addr, index: Some(1), #[cfg(windows)] adapter_name: "ethernet".to_string(), }; let ipv4_a = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)); let ipv4_b = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 11)); let result = valid_two_addrs_on_intf(&ipv4_a, &ipv4_b, &ipv4_intf); assert!(result); let ipv4_c = IpAddr::V4(Ipv4Addr::new(172, 17, 0, 1)); let result = valid_two_addrs_on_intf(&ipv4_a, &ipv4_c, &ipv4_intf); assert!(!result); // test IPv6 (generated by AI) let ipv6_netmask = Ipv6Addr::new(0xffff, 0xffff, 0, 0, 0, 0, 0, 0); // Equivalent to /32 prefix length let ipv6_intf_addr = IfAddr::V6(Ifv6Addr { ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), netmask: ipv6_netmask, prefixlen: 32, broadcast: None, }); let ipv6_intf = Interface { name: "eth0".to_string(), addr: ipv6_intf_addr, index: Some(2), #[cfg(windows)] adapter_name: "ethernet".to_string(), }; let ipv6_a = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)); let ipv6_b = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)); let result = valid_two_addrs_on_intf(&ipv6_a, &ipv6_b, &ipv6_intf); assert!(result); // Expect true since both addresses are in the same subnet let ipv6_c = IpAddr::V6(Ipv6Addr::new(0x2002, 0xdb8, 0, 0, 0, 0, 0, 1)); let result = valid_two_addrs_on_intf(&ipv6_a, &ipv6_c, &ipv6_intf); assert!(!result); // Expect false since addresses are in different subnets } } mdns-sd-0.13.3/tests/addr_parse.rs000064400000000000000000000064651046102023000151040ustar 00000000000000use mdns_sd::AsIpAddrs; use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[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 }) ); } mdns-sd-0.13.3/tests/mdns_test.rs000064400000000000000000002207011046102023000147670ustar 00000000000000use if_addrs::{IfAddr, Interface}; use mdns_sd::{ DaemonEvent, DaemonStatus, HostnameResolutionEvent, IfKind, IntoTxtProperties, ServiceDaemon, ServiceEvent, ServiceInfo, TxtProperty, UnregisterStatus, }; use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::thread::sleep; use std::time::{Duration, SystemTime}; use test_log::test; /// This test covers: /// register(announce), browse(query), response, unregister, shutdown. #[test] fn integration_success() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service let ty_domain = "_mdns-sd-it._udp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let all_interfaces = my_ip_interfaces(); println!("all interfaces count: {}", all_interfaces.len()); // as we send only once per interface and ip we need a count of unique addresses to verify number of sent unregisters later on let mut unique_intf_idx_ip_ver_set = HashSet::new(); let mut non_idx_count = 0; for intf in all_interfaces.iter() { let ip_ver = match intf.addr { IfAddr::V4(_) => 4u8, IfAddr::V6(_) => 6u8, }; // use the same approach as `IntfSock.multicast_send_tracker` if let Some(idx) = intf.index { if !unique_intf_idx_ip_ver_set.insert((idx, ip_ver)) { println!("index {idx} IP v{ip_ver} repeated on interface {}, likely multi-addr on the same interface", intf.name); } } else { non_idx_count += 1; } } let unique_intf_idx_ip_ver_count = unique_intf_idx_ip_ver_set.len() + non_idx_count; let ifaddrs_set: HashSet<_> = all_interfaces.iter().map(|intf| intf.ip()).collect(); let my_ifaddrs: Vec<_> = ifaddrs_set.into_iter().collect(); let my_addrs_count = my_ifaddrs.len(); println!("My IP {} addr(s):", my_ifaddrs.len()); for item in my_ifaddrs.iter() { println!("{}", &item); } let host_name = "integration_host.local."; let port = 5200; let mut properties = HashMap::new(); properties.insert("property_1".to_string(), "test".to_string()); properties.insert("property_2".to_string(), "1".to_string()); properties.insert("property_3".to_string(), "1234".to_string()); let my_service = ServiceInfo::new( ty_domain, &instance_name, host_name, &my_ifaddrs[..], port, Some(properties), ) .expect("valid service info"); let fullname = my_service.get_fullname().to_string(); d.register(my_service) .expect("Failed to register our service"); // Browse for a service let mut resolved_ips: HashSet = HashSet::new(); let mut addr_count = 0; let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); while let Ok(event) = browse_chan.recv_timeout(timeout) { match event { ServiceEvent::SearchStarted(ty_domain) => { println!("Search started for {}", &ty_domain); } ServiceEvent::ServiceFound(_ty_domain, fullname) => { println!("Found a new service: {}", &fullname); } ServiceEvent::ServiceResolved(info) => { let addrs = info.get_addresses(); 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 addr_set = info.get_addresses(); addr_count = addr_set.len(); 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()) ); let host_ttl = info.get_host_ttl(); assert_eq!(host_ttl, 120); // default value. let other_ttl = info.get_other_ttl(); assert_eq!(other_ttl, 4500); // default value. } _ => {} } } // 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.len() >= 1); // Unregister the service let receiver = d.unregister(&fullname).unwrap(); let response = receiver.recv().unwrap(); assert!(matches!(response, UnregisterStatus::OK)); let mut remove_count = 0; 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; } _ => {} } } assert_eq!(remove_count, 1); // 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) { match event { ServiceEvent::SearchStopped(ty) => { println!("Search stopped for {}", &ty); stopped_count += 1; break; } _ => {} } } assert_eq!(stopped_count, 1); // Verify metrics. let metrics_receiver = d.get_metrics().unwrap(); let metrics = metrics_receiver.recv().unwrap(); println!("metrics: {:?}", &metrics); assert_eq!(metrics["register"], 1); assert_eq!(metrics["unregister"], 1); assert!(metrics["register-resend"] >= 1); println!("unique interface set: {:?}", unique_intf_idx_ip_ver_set); assert_eq!( metrics["unregister-resend"], unique_intf_idx_ip_ver_count as i64 ); assert!(metrics["browse"] >= 2); // browse has been retransmitted. // respond has been sent for every browse, or they are suppressed by "known answer". let respond_count = metrics.get("respond").unwrap_or(&0); let known_answer_count = metrics.get("known-answer-suppression").unwrap_or(&0); assert!(*respond_count >= 2 || *known_answer_count > 0); // Test the special meta-query of "_services._dns-sd._udp.local." let service2_type = "_my-service2._udp.local."; let service2_instance = "instance2"; let service2 = ServiceInfo::new( service2_type, service2_instance, host_name, &my_ifaddrs[..], port, None, ) .expect("valid service info"); d.register(service2) .expect("Failed to register the 2nd service"); // Browse using the special meta-query. let meta_query = "_services._dns-sd._udp.local."; let browse_chan = d.browse(meta_query).unwrap(); let timeout = Duration::from_secs(2); loop { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceFound(ty_domain, fullname) => { println!("Found a service of {}: {}", &ty_domain, &fullname); // Among all services found, should have our 2nd service. if fullname == service2_type { break; } } e => { println!("Received event {:?}", e); sleep(Duration::from_millis(100)); } }, Err(e) => { panic!("browse error: {}", e); } } } // Shutdown d.shutdown().unwrap(); } #[test] fn service_without_properties_with_alter_net_v4() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service without properties. let ty_domain = "_serv-no-prop._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let if_addrs: Vec = my_ip_interfaces() .into_iter() .filter(|iface| iface.addr.ip().is_ipv4()) .collect(); let first_ip = if_addrs[0].ip(); let alter_ip = ipv4_alter_net(&if_addrs); let host_ip = vec![first_ip, alter_ip]; let host_name = "serv-no-prop-v4.local."; let port = 5201; let my_service = ServiceInfo::new( ty_domain, &instance_name, host_name, &host_ip[..], port, None, ) .expect("valid service info"); let fullname = my_service.get_fullname().to_string(); d.register(my_service) .expect("Failed to register our service"); println!("Registered service with host_ip: {:?}", &host_ip); // Browse for a service let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); let timer = std::time::Instant::now() + timeout; let mut found = false; while std::time::Instant::now() < timer { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), info.get_addresses() ); // match only our service and not v6 one if info.get_addresses_v4().is_empty() { continue; } if fullname.as_str() == info.get_fullname() { let addrs = info.get_addresses_v4(); assert_eq!(addrs.len(), 1); // first_ipv4 but no alter_ipv. found = true; break; } } e => { println!("Received event {:?}", e); } }, Err(e) => { panic!("browse error: {}", e); } } } d.shutdown().unwrap(); assert!(found); } #[test] fn service_without_properties_with_alter_net_v6() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service without properties. let ty_domain = "_serv-no-prop._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let if_addrs: Vec = my_ip_interfaces() .into_iter() .filter(|iface| iface.addr.ip().is_ipv6()) .collect(); let first_ip = if_addrs[0].ip(); let alter_ip = ipv6_alter_net(&if_addrs); let host_ip = vec![first_ip, alter_ip]; let host_name = "serv-no-prop-v6.local."; let port = 5201; let my_service = ServiceInfo::new( ty_domain, &instance_name, host_name, &host_ip[..], port, None, ) .expect("valid service info"); let fullname = my_service.get_fullname().to_string(); d.register(my_service) .expect("Failed to register our service"); println!("Registered service with host_ip: {:?}", &host_ip); // Browse for a service let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); let timer = std::time::Instant::now() + timeout; let mut found = false; while std::time::Instant::now() < timer { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), info.get_addresses() ); // match only our service and not v4 one if fullname.as_str() == info.get_fullname() { let addrs: Vec<&IpAddr> = 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 = vec![("key1", "val1"), ("key2", "val2")].into_txt_properties(); let property_map = properties.into_property_map_str(); println!("property_map: {:?}", property_map); assert_eq!(property_map.len(), 2); assert_eq!(property_map.get("key1"), Some(&"val1".to_string())); assert_eq!(property_map.get("key2"), Some(&"val2".to_string())); // Test property with no value and property with invalid UTF-8 let invalid_vec: Vec = vec![200, 200]; // Invalid UTF-8 bytes let prop1 = TxtProperty::from("key1"); let prop2 = TxtProperty::from(("key2", invalid_vec.as_slice())); let properties = vec![prop1, prop2].into_txt_properties(); let property_map = properties.into_property_map_str(); // Property with no value should map to empty string // Property with invalid UTF-8 should be skipped assert_eq!(property_map.get("key1"), Some(&"".to_string())); assert_eq!(property_map.len(), 1); } #[test] fn test_into_txt_properties() { // Verify (&str, String) tuple is supported. let properties = vec![("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 = vec![(String::from("key2"), String::from("val2"))]; let txt_props = properties.into_txt_properties(); assert_eq!(txt_props.get_property_val_str("key2").unwrap(), "val2"); } #[test] fn test_info_as_resolved_service() { let sub_ty_domain = "_printer._sub._test._tcp.local."; let service_info = ServiceInfo::new( sub_ty_domain, "my_instance", "my_host.local.", "192.168.0.1", 5200, None, ) .unwrap(); let resolved_service = service_info.as_resolved_service(); assert!(resolved_service.is_valid()); assert_eq!(resolved_service.sub_ty_domain.unwrap(), sub_ty_domain); assert_eq!(resolved_service.ty_domain, "_test._tcp.local."); let info_missing_addr = ServiceInfo::new( "_test._tcp.local.", "my_instance", "my_host.local.", "", 5200, None, ) .unwrap(); let invalid_service = info_missing_addr.as_resolved_service(); assert!(!invalid_service.is_valid()); assert!(invalid_service.sub_ty_domain.is_none()); } /// Test enabling an interface using its name, for example "en0". /// Also tests an instance name with Upper Case. #[test] fn service_with_named_interface_only() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // First, disable all interfaces. d.disable_interface(IfKind::All).unwrap(); // Register a service with a name len > 15. let my_ty_domain = "_named_intf_only._udp.local."; let host_name = "named_intf_host.local."; let host_ipv4 = ""; let port = 5202; let my_service = ServiceInfo::new( my_ty_domain, "UpperCaseInstance", host_name, host_ipv4, port, None, ) .expect("invalid service info") .enable_addr_auto(); d.register(my_service).unwrap(); // Browse for a service and verify all addresses are IPv4. let browse_chan = d.browse(my_ty_domain).unwrap(); let timeout = Duration::from_secs(2); let mut resolved = false; while let Ok(event) = browse_chan.recv_timeout(timeout) { match event { ServiceEvent::ServiceResolved(info) => { let addrs = info.get_addresses(); resolved = true; println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), addrs ); break; } e => { println!("Received event {:?}", e); } } } assert!(!resolved); // Second, find an interface. let if_addrs: Vec = my_ip_interfaces() .into_iter() .filter(|iface| iface.addr.ip().is_ipv4()) .collect(); let if_name = if_addrs[0].name.clone(); // Enable the named interface. println!("Enable interface with name {}", &if_name); d.enable_interface(&if_name).unwrap(); // Browse again. let browse_chan = d.browse(my_ty_domain).unwrap(); let timeout = Duration::from_secs(3); let mut resolved = false; while let Ok(event) = browse_chan.recv_timeout(timeout) { match event { ServiceEvent::ServiceResolved(info) => { let addrs = info.get_addresses(); resolved = true; println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), addrs ); break; } e => { println!("Received event {:?}", e); } } } assert!(resolved); d.shutdown().unwrap(); } #[test] fn service_with_ipv4_only() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Disable IPv6, so the daemon is IPv4 only now. d.disable_interface(IfKind::IPv6).unwrap(); // Register a service with a name len > 15. let service_ipv4_only = "_test_ipv4_only._udp.local."; let host_name = "my_host_ipv4_only.local."; let host_ipv4 = ""; let port = 5201; let my_service = ServiceInfo::new( service_ipv4_only, "my_instance", host_name, host_ipv4, port, None, ) .expect("invalid service info") .enable_addr_auto(); let result = d.register(my_service); assert!(result.is_ok()); // Browse for a service and verify all addresses are IPv4. let browse_chan = d.browse(service_ipv4_only).unwrap(); let timeout = Duration::from_secs(2); let mut resolved = false; // run till the timeout and collect the resolved addresses // from all enabled interfaces. while let Ok(event) = browse_chan.recv_timeout(timeout) { match event { ServiceEvent::ServiceResolved(info) => { let addrs = info.get_addresses(); resolved = true; println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), addrs ); assert!(!info.get_addresses().is_empty()); for addr in info.get_addresses().iter() { assert!(addr.is_ipv4()); } // We don't break here, as there could be more addresses coming. } e => { println!("Received event {:?}", e); } } } assert!(resolved); d.shutdown().unwrap(); } #[test] fn 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 service_ip_addr = my_ip_interfaces() .iter() .find(|iface| iface.ip().is_ipv4()) .map(|iface| iface.ip()) .unwrap(); let host_name = "disabled_intf_host.local."; let port = 5201; let my_service = ServiceInfo::new( &ty_domain, &instance_name, host_name, &service_ip_addr, 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. client.disable_interface(service_ip_addr).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. loop { match handle.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; } _ => {} }, Err(_) => { break; } } } // We cannot resolve the service because the interface is disabled. assert!(!resolved); // Clean up. server.shutdown().unwrap(); client.shutdown().unwrap(); } #[test] fn service_with_invalid_addr_v4() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service without properties. let ty_domain = "_invalid-addr._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let if_addrs: Vec = my_ip_interfaces() .into_iter() .filter(|iface| iface.addr.ip().is_ipv4()) .collect(); let alter_ip = ipv4_alter_net(&if_addrs); let host_name = "invalid_ipv4_host.local."; let port = 5201; let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None) .expect("valid service info"); d.register(my_service) .expect("Failed to register our service"); // Browse for a service let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); let mut resolved = false; loop { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), info.get_addresses() ); resolved = true; break; } e => { println!("Received event {:?}", e); } }, Err(e) => { println!("browse error: {}", e); break; } } } d.shutdown().unwrap(); // We cannot resolve the service because the published address // is not valid in the LAN. assert!(!resolved); } #[test] fn service_with_invalid_addr_v6() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service without properties. let ty_domain = "_invalid-addr._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let if_addrs: Vec = my_ip_interfaces() .into_iter() .filter(|iface| iface.addr.ip().is_ipv6()) .collect(); let alter_ip = ipv6_alter_net(&if_addrs); let host_name = "my_host.local."; let port = 5201; let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None) .expect("valid service info"); d.register(my_service) .expect("Failed to register our service"); // Browse for a service let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); let mut resolved = false; loop { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} addr(s): {:?}", &info.get_fullname(), info.get_addresses() ); resolved = true; break; } e => { println!("Received event {:?}", e); } }, Err(e) => { println!("browse error: {}", e); break; } } } d.shutdown().unwrap(); // We cannot resolve the service because the published address // is not valid in the LAN. assert!(!resolved); } #[test] fn service_with_loopback_addr() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); d.enable_interface(IfKind::LoopbackV4) .expect("Failed to enable loopback interface"); // Define a unique service type and instance name. let ty_domain = "_test-loopback._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Use a loopback address (127.0.0.1) for the service. let loopback_ip: IpAddr = "127.0.0.1".parse().unwrap(); let host_name = "localhost.local."; let port = 5201; let my_service = ServiceInfo::new( ty_domain, &instance_name, host_name, loopback_ip, port, None, ) .expect("valid service info"); d.register(my_service) .expect("Failed to register our service"); // Browse for the service. let browse_chan = d.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); let mut found_loopback = false; loop { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved service {} with addresses: {:?}", info.get_fullname(), info.get_addresses() ); // Check that at least one of the addresses is a loopback address. if info.get_addresses().iter().any(|ip| ip.is_loopback()) { found_loopback = true; } break; } e => { println!("Received event {:?}", e); } }, Err(e) => { println!("browse error: {}", e); break; } } } d.shutdown().unwrap(); // Assert that the resolved service includes a loopback address. assert!( found_loopback, "The service should include a loopback address" ); } #[test] fn subtype() { // Create a daemon let d = ServiceDaemon::new().expect("Failed to create daemon"); // Register a service with a subdomain let subtype_domain = "_directory._sub._test-subtype._tcp.local."; let ty_domain = "_test-subtype._tcp.local."; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); // Create a unique name. let host_ipv4 = my_ip_interfaces()[0].ip().to_string(); let host_name = "subtype_host.local."; let port = 5201; let my_service = ServiceInfo::new( subtype_domain, &instance_name, host_name, host_ipv4, port, None, ) .expect("valid service info"); let fullname = my_service.get_fullname().to_string(); d.register(my_service) .expect("Failed to register our service"); // Browse for the service via ty_domain and subtype_domain for domain in [ty_domain, subtype_domain].iter() { let browse_chan = d.browse(domain).unwrap(); let timeout = Duration::from_secs(2); loop { match browse_chan.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} subdomain {:?}", &info.get_fullname(), info.get_subtype() ); assert_eq!(fullname.as_str(), info.get_fullname()); assert_eq!(subtype_domain, info.get_subtype().as_ref().unwrap()); break; } e => { println!("Received event {:?}", e); } }, Err(e) => { panic!("browse error: {}", e); } } } } d.shutdown().unwrap(); } /// Verify service name has to be valid. #[test] fn test_service_name_check() { // Create a daemon for the server. let server_daemon = ServiceDaemon::new().expect("Failed to create server daemon"); let monitor = server_daemon.monitor().unwrap(); // Register a service with a name len > 15. let service_name_too_long = "_service-name-too-long._udp.local."; let host_ipv4 = ""; let host_name = "my_host.local."; let port = 5200; let mut my_service = ServiceInfo::new( service_name_too_long, "my_instance", host_name, host_ipv4, port, None, ) .expect("valid service info") .enable_addr_auto(); my_service.set_requires_probe(false); let result = server_daemon.register(my_service.clone()); assert!(result.is_ok()); // Verify that the daemon reported error. let event = monitor.recv_timeout(Duration::from_millis(500)).unwrap(); assert!(matches!(event, DaemonEvent::Error(_))); if let DaemonEvent::Error(e) = event { println!("Daemon error: {}", e) } // Verify that we can increase the service name length max. server_daemon.set_service_name_len_max(30).unwrap(); let result = server_daemon.register(my_service); assert!(result.is_ok()); // Verify that the service was published successfully. let mut published = false; let publish_timeout = 1200; while let Ok(event) = monitor.recv_timeout(Duration::from_millis(publish_timeout)) { match event { DaemonEvent::Announce(_, _) => { published = true; break; } other => { println!("other daemon events: {:?}", other); } } } assert!(published); // Check for the internal upper limit of service name length max. let r = server_daemon.set_service_name_len_max(31); assert!(r.is_err()); server_daemon.shutdown().unwrap(); } #[test] fn service_new_publish_after_browser() { let service_type = "_new-pub._udp.local."; let daemon = ServiceDaemon::new().expect("Failed to create a new daemon"); // First, starts the browser. let receiver = daemon.browse(service_type).unwrap(); sleep(Duration::from_millis(1000)); let txt_properties = [("key1", "value1")]; let service_info = ServiceInfo::new( "_new-pub._udp.local.", "test1", "my_host.local.", "", 1234, &txt_properties[..], ) .expect("valid service info") .enable_addr_auto(); // Second, publish a service. let result = daemon.register(service_info); assert!(result.is_ok()); let mut resolved = false; let timeout = Duration::from_secs(2); loop { match receiver.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service of {} addr(s): {:?} props: {:?}", &info.get_fullname(), info.get_addresses(), info.get_properties() ); resolved = true; break; } e => { println!("Received event {:?}", e); } }, Err(e) => { println!("browse error: {}", e); break; } } } assert!(resolved); daemon.shutdown().unwrap(); } // This test covers the sanity check in `read_others` decoding RDATA. #[test] fn instance_name_two_dots() { // Create a daemon for the server. let server_daemon = ServiceDaemon::new().expect("Failed to create server daemon"); let monitor = server_daemon.monitor().unwrap(); // Register an instance name with a ending dot. // Then the full name will have two dots in the middle. // This would create a PTR record RDATA with a skewed name field. let service_type = "_two-dots._udp.local."; let instance_name = "my_instance."; let host_ipv4 = ""; let host_name = "my_host.local."; let port = 5200; let my_service = ServiceInfo::new( service_type, instance_name, host_name, host_ipv4, port, None, ) .expect("valid service info") .enable_addr_auto(); let result = server_daemon.register(my_service.clone()); 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); // Browse the service. let receiver = server_daemon.browse(service_type).unwrap(); let mut resolved = false; let timeout = Duration::from_secs(2); loop { match receiver.recv_timeout(timeout) { Ok(event) => match event { ServiceEvent::ServiceResolved(_) => { resolved = true; break; } e => { println!("Received event {:?}", e); } }, Err(e) => { println!("browse error: {}", e); break; } } } assert!(!resolved); server_daemon.shutdown().unwrap(); } fn my_ip_interfaces() -> Vec { // Use a random port for binding test. let test_port = fastrand::u16(8000u16..9000u16); if_addrs::get_if_addrs() .unwrap_or_default() .into_iter() .filter_map(|i| { if i.is_loopback() { 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, test_port)) { 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, test_port, 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 = my_ip_interfaces() .iter() .find(|iface| iface.ip().is_ipv4()) .map(|iface| iface.ip()) .unwrap(); let my_service = ServiceInfo::new( "_host_res_test._tcp.local.", "my_instance", hostname, &[service_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 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 = vec![("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 { assert!(false); } // 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, &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 annoucement // 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, &ip_addr1); resolved = true; break; } } e => { println!("Received event {:?}", e); } } } 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) { match event { ServiceEvent::ServiceResolved(info) => { 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 { assert!(false); return; }; 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) { match event { ServiceEvent::ServiceResolved(info) => { 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 { assert!(false); return; }; 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. let server1_monitor = server1.monitor().unwrap(); let timeout = Duration::from_secs(2); 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) { match event { ServiceEvent::ServiceResolved(info) => { 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, &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 { assert!(false); return; }; 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) { match event { ServiceEvent::ServiceResolved(info) => { 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) { match event { ServiceEvent::ServiceResolved(info) => { 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) { match event { ServiceEvent::ServiceRemoved(service_type, fullname) => { 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) { match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service: {} host {} IP {:?}", info.get_fullname(), info.get_hostname(), info.get_addresses_v4() ); resolved = true; break; } _ => {} } } assert_eq!(resolved, false); // 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) { match event { ServiceEvent::ServiceResolved(info) => { 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) { match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service: {} host {} IP {:?}", info.get_fullname(), info.get_hostname(), info.get_addresses() ); resolved = true; break; } _ => {} } } assert_eq!(resolved, false); // 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) { match event { ServiceEvent::ServiceResolved(info) => { println!( "Resolved a service: {} host {} IP {:?}", info.get_fullname(), info.get_hostname(), info.get_addresses() ); resolved = true; break; } _ => {} } } assert!(resolved); } /// 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); }