├── .github └── workflows │ ├── build.yml │ ├── ci.yml │ ├── prs.yml │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── azure-pipelines.yml ├── build.rs ├── examples ├── browse-drop.rs ├── browse.rs └── register.rs └── src ├── browse.rs ├── ffi.rs ├── ffi ├── apple.rs └── windows.rs ├── lib.rs ├── non_blocking.rs ├── os ├── apple.rs ├── apple │ ├── browse.rs │ ├── register.rs │ └── txt.rs ├── mod.rs ├── windows.rs └── windows │ ├── browse.rs │ └── register.rs └── register.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | strategy: 9 | matrix: 10 | os: ["macos-latest", "windows-latest", "ubuntu-latest"] 11 | runs-on: '${{ matrix.os }}' 12 | name: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Linux deps 17 | run: sudo apt install -y libavahi-compat-libdnssd-dev libavahi-compat-libdnssd1 18 | if: ${{ matrix.os == 'ubuntu-latest' }} 19 | - name: iOS Rust target 20 | run: rustup target add aarch64-apple-ios 21 | - name: Build 22 | run: cargo build --all 23 | - name: Build for iOS 24 | run: cargo build --workspace --all-features --target aarch64-apple-ios 25 | if: ${{ matrix.os == 'macos-latest' }} 26 | - name: Run Clippy 27 | uses: actions-rs/clippy-check@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ 'main', 'develop', 'release/**' ] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | uses: ./.github/workflows/build.yml 11 | secrets: inherit 12 | test: 13 | name: Test 14 | uses: ./.github/workflows/test.yml 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/prs.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: [ '**' ] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | uses: ./.github/workflows/build.yml 11 | secrets: inherit 12 | test: 13 | name: Cargo Test 14 | uses: ./.github/workflows/test.yml 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | os: ["macos-latest", "windows-latest"] 11 | runs-on: '${{ matrix.os }}' 12 | name: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Test 17 | run: cargo test --all 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "astro-dnssd" 3 | version = "0.3.5" 4 | authors = ["Jeremy Knope "] 5 | description = "Simple & safe DNS-SD wrapper" 6 | keywords = ["dns-sd", "dnssd", "bonjour", "zeroconf", "mdns"] 7 | categories = ["network-programming", "api-bindings"] 8 | repository = "https://github.com/AstroHQ/astro-dnssd" 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | log = "0.4.8" 15 | thiserror = "1.0.20" 16 | 17 | [target.'cfg(target_os = "windows")'.dependencies] 18 | winapi = { version = "0.3", features = ["winsock2"] } 19 | widestring = "1.0.2" 20 | 21 | [target.'cfg(not(target_os = "windows"))'.dependencies] 22 | libc = "0.2" 23 | 24 | [build-dependencies] 25 | pkg-config = "0.3.9" 26 | 27 | [dev-dependencies] 28 | env_logger = "0.11" 29 | 30 | [features] 31 | default = [] 32 | win-bonjour = [] 33 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro DNS-SD 2 | 3 | [![Build Status](https://dev.azure.com/AstroHQ/astro-dnssd/_apis/build/status/AstroHQ.astro-dnssd?branchName=master)](https://dev.azure.com/AstroHQ/astro-dnssd/_build/latest?definitionId=1&branchName=master) 4 | [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/AstroHQ/astro-dnssd) 5 | [![Cargo](https://img.shields.io/crates/v/astro-dnssd.svg)](https://crates.io/crates/astro-dnssd) 6 | [![Documentation](https://docs.rs/astro-dnssd/badge.svg)](https://docs.rs/astro-dnssd) 7 | 8 | Minimal but friendly safe wrapper around dns-sd(Bonjour, mDNS, Zeroconf DNS) APIs. 9 | 10 | [Documentation](https://crates.io/crates/astro-dnssd) 11 | 12 | ## Features 13 | 14 | ### Complete 15 | 16 | - Service registration 17 | - TXTRecord support for service registration via HashMap 18 | - Service browsing 19 | 20 | ### Todo 21 | 22 | - Record creation 23 | - Name resolution 24 | - Port map 25 | - Tests 26 | - Documentation 27 | 28 | ## Build Requirements 29 | `astro-dnssd` requires the Bonjour SDK (as of 0.3 on windows, it's optional, see win-bonjour feature flag) 30 | 31 | - **Windows:** Download the SDK [here]( https://developer.apple.com/bonjour/) 32 | - **Linux:** Install `avahi-compat-libdns_sd` for your distro of choice. 33 | 34 | ## Technical Background 35 | This [website](http://www.dns-sd.org/) provides a good overview of the DNS-SD protocol. 36 | 37 | ## Example 38 | 39 | ```rust 40 | use astro_dnssd::DNSServiceBuilder; 41 | use env_logger::Env; 42 | use std::thread::sleep; 43 | use std::time::Duration; 44 | 45 | fn main() { 46 | env_logger::from_env(Env::default().default_filter_or("trace")).init(); 47 | println!("Registering service..."); 48 | let service = DNSServiceBuilder::new("_http._tcp", 8080) 49 | .with_key_value("status".into(), "open".into()) 50 | .register(); 51 | 52 | { 53 | match service { 54 | Ok(service) => { 55 | println!("Registered... waiting 20s"); 56 | sleep(Duration::from_secs(20)); 57 | println!("Dropping... {:?}", service); 58 | } 59 | Err(e) => { 60 | println!("Error registering: {:?}", e); 61 | } 62 | } 63 | } 64 | log::info!("Drop should have happened"); 65 | sleep(Duration::from_secs(5)); 66 | } 67 | 68 | ``` 69 | 70 | ## License 71 | 72 | Licensed under either of 73 | 74 | - Apache License, Version 2.0 75 | ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 76 | - MIT license 77 | ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 78 | 79 | at your option. 80 | 81 | ## Contribution 82 | 83 | Unless you explicitly state otherwise, any contribution intentionally submitted 84 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 85 | dual licensed as above, without any additional terms or conditions. 86 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | strategy: 5 | matrix: 6 | windows-stable: 7 | imageName: 'windows-latest' 8 | rustup_toolchain: stable 9 | mac-stable: 10 | imageName: 'macos-latest' 11 | rustup_toolchain: stable 12 | linux-stable: 13 | imageName: 'ubuntu-latest' 14 | rustup_toolchain: stable 15 | 16 | pool: 17 | vmImage: $(imageName) 18 | 19 | steps: 20 | # - script: | 21 | # choco install bonjour 22 | # displayName: Install Windows dependencies 23 | # condition: eq( variables['Agent.OS'], 'Windows_NT' ) 24 | - script: | 25 | curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN 26 | echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" 27 | displayName: Install rust 28 | condition: ne( variables['Agent.OS'], 'Windows_NT' ) 29 | - script: | 30 | curl -sSf -o rustup-init.exe https://win.rustup.rs 31 | rustup-init.exe -y --default-host x86_64-pc-windows-msvc 32 | echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin" 33 | displayName: Windows install rust 34 | condition: eq( variables['Agent.OS'], 'Windows_NT' ) 35 | - script: | 36 | sudo apt install -y libavahi-compat-libdnssd-dev libavahi-compat-libdnssd1 37 | displayName: Install Linux dependencies 38 | condition: eq( variables['Agent.OS'], 'Linux' ) 39 | - script: cargo build --all 40 | displayName: Cargo build 41 | - script: cargo test --all 42 | displayName: Cargo test 43 | condition: ne( variables['Agent.OS'], 'Linux' ) # disable linux testing until we fix it 44 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env::{var, var_os}; 2 | use std::path::PathBuf; 3 | 4 | fn cfg_arch() -> String { 5 | var("CARGO_CFG_TARGET_ARCH").expect("couldn't find target architecture") 6 | } 7 | 8 | fn cfg_family_is(family: &str) -> bool { 9 | var_os("CARGO_CFG_TARGET_FAMILY").unwrap() == *family 10 | } 11 | 12 | fn cfg_os_is(family: &str) -> bool { 13 | var_os("CARGO_CFG_TARGET_OS").unwrap() == *family 14 | } 15 | 16 | fn find_avahi_compat_dns_sd() { 17 | // on unix but not darwin link avahi compat 18 | if cfg_family_is("unix") && !(cfg_os_is("macos") || cfg_os_is("ios")) { 19 | pkg_config::probe_library("avahi-compat-libdns_sd").unwrap(); 20 | } 21 | } 22 | 23 | fn dns_sd_lib_path() -> Option { 24 | if cfg!(feature = "win-bonjour") && cfg_family_is("windows") { 25 | let platform = match cfg_arch().as_str() { 26 | "x86_64" => "x64", 27 | "x86" => "Win32", 28 | arch => panic!("unsupported target architecture: {:?}", arch), 29 | }; 30 | match var("BONJOUR_SDK_HOME") { 31 | Ok(path) => Some(PathBuf::from(path).join("Lib").join(platform)), 32 | Err(e) => panic!("Can't find Bonjour SDK (download from https://developer.apple.com/bonjour/ ) at BONJOUR_SDK_HOME: {}", e), 33 | } 34 | } else { 35 | None 36 | } 37 | } 38 | 39 | fn find_windows_dns_sd() { 40 | if let Some(path) = dns_sd_lib_path() { 41 | println!("cargo:rustc-link-search=native={}", path.display()) 42 | } 43 | if cfg!(feature = "win-bonjour") && cfg_family_is("windows") { 44 | println!("cargo:rustc-link-lib=dnssd"); 45 | } else if cfg_family_is("windows") { 46 | println!("cargo:rustc-link-lib=dnsapi"); 47 | } 48 | } 49 | 50 | fn main() { 51 | println!("cargo:rerun-if-changed=build.rs"); 52 | find_avahi_compat_dns_sd(); 53 | find_windows_dns_sd(); 54 | } 55 | -------------------------------------------------------------------------------- /examples/browse-drop.rs: -------------------------------------------------------------------------------- 1 | use astro_dnssd::{BrowseError, ServiceBrowserBuilder, ServiceEventType}; 2 | use env_logger::Env; 3 | use log::{error, info}; 4 | use std::io::ErrorKind; 5 | use std::time::{Duration, Instant}; 6 | 7 | fn main() { 8 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 9 | info!("Starting browser..."); 10 | let start = Instant::now(); 11 | { 12 | let browser = ServiceBrowserBuilder::new("_http._tcp").browse(); 13 | match browser { 14 | Ok(browser) => { 15 | info!("Browser started!"); 16 | loop { 17 | match browser.recv_timeout(Duration::from_millis(500)) { 18 | Ok(service) => { 19 | if service.event_type == ServiceEventType::Added { 20 | info!("Service found: {:?}", service); 21 | } else { 22 | info!("Service left: {}", service.hostname); 23 | } 24 | } 25 | Err(BrowseError::IoError(e)) if e.kind() == ErrorKind::TimedOut => { 26 | std::thread::sleep(Duration::from_millis(100)); 27 | } 28 | Err(BrowseError::Timeout) => { 29 | if start.elapsed() > Duration::from_secs(5) { 30 | info!("Exiting browser loop to test drop"); 31 | break; 32 | } 33 | std::thread::sleep(Duration::from_millis(100)); 34 | } 35 | Err(e) => { 36 | error!("Error receiving browser service: {:?}", e); 37 | std::thread::sleep(Duration::from_millis(100)); 38 | } 39 | } 40 | } 41 | } 42 | Err(e) => { 43 | error!("Error starting browser: {:?}", e); 44 | } 45 | } 46 | } 47 | info!("Browser should be dropped!"); 48 | std::thread::sleep(Duration::from_secs(10)); 49 | } 50 | -------------------------------------------------------------------------------- /examples/browse.rs: -------------------------------------------------------------------------------- 1 | use env_logger::Env; 2 | use log::{error, info}; 3 | // use std::net::ToSocketAddrs; 4 | use astro_dnssd::{BrowseError, ServiceBrowserBuilder, ServiceEventType}; 5 | use std::io::ErrorKind; 6 | use std::time::Duration; 7 | 8 | fn main() { 9 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 10 | info!("Starting browser..."); 11 | let browser = ServiceBrowserBuilder::new("_http._tcp").browse(); 12 | match browser { 13 | Ok(browser) => { 14 | info!("Browser started!"); 15 | loop { 16 | match browser.recv_timeout(Duration::from_millis(500)) { 17 | Ok(service) => { 18 | if service.event_type == ServiceEventType::Added { 19 | info!("Service found: {:?}", service); 20 | } else { 21 | info!("Service left: {}", service.hostname); 22 | } 23 | } 24 | Err(BrowseError::IoError(e)) if e.kind() == ErrorKind::TimedOut => { 25 | std::thread::sleep(Duration::from_millis(100)); 26 | } 27 | Err(BrowseError::Timeout) => { 28 | std::thread::sleep(Duration::from_millis(100)); 29 | } 30 | Err(e) => { 31 | error!("Error receiving browser service: {:?}", e); 32 | std::thread::sleep(Duration::from_millis(100)); 33 | } 34 | } 35 | } 36 | } 37 | Err(e) => { 38 | error!("Error starting browser: {:?}", e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/register.rs: -------------------------------------------------------------------------------- 1 | // use astro_dnssd::register::DNSServiceBuilder; 2 | use astro_dnssd::DNSServiceBuilder; 3 | use env_logger::Env; 4 | use std::thread::sleep; 5 | use std::time::Duration; 6 | 7 | fn main() { 8 | env_logger::Builder::from_env(Env::default().default_filter_or("trace")).init(); 9 | println!("Registering service..."); 10 | let service = DNSServiceBuilder::new("_http._tcp", 8080) 11 | .with_key_value("status".into(), "open".into()) 12 | .register(); 13 | 14 | { 15 | match service { 16 | Ok(service) => { 17 | println!("Registered... waiting 20s"); 18 | sleep(Duration::from_secs(20)); 19 | println!("Dropping... {:?}", service); 20 | } 21 | Err(e) => { 22 | println!("Error registering: {:?}", e); 23 | } 24 | } 25 | } 26 | log::info!("Drop should have happened"); 27 | sleep(Duration::from_secs(5)); 28 | } 29 | -------------------------------------------------------------------------------- /src/browse.rs: -------------------------------------------------------------------------------- 1 | pub use crate::os::{BrowseError, ServiceBrowser}; 2 | use std::collections::HashMap; 3 | 4 | /// Service browsing result type 5 | pub type Result = std::result::Result; 6 | 7 | /// Type of service event from browser, if a service is being added or removed from network 8 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 9 | pub enum ServiceEventType { 10 | /// Service has been added to the network 11 | Added, 12 | /// Service is removed from the network 13 | Removed, 14 | } 15 | 16 | /// Encapsulates information about a service 17 | #[derive(Debug)] 18 | pub struct Service { 19 | /// Name of service, usually a user friendly name 20 | pub name: String, 21 | /// Registration type, i.e. _http._tcp. 22 | pub regtype: String, 23 | /// Interface index (unsure what this is for) 24 | pub interface_index: Option, 25 | /// Domain service is on, typically local. 26 | pub domain: String, 27 | /// Whether this service is being added or not 28 | pub event_type: ServiceEventType, 29 | // /// Full name of service 30 | // pub full_name: String, 31 | /// Hostname of service, usable with gethostbyname() 32 | pub hostname: String, 33 | /// Port service is on 34 | pub port: u16, 35 | /// TXT record service has if any 36 | pub txt_record: Option>, 37 | } 38 | 39 | /// Builder for creating a browser, allowing optionally specifying a domain with chaining (maybe builder is excessive) 40 | pub struct ServiceBrowserBuilder { 41 | pub(crate) regtype: String, 42 | pub(crate) domain: Option, 43 | } 44 | 45 | impl ServiceBrowserBuilder { 46 | /// Creates new service browser for given service type, i.e. ._http._tcp 47 | pub fn new(regtype: &str) -> ServiceBrowserBuilder { 48 | ServiceBrowserBuilder { 49 | regtype: String::from(regtype), 50 | domain: None, 51 | } 52 | } 53 | /// Adds a specified domain to browser's search 54 | pub fn with_domain(mut self, domain: &str) -> ServiceBrowserBuilder { 55 | self.domain = Some(String::from(domain)); 56 | self 57 | } 58 | /// Starts the browser 59 | pub fn browse(self) -> Result { 60 | crate::os::browse(self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "win-bonjour", not(target_os = "windows")))] 2 | pub(crate) mod apple; 3 | #[cfg(windows)] 4 | pub(crate) mod windows; 5 | -------------------------------------------------------------------------------- /src/ffi/apple.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | non_upper_case_globals, 3 | non_camel_case_types, 4 | non_snake_case, 5 | dead_code 6 | )] 7 | 8 | pub const kDNSServiceMaxServiceName: u32 = 64; 9 | pub const kDNSServiceMaxDomainName: u32 = 1009; 10 | pub const kDNSServiceInterfaceIndexAny: u32 = 0; 11 | pub const kDNSServiceProperty_DaemonVersion: &[u8; 14usize] = b"DaemonVersion\0"; 12 | pub type dnssd_sock_t = ::std::os::raw::c_int; 13 | pub type dispatch_queue_t = *mut dispatch_queue_s; 14 | #[repr(C)] 15 | #[derive(Debug, Copy, Clone)] 16 | pub struct dispatch_queue_s { 17 | pub _address: u8, 18 | } 19 | #[repr(C)] 20 | #[derive(Debug, Copy, Clone)] 21 | pub struct _DNSServiceRef_t { 22 | _unused: [u8; 0], 23 | } 24 | pub type DNSServiceRef = *mut _DNSServiceRef_t; 25 | #[repr(C)] 26 | #[derive(Debug, Copy, Clone)] 27 | pub struct _DNSRecordRef_t { 28 | _unused: [u8; 0], 29 | } 30 | pub type DNSRecordRef = *mut _DNSRecordRef_t; 31 | #[repr(C)] 32 | #[derive(Debug, Copy, Clone)] 33 | pub struct sockaddr { 34 | _unused: [u8; 0], 35 | } 36 | pub const kDNSServiceFlagsMoreComing: _bindgen_ty_3 = 1; 37 | pub const kDNSServiceFlagsAutoTrigger: _bindgen_ty_3 = 1; 38 | pub const kDNSServiceFlagsAdd: _bindgen_ty_3 = 2; 39 | pub const kDNSServiceFlagsDefault: _bindgen_ty_3 = 4; 40 | pub const kDNSServiceFlagsNoAutoRename: _bindgen_ty_3 = 8; 41 | pub const kDNSServiceFlagsShared: _bindgen_ty_3 = 16; 42 | pub const kDNSServiceFlagsUnique: _bindgen_ty_3 = 32; 43 | pub const kDNSServiceFlagsBrowseDomains: _bindgen_ty_3 = 64; 44 | pub const kDNSServiceFlagsRegistrationDomains: _bindgen_ty_3 = 128; 45 | pub const kDNSServiceFlagsLongLivedQuery: _bindgen_ty_3 = 256; 46 | pub const kDNSServiceFlagsAllowRemoteQuery: _bindgen_ty_3 = 512; 47 | pub const kDNSServiceFlagsForceMulticast: _bindgen_ty_3 = 1024; 48 | pub const kDNSServiceFlagsForce: _bindgen_ty_3 = 2048; 49 | pub const kDNSServiceFlagsKnownUnique: _bindgen_ty_3 = 2048; 50 | pub const kDNSServiceFlagsReturnIntermediates: _bindgen_ty_3 = 4096; 51 | pub const kDNSServiceFlagsShareConnection: _bindgen_ty_3 = 16384; 52 | pub const kDNSServiceFlagsSuppressUnusable: _bindgen_ty_3 = 32768; 53 | pub const kDNSServiceFlagsTimeout: _bindgen_ty_3 = 65536; 54 | pub const kDNSServiceFlagsIncludeP2P: _bindgen_ty_3 = 131072; 55 | pub const kDNSServiceFlagsWakeOnResolve: _bindgen_ty_3 = 262144; 56 | pub const kDNSServiceFlagsBackgroundTrafficClass: _bindgen_ty_3 = 524288; 57 | pub const kDNSServiceFlagsIncludeAWDL: _bindgen_ty_3 = 1048576; 58 | pub const kDNSServiceFlagsValidate: _bindgen_ty_3 = 2097152; 59 | pub const kDNSServiceFlagsSecure: _bindgen_ty_3 = 2097168; 60 | pub const kDNSServiceFlagsInsecure: _bindgen_ty_3 = 2097184; 61 | pub const kDNSServiceFlagsBogus: _bindgen_ty_3 = 2097216; 62 | pub const kDNSServiceFlagsIndeterminate: _bindgen_ty_3 = 2097280; 63 | pub const kDNSServiceFlagsUnicastResponse: _bindgen_ty_3 = 4194304; 64 | pub const kDNSServiceFlagsValidateOptional: _bindgen_ty_3 = 8388608; 65 | pub const kDNSServiceFlagsWakeOnlyService: _bindgen_ty_3 = 16777216; 66 | pub const kDNSServiceFlagsThresholdOne: _bindgen_ty_3 = 33554432; 67 | pub const kDNSServiceFlagsThresholdFinder: _bindgen_ty_3 = 67108864; 68 | pub const kDNSServiceFlagsThresholdReached: _bindgen_ty_3 = 33554432; 69 | pub const kDNSServiceFlagsPrivateOne: _bindgen_ty_3 = 8192; 70 | pub const kDNSServiceFlagsPrivateTwo: _bindgen_ty_3 = 134217728; 71 | pub const kDNSServiceFlagsPrivateThree: _bindgen_ty_3 = 268435456; 72 | pub const kDNSServiceFlagsPrivateFour: _bindgen_ty_3 = 536870912; 73 | pub const kDNSServiceFlagsPrivateFive: _bindgen_ty_3 = 1073741824; 74 | pub const kDNSServiceFlagAnsweredFromCache: _bindgen_ty_3 = 1073741824; 75 | pub const kDNSServiceFlagsAllowExpiredAnswers: _bindgen_ty_3 = 2147483648; 76 | pub const kDNSServiceFlagsExpiredAnswer: _bindgen_ty_3 = 2147483648; 77 | pub type _bindgen_ty_3 = u32; 78 | pub const kDNSServiceProtocol_IPv4: _bindgen_ty_4 = 1; 79 | pub const kDNSServiceProtocol_IPv6: _bindgen_ty_4 = 2; 80 | pub const kDNSServiceProtocol_UDP: _bindgen_ty_4 = 16; 81 | pub const kDNSServiceProtocol_TCP: _bindgen_ty_4 = 32; 82 | pub type _bindgen_ty_4 = u32; 83 | pub const kDNSServiceClass_IN: _bindgen_ty_5 = 1; 84 | pub type _bindgen_ty_5 = u32; 85 | pub const kDNSServiceType_A: _bindgen_ty_6 = 1; 86 | pub const kDNSServiceType_NS: _bindgen_ty_6 = 2; 87 | pub const kDNSServiceType_MD: _bindgen_ty_6 = 3; 88 | pub const kDNSServiceType_MF: _bindgen_ty_6 = 4; 89 | pub const kDNSServiceType_CNAME: _bindgen_ty_6 = 5; 90 | pub const kDNSServiceType_SOA: _bindgen_ty_6 = 6; 91 | pub const kDNSServiceType_MB: _bindgen_ty_6 = 7; 92 | pub const kDNSServiceType_MG: _bindgen_ty_6 = 8; 93 | pub const kDNSServiceType_MR: _bindgen_ty_6 = 9; 94 | pub const kDNSServiceType_NULL: _bindgen_ty_6 = 10; 95 | pub const kDNSServiceType_WKS: _bindgen_ty_6 = 11; 96 | pub const kDNSServiceType_PTR: _bindgen_ty_6 = 12; 97 | pub const kDNSServiceType_HINFO: _bindgen_ty_6 = 13; 98 | pub const kDNSServiceType_MINFO: _bindgen_ty_6 = 14; 99 | pub const kDNSServiceType_MX: _bindgen_ty_6 = 15; 100 | pub const kDNSServiceType_TXT: _bindgen_ty_6 = 16; 101 | pub const kDNSServiceType_RP: _bindgen_ty_6 = 17; 102 | pub const kDNSServiceType_AFSDB: _bindgen_ty_6 = 18; 103 | pub const kDNSServiceType_X25: _bindgen_ty_6 = 19; 104 | pub const kDNSServiceType_ISDN: _bindgen_ty_6 = 20; 105 | pub const kDNSServiceType_RT: _bindgen_ty_6 = 21; 106 | pub const kDNSServiceType_NSAP: _bindgen_ty_6 = 22; 107 | pub const kDNSServiceType_NSAP_PTR: _bindgen_ty_6 = 23; 108 | pub const kDNSServiceType_SIG: _bindgen_ty_6 = 24; 109 | pub const kDNSServiceType_KEY: _bindgen_ty_6 = 25; 110 | pub const kDNSServiceType_PX: _bindgen_ty_6 = 26; 111 | pub const kDNSServiceType_GPOS: _bindgen_ty_6 = 27; 112 | pub const kDNSServiceType_AAAA: _bindgen_ty_6 = 28; 113 | pub const kDNSServiceType_LOC: _bindgen_ty_6 = 29; 114 | pub const kDNSServiceType_NXT: _bindgen_ty_6 = 30; 115 | pub const kDNSServiceType_EID: _bindgen_ty_6 = 31; 116 | pub const kDNSServiceType_NIMLOC: _bindgen_ty_6 = 32; 117 | pub const kDNSServiceType_SRV: _bindgen_ty_6 = 33; 118 | pub const kDNSServiceType_ATMA: _bindgen_ty_6 = 34; 119 | pub const kDNSServiceType_NAPTR: _bindgen_ty_6 = 35; 120 | pub const kDNSServiceType_KX: _bindgen_ty_6 = 36; 121 | pub const kDNSServiceType_CERT: _bindgen_ty_6 = 37; 122 | pub const kDNSServiceType_A6: _bindgen_ty_6 = 38; 123 | pub const kDNSServiceType_DNAME: _bindgen_ty_6 = 39; 124 | pub const kDNSServiceType_SINK: _bindgen_ty_6 = 40; 125 | pub const kDNSServiceType_OPT: _bindgen_ty_6 = 41; 126 | pub const kDNSServiceType_APL: _bindgen_ty_6 = 42; 127 | pub const kDNSServiceType_DS: _bindgen_ty_6 = 43; 128 | pub const kDNSServiceType_SSHFP: _bindgen_ty_6 = 44; 129 | pub const kDNSServiceType_IPSECKEY: _bindgen_ty_6 = 45; 130 | pub const kDNSServiceType_RRSIG: _bindgen_ty_6 = 46; 131 | pub const kDNSServiceType_NSEC: _bindgen_ty_6 = 47; 132 | pub const kDNSServiceType_DNSKEY: _bindgen_ty_6 = 48; 133 | pub const kDNSServiceType_DHCID: _bindgen_ty_6 = 49; 134 | pub const kDNSServiceType_NSEC3: _bindgen_ty_6 = 50; 135 | pub const kDNSServiceType_NSEC3PARAM: _bindgen_ty_6 = 51; 136 | pub const kDNSServiceType_HIP: _bindgen_ty_6 = 55; 137 | pub const kDNSServiceType_SPF: _bindgen_ty_6 = 99; 138 | pub const kDNSServiceType_UINFO: _bindgen_ty_6 = 100; 139 | pub const kDNSServiceType_UID: _bindgen_ty_6 = 101; 140 | pub const kDNSServiceType_GID: _bindgen_ty_6 = 102; 141 | pub const kDNSServiceType_UNSPEC: _bindgen_ty_6 = 103; 142 | pub const kDNSServiceType_TKEY: _bindgen_ty_6 = 249; 143 | pub const kDNSServiceType_TSIG: _bindgen_ty_6 = 250; 144 | pub const kDNSServiceType_IXFR: _bindgen_ty_6 = 251; 145 | pub const kDNSServiceType_AXFR: _bindgen_ty_6 = 252; 146 | pub const kDNSServiceType_MAILB: _bindgen_ty_6 = 253; 147 | pub const kDNSServiceType_MAILA: _bindgen_ty_6 = 254; 148 | pub const kDNSServiceType_ANY: _bindgen_ty_6 = 255; 149 | pub type _bindgen_ty_6 = u32; 150 | pub const kDNSServiceErr_NoError: _bindgen_ty_7 = 0; 151 | pub const kDNSServiceErr_Unknown: _bindgen_ty_7 = -65537; 152 | pub const kDNSServiceErr_NoSuchName: _bindgen_ty_7 = -65538; 153 | pub const kDNSServiceErr_NoMemory: _bindgen_ty_7 = -65539; 154 | pub const kDNSServiceErr_BadParam: _bindgen_ty_7 = -65540; 155 | pub const kDNSServiceErr_BadReference: _bindgen_ty_7 = -65541; 156 | pub const kDNSServiceErr_BadState: _bindgen_ty_7 = -65542; 157 | pub const kDNSServiceErr_BadFlags: _bindgen_ty_7 = -65543; 158 | pub const kDNSServiceErr_Unsupported: _bindgen_ty_7 = -65544; 159 | pub const kDNSServiceErr_NotInitialized: _bindgen_ty_7 = -65545; 160 | pub const kDNSServiceErr_AlreadyRegistered: _bindgen_ty_7 = -65547; 161 | pub const kDNSServiceErr_NameConflict: _bindgen_ty_7 = -65548; 162 | pub const kDNSServiceErr_Invalid: _bindgen_ty_7 = -65549; 163 | pub const kDNSServiceErr_Firewall: _bindgen_ty_7 = -65550; 164 | pub const kDNSServiceErr_Incompatible: _bindgen_ty_7 = -65551; 165 | pub const kDNSServiceErr_BadInterfaceIndex: _bindgen_ty_7 = -65552; 166 | pub const kDNSServiceErr_Refused: _bindgen_ty_7 = -65553; 167 | pub const kDNSServiceErr_NoSuchRecord: _bindgen_ty_7 = -65554; 168 | pub const kDNSServiceErr_NoAuth: _bindgen_ty_7 = -65555; 169 | pub const kDNSServiceErr_NoSuchKey: _bindgen_ty_7 = -65556; 170 | pub const kDNSServiceErr_NATTraversal: _bindgen_ty_7 = -65557; 171 | pub const kDNSServiceErr_DoubleNAT: _bindgen_ty_7 = -65558; 172 | pub const kDNSServiceErr_BadTime: _bindgen_ty_7 = -65559; 173 | pub const kDNSServiceErr_BadSig: _bindgen_ty_7 = -65560; 174 | pub const kDNSServiceErr_BadKey: _bindgen_ty_7 = -65561; 175 | pub const kDNSServiceErr_Transient: _bindgen_ty_7 = -65562; 176 | pub const kDNSServiceErr_ServiceNotRunning: _bindgen_ty_7 = -65563; 177 | pub const kDNSServiceErr_NATPortMappingUnsupported: _bindgen_ty_7 = -65564; 178 | pub const kDNSServiceErr_NATPortMappingDisabled: _bindgen_ty_7 = -65565; 179 | pub const kDNSServiceErr_NoRouter: _bindgen_ty_7 = -65566; 180 | pub const kDNSServiceErr_PollingMode: _bindgen_ty_7 = -65567; 181 | pub const kDNSServiceErr_Timeout: _bindgen_ty_7 = -65568; 182 | pub const kDNSServiceErr_DefunctConnection: _bindgen_ty_7 = -65569; 183 | pub type _bindgen_ty_7 = i32; 184 | pub type DNSServiceFlags = u32; 185 | pub type DNSServiceProtocol = u32; 186 | pub type DNSServiceErrorType = i32; 187 | extern "C" { 188 | pub fn DNSServiceGetProperty( 189 | property: *const ::std::os::raw::c_char, 190 | result: *mut ::std::os::raw::c_void, 191 | size: *mut u32, 192 | ) -> DNSServiceErrorType; 193 | } 194 | extern "C" { 195 | pub fn DNSServiceRefSockFD(sdRef: DNSServiceRef) -> dnssd_sock_t; 196 | } 197 | extern "C" { 198 | pub fn DNSServiceProcessResult(sdRef: DNSServiceRef) -> DNSServiceErrorType; 199 | } 200 | extern "C" { 201 | pub fn DNSServiceRefDeallocate(sdRef: DNSServiceRef); 202 | } 203 | pub type DNSServiceDomainEnumReply = ::std::option::Option< 204 | unsafe extern "C" fn( 205 | sdRef: DNSServiceRef, 206 | flags: DNSServiceFlags, 207 | interfaceIndex: u32, 208 | errorCode: DNSServiceErrorType, 209 | replyDomain: *const ::std::os::raw::c_char, 210 | context: *mut ::std::os::raw::c_void, 211 | ), 212 | >; 213 | extern "C" { 214 | pub fn DNSServiceEnumerateDomains( 215 | sdRef: *mut DNSServiceRef, 216 | flags: DNSServiceFlags, 217 | interfaceIndex: u32, 218 | callBack: DNSServiceDomainEnumReply, 219 | context: *mut ::std::os::raw::c_void, 220 | ) -> DNSServiceErrorType; 221 | } 222 | pub type DNSServiceRegisterReply = ::std::option::Option< 223 | unsafe extern "C" fn( 224 | sdRef: DNSServiceRef, 225 | flags: DNSServiceFlags, 226 | errorCode: DNSServiceErrorType, 227 | name: *const ::std::os::raw::c_char, 228 | regtype: *const ::std::os::raw::c_char, 229 | domain: *const ::std::os::raw::c_char, 230 | context: *mut ::std::os::raw::c_void, 231 | ), 232 | >; 233 | extern "C" { 234 | pub fn DNSServiceRegister( 235 | sdRef: *mut DNSServiceRef, 236 | flags: DNSServiceFlags, 237 | interfaceIndex: u32, 238 | name: *const ::std::os::raw::c_char, 239 | regtype: *const ::std::os::raw::c_char, 240 | domain: *const ::std::os::raw::c_char, 241 | host: *const ::std::os::raw::c_char, 242 | port: u16, 243 | txtLen: u16, 244 | txtRecord: *const ::std::os::raw::c_void, 245 | callBack: DNSServiceRegisterReply, 246 | context: *mut ::std::os::raw::c_void, 247 | ) -> DNSServiceErrorType; 248 | } 249 | extern "C" { 250 | pub fn DNSServiceAddRecord( 251 | sdRef: DNSServiceRef, 252 | RecordRef: *mut DNSRecordRef, 253 | flags: DNSServiceFlags, 254 | rrtype: u16, 255 | rdlen: u16, 256 | rdata: *const ::std::os::raw::c_void, 257 | ttl: u32, 258 | ) -> DNSServiceErrorType; 259 | } 260 | extern "C" { 261 | pub fn DNSServiceUpdateRecord( 262 | sdRef: DNSServiceRef, 263 | RecordRef: DNSRecordRef, 264 | flags: DNSServiceFlags, 265 | rdlen: u16, 266 | rdata: *const ::std::os::raw::c_void, 267 | ttl: u32, 268 | ) -> DNSServiceErrorType; 269 | } 270 | extern "C" { 271 | pub fn DNSServiceRemoveRecord( 272 | sdRef: DNSServiceRef, 273 | RecordRef: DNSRecordRef, 274 | flags: DNSServiceFlags, 275 | ) -> DNSServiceErrorType; 276 | } 277 | pub type DNSServiceBrowseReply = ::std::option::Option< 278 | unsafe extern "C" fn( 279 | sdRef: DNSServiceRef, 280 | flags: DNSServiceFlags, 281 | interfaceIndex: u32, 282 | errorCode: DNSServiceErrorType, 283 | serviceName: *const ::std::os::raw::c_char, 284 | regtype: *const ::std::os::raw::c_char, 285 | replyDomain: *const ::std::os::raw::c_char, 286 | context: *mut ::std::os::raw::c_void, 287 | ), 288 | >; 289 | extern "C" { 290 | pub fn DNSServiceBrowse( 291 | sdRef: *mut DNSServiceRef, 292 | flags: DNSServiceFlags, 293 | interfaceIndex: u32, 294 | regtype: *const ::std::os::raw::c_char, 295 | domain: *const ::std::os::raw::c_char, 296 | callBack: DNSServiceBrowseReply, 297 | context: *mut ::std::os::raw::c_void, 298 | ) -> DNSServiceErrorType; 299 | } 300 | pub type DNSServiceResolveReply = ::std::option::Option< 301 | unsafe extern "C" fn( 302 | sdRef: DNSServiceRef, 303 | flags: DNSServiceFlags, 304 | interfaceIndex: u32, 305 | errorCode: DNSServiceErrorType, 306 | fullname: *const ::std::os::raw::c_char, 307 | hosttarget: *const ::std::os::raw::c_char, 308 | port: u16, 309 | txtLen: u16, 310 | txtRecord: *const ::std::os::raw::c_uchar, 311 | context: *mut ::std::os::raw::c_void, 312 | ), 313 | >; 314 | extern "C" { 315 | pub fn DNSServiceResolve( 316 | sdRef: *mut DNSServiceRef, 317 | flags: DNSServiceFlags, 318 | interfaceIndex: u32, 319 | name: *const ::std::os::raw::c_char, 320 | regtype: *const ::std::os::raw::c_char, 321 | domain: *const ::std::os::raw::c_char, 322 | callBack: DNSServiceResolveReply, 323 | context: *mut ::std::os::raw::c_void, 324 | ) -> DNSServiceErrorType; 325 | } 326 | pub type DNSServiceQueryRecordReply = ::std::option::Option< 327 | unsafe extern "C" fn( 328 | sdRef: DNSServiceRef, 329 | flags: DNSServiceFlags, 330 | interfaceIndex: u32, 331 | errorCode: DNSServiceErrorType, 332 | fullname: *const ::std::os::raw::c_char, 333 | rrtype: u16, 334 | rrclass: u16, 335 | rdlen: u16, 336 | rdata: *const ::std::os::raw::c_void, 337 | ttl: u32, 338 | context: *mut ::std::os::raw::c_void, 339 | ), 340 | >; 341 | extern "C" { 342 | pub fn DNSServiceQueryRecord( 343 | sdRef: *mut DNSServiceRef, 344 | flags: DNSServiceFlags, 345 | interfaceIndex: u32, 346 | fullname: *const ::std::os::raw::c_char, 347 | rrtype: u16, 348 | rrclass: u16, 349 | callBack: DNSServiceQueryRecordReply, 350 | context: *mut ::std::os::raw::c_void, 351 | ) -> DNSServiceErrorType; 352 | } 353 | pub type DNSServiceGetAddrInfoReply = ::std::option::Option< 354 | unsafe extern "C" fn( 355 | sdRef: DNSServiceRef, 356 | flags: DNSServiceFlags, 357 | interfaceIndex: u32, 358 | errorCode: DNSServiceErrorType, 359 | hostname: *const ::std::os::raw::c_char, 360 | address: *const sockaddr, 361 | ttl: u32, 362 | context: *mut ::std::os::raw::c_void, 363 | ), 364 | >; 365 | extern "C" { 366 | pub fn DNSServiceGetAddrInfo( 367 | sdRef: *mut DNSServiceRef, 368 | flags: DNSServiceFlags, 369 | interfaceIndex: u32, 370 | protocol: DNSServiceProtocol, 371 | hostname: *const ::std::os::raw::c_char, 372 | callBack: DNSServiceGetAddrInfoReply, 373 | context: *mut ::std::os::raw::c_void, 374 | ) -> DNSServiceErrorType; 375 | } 376 | extern "C" { 377 | pub fn DNSServiceCreateConnection(sdRef: *mut DNSServiceRef) -> DNSServiceErrorType; 378 | } 379 | pub type DNSServiceRegisterRecordReply = ::std::option::Option< 380 | unsafe extern "C" fn( 381 | sdRef: DNSServiceRef, 382 | RecordRef: DNSRecordRef, 383 | flags: DNSServiceFlags, 384 | errorCode: DNSServiceErrorType, 385 | context: *mut ::std::os::raw::c_void, 386 | ), 387 | >; 388 | extern "C" { 389 | pub fn DNSServiceRegisterRecord( 390 | sdRef: DNSServiceRef, 391 | RecordRef: *mut DNSRecordRef, 392 | flags: DNSServiceFlags, 393 | interfaceIndex: u32, 394 | fullname: *const ::std::os::raw::c_char, 395 | rrtype: u16, 396 | rrclass: u16, 397 | rdlen: u16, 398 | rdata: *const ::std::os::raw::c_void, 399 | ttl: u32, 400 | callBack: DNSServiceRegisterRecordReply, 401 | context: *mut ::std::os::raw::c_void, 402 | ) -> DNSServiceErrorType; 403 | } 404 | extern "C" { 405 | pub fn DNSServiceReconfirmRecord( 406 | flags: DNSServiceFlags, 407 | interfaceIndex: u32, 408 | fullname: *const ::std::os::raw::c_char, 409 | rrtype: u16, 410 | rrclass: u16, 411 | rdlen: u16, 412 | rdata: *const ::std::os::raw::c_void, 413 | ) -> DNSServiceErrorType; 414 | } 415 | pub type DNSServiceNATPortMappingReply = ::std::option::Option< 416 | unsafe extern "C" fn( 417 | sdRef: DNSServiceRef, 418 | flags: DNSServiceFlags, 419 | interfaceIndex: u32, 420 | errorCode: DNSServiceErrorType, 421 | externalAddress: u32, 422 | protocol: DNSServiceProtocol, 423 | internalPort: u16, 424 | externalPort: u16, 425 | ttl: u32, 426 | context: *mut ::std::os::raw::c_void, 427 | ), 428 | >; 429 | extern "C" { 430 | pub fn DNSServiceNATPortMappingCreate( 431 | sdRef: *mut DNSServiceRef, 432 | flags: DNSServiceFlags, 433 | interfaceIndex: u32, 434 | protocol: DNSServiceProtocol, 435 | internalPort: u16, 436 | externalPort: u16, 437 | ttl: u32, 438 | callBack: DNSServiceNATPortMappingReply, 439 | context: *mut ::std::os::raw::c_void, 440 | ) -> DNSServiceErrorType; 441 | } 442 | extern "C" { 443 | pub fn DNSServiceConstructFullName( 444 | fullName: *mut ::std::os::raw::c_char, 445 | service: *const ::std::os::raw::c_char, 446 | regtype: *const ::std::os::raw::c_char, 447 | domain: *const ::std::os::raw::c_char, 448 | ) -> DNSServiceErrorType; 449 | } 450 | #[repr(C)] 451 | #[derive(Copy, Clone)] 452 | pub union _TXTRecordRef_t { 453 | pub PrivateData: [::std::os::raw::c_char; 16usize], 454 | pub ForceNaturalAlignment: *mut ::std::os::raw::c_char, 455 | _bindgen_union_align: [u64; 2usize], 456 | } 457 | #[test] 458 | fn bindgen_test_layout__TXTRecordRef_t() { 459 | assert_eq!( 460 | ::std::mem::size_of::<_TXTRecordRef_t>(), 461 | 16usize, 462 | concat!("Size of: ", stringify!(_TXTRecordRef_t)) 463 | ); 464 | assert_eq!( 465 | ::std::mem::align_of::<_TXTRecordRef_t>(), 466 | 8usize, 467 | concat!("Alignment of ", stringify!(_TXTRecordRef_t)) 468 | ); 469 | assert_eq!( 470 | unsafe { &(*(::std::ptr::null::<_TXTRecordRef_t>())).PrivateData as *const _ as usize }, 471 | 0usize, 472 | concat!( 473 | "Offset of field: ", 474 | stringify!(_TXTRecordRef_t), 475 | "::", 476 | stringify!(PrivateData) 477 | ) 478 | ); 479 | assert_eq!( 480 | unsafe { 481 | &(*(::std::ptr::null::<_TXTRecordRef_t>())).ForceNaturalAlignment as *const _ as usize 482 | }, 483 | 0usize, 484 | concat!( 485 | "Offset of field: ", 486 | stringify!(_TXTRecordRef_t), 487 | "::", 488 | stringify!(ForceNaturalAlignment) 489 | ) 490 | ); 491 | } 492 | pub type TXTRecordRef = _TXTRecordRef_t; 493 | extern "C" { 494 | pub fn TXTRecordCreate( 495 | txtRecord: *mut TXTRecordRef, 496 | bufferLen: u16, 497 | buffer: *mut ::std::os::raw::c_void, 498 | ); 499 | } 500 | extern "C" { 501 | pub fn TXTRecordDeallocate(txtRecord: *mut TXTRecordRef); 502 | } 503 | extern "C" { 504 | pub fn TXTRecordSetValue( 505 | txtRecord: *mut TXTRecordRef, 506 | key: *const ::std::os::raw::c_char, 507 | valueSize: u8, 508 | value: *const ::std::os::raw::c_void, 509 | ) -> DNSServiceErrorType; 510 | } 511 | extern "C" { 512 | pub fn TXTRecordRemoveValue( 513 | txtRecord: *mut TXTRecordRef, 514 | key: *const ::std::os::raw::c_char, 515 | ) -> DNSServiceErrorType; 516 | } 517 | extern "C" { 518 | pub fn TXTRecordGetLength(txtRecord: *const TXTRecordRef) -> u16; 519 | } 520 | extern "C" { 521 | pub fn TXTRecordGetBytesPtr(txtRecord: *const TXTRecordRef) -> *const ::std::os::raw::c_void; 522 | } 523 | extern "C" { 524 | pub fn TXTRecordContainsKey( 525 | txtLen: u16, 526 | txtRecord: *const ::std::os::raw::c_void, 527 | key: *const ::std::os::raw::c_char, 528 | ) -> ::std::os::raw::c_int; 529 | } 530 | extern "C" { 531 | pub fn TXTRecordGetValuePtr( 532 | txtLen: u16, 533 | txtRecord: *const ::std::os::raw::c_void, 534 | key: *const ::std::os::raw::c_char, 535 | valueLen: *mut u8, 536 | ) -> *const ::std::os::raw::c_void; 537 | } 538 | extern "C" { 539 | pub fn TXTRecordGetCount(txtLen: u16, txtRecord: *const ::std::os::raw::c_void) -> u16; 540 | } 541 | extern "C" { 542 | pub fn TXTRecordGetItemAtIndex( 543 | txtLen: u16, 544 | txtRecord: *const ::std::os::raw::c_void, 545 | itemIndex: u16, 546 | keyBufLen: u16, 547 | key: *mut ::std::os::raw::c_char, 548 | valueLen: *mut u8, 549 | value: *mut *const ::std::os::raw::c_void, 550 | ) -> DNSServiceErrorType; 551 | } 552 | extern "C" { 553 | pub fn DNSServiceSetDispatchQueue( 554 | service: DNSServiceRef, 555 | queue: dispatch_queue_t, 556 | ) -> DNSServiceErrorType; 557 | } 558 | pub type DNSServiceSleepKeepaliveReply = ::std::option::Option< 559 | unsafe extern "C" fn( 560 | sdRef: DNSServiceRef, 561 | errorCode: DNSServiceErrorType, 562 | context: *mut ::std::os::raw::c_void, 563 | ), 564 | >; 565 | extern "C" { 566 | pub fn DNSServiceSleepKeepalive( 567 | sdRef: *mut DNSServiceRef, 568 | flags: DNSServiceFlags, 569 | fd: ::std::os::raw::c_int, 570 | timeout: ::std::os::raw::c_uint, 571 | callBack: DNSServiceSleepKeepaliveReply, 572 | context: *mut ::std::os::raw::c_void, 573 | ) -> DNSServiceErrorType; 574 | } 575 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Astro DNS-SD - Rust wrapper crate for DNS-SD libraries 2 | 3 | #![forbid(missing_docs)] 4 | 5 | // pub mod browser; 6 | mod browse; 7 | mod ffi; 8 | mod non_blocking; 9 | mod os; 10 | mod register; 11 | 12 | pub use crate::browse::{ 13 | BrowseError, Service, ServiceBrowser, ServiceBrowserBuilder, ServiceEventType, 14 | }; 15 | pub use crate::os::{RegisteredDnsService, RegistrationError}; 16 | pub use crate::register::DNSServiceBuilder; 17 | 18 | #[macro_use] 19 | extern crate log; 20 | 21 | // /// Result type for dns-sd fallible returns 22 | // pub type Result = std::result::Result; 23 | -------------------------------------------------------------------------------- /src/non_blocking.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(target_os = "windows", feature = "win-bonjour"))] 2 | mod os { 3 | use super::*; 4 | use winapi::um::winsock2::{WSAPoll, POLLIN, SOCKET, SOCKET_ERROR, WSAPOLLFD}; 5 | 6 | pub fn socket_is_ready( 7 | socket: SOCKET, 8 | timeout: std::time::Duration, 9 | ) -> Result { 10 | let info = WSAPOLLFD { 11 | fd: socket, 12 | events: POLLIN, 13 | revents: 0, 14 | }; 15 | let mut sockets = [info]; 16 | let r = unsafe { 17 | WSAPoll( 18 | sockets.as_mut_ptr(), 19 | sockets.len() as u32, 20 | timeout.as_millis() as i32, 21 | ) 22 | }; 23 | if r != SOCKET_ERROR && r > 0 { 24 | // let ready_to_read = info.revents & POLLIN; 25 | trace!( 26 | "Some ready, checking flags: {:b} vs {:b}", 27 | info.revents, 28 | POLLIN 29 | ); 30 | // Ok(ready_to_read != 0) 31 | // TODO: figure out why no flags are set, or maybe switch to IOCP 32 | Ok(true) 33 | } else if r == SOCKET_ERROR { 34 | Err(std::io::Error::from_raw_os_error(r)) 35 | } else { 36 | trace!("Nothing ready"); 37 | Ok(false) 38 | } 39 | } 40 | } 41 | #[cfg(not(target_os = "windows"))] 42 | mod os { 43 | pub fn socket_is_ready( 44 | socket: i32, 45 | timeout: std::time::Duration, 46 | ) -> Result { 47 | unsafe { 48 | let fd = socket; 49 | let mut timeout = libc::timeval { 50 | tv_sec: timeout.as_secs() as _, 51 | tv_usec: timeout.as_micros() as _, 52 | }; 53 | let mut read_set = std::mem::zeroed(); 54 | libc::FD_ZERO(&mut read_set); 55 | libc::FD_SET(fd, &mut read_set); 56 | libc::select( 57 | fd + 1, 58 | &mut read_set, 59 | std::ptr::null_mut(), 60 | std::ptr::null_mut(), 61 | &mut timeout, 62 | ); 63 | Ok(libc::FD_ISSET(fd, &read_set)) 64 | } 65 | } 66 | } 67 | #[cfg(any(not(target_os = "windows"), feature = "win-bonjour"))] 68 | pub use os::socket_is_ready; 69 | -------------------------------------------------------------------------------- /src/os/apple.rs: -------------------------------------------------------------------------------- 1 | pub mod browse; 2 | pub mod register; 3 | mod txt; 4 | -------------------------------------------------------------------------------- /src/os/apple/browse.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi::apple as ffi; 2 | // use std::collections::HashMap; 3 | use crate::browse::{Service, ServiceEventType}; 4 | use crate::ffi::apple::kDNSServiceErr_NoError; 5 | use crate::ServiceBrowserBuilder; 6 | use std::collections::HashMap; 7 | use std::ffi::{c_void, CStr, CString}; 8 | use std::io::{Error as IoError, ErrorKind}; 9 | use std::net::{SocketAddr, ToSocketAddrs}; 10 | use std::os::raw::c_char; 11 | use std::ptr; 12 | use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError, SyncSender}; 13 | use std::time::Duration; 14 | use thiserror::Error; 15 | 16 | impl From for ServiceEventType { 17 | fn from(flags: ffi::DNSServiceFlags) -> Self { 18 | if flags & ffi::kDNSServiceFlagsAdd as u32 != 0 { 19 | ServiceEventType::Added 20 | } else { 21 | ServiceEventType::Removed 22 | } 23 | } 24 | } 25 | 26 | /// Common error for DNS-SD service 27 | #[derive(Debug, Error)] 28 | pub enum BrowseError { 29 | /// Invalid input string 30 | #[error("Invalid string argument, must be C string compatible")] 31 | InvalidString, 32 | /// Unexpected invalid strings from C API 33 | #[error("DNSSD API returned invalid UTF-8 string")] 34 | InternalInvalidString, 35 | /// Error from DNSSD service 36 | #[error("DNSSD Error: {0}")] 37 | ServiceError(i32), 38 | /// IO error 39 | #[error("IO Error: {0}")] 40 | IoError(#[from] IoError), 41 | /// Timeout error when waiting for more data from browser 42 | #[error("Timeout waiting for more data")] 43 | Timeout, 44 | } 45 | /// Apple based DNS-SD result type 46 | pub type Result = std::result::Result; 47 | 48 | unsafe extern "C" fn browse_callback( 49 | _sd_ref: ffi::DNSServiceRef, 50 | flags: ffi::DNSServiceFlags, 51 | interface_index: u32, 52 | error_code: ffi::DNSServiceErrorType, 53 | service_name: *const c_char, 54 | regtype: *const c_char, 55 | reply_domain: *const c_char, 56 | context: *mut c_void, 57 | ) { 58 | if !context.is_null() { 59 | let tx_ptr: *mut SyncSender> = context as _; 60 | let tx = &*tx_ptr; 61 | 62 | // shouldn't need any other args if there's an error 63 | if error_code != 0 { 64 | match tx.try_send(Err(BrowseError::ServiceError(error_code))) { 65 | Ok(_) => {} 66 | Err(e) => { 67 | error!("Error sending service notification on channel: {:?}", e); 68 | } 69 | } 70 | return; 71 | } 72 | 73 | // build Strings from c_char 74 | let process = || -> Result<(String, String, String)> { 75 | let c_str: &CStr = CStr::from_ptr(service_name); 76 | let service_name: &str = c_str 77 | .to_str() 78 | .map_err(|_| BrowseError::InternalInvalidString)?; 79 | let c_str: &CStr = CStr::from_ptr(regtype); 80 | let regtype: &str = c_str 81 | .to_str() 82 | .map_err(|_| BrowseError::InternalInvalidString)?; 83 | let c_str: &CStr = CStr::from_ptr(reply_domain); 84 | let reply_domain: &str = c_str 85 | .to_str() 86 | .map_err(|_| BrowseError::InternalInvalidString)?; 87 | Ok(( 88 | service_name.to_owned(), 89 | regtype.to_owned(), 90 | reply_domain.to_owned(), 91 | )) 92 | }; 93 | match process() { 94 | Ok((name, regtype, domain)) => { 95 | let service = DiscoveredService { 96 | name, 97 | regtype, 98 | interface_index, 99 | domain, 100 | event_type: flags.into(), 101 | }; 102 | trace!("Informing of discovered service: {:?}", service); 103 | match tx.try_send(Ok(service)) { 104 | Ok(_) => {} 105 | Err(e) => { 106 | error!("Error sending service notification on channel: {:?}", e); 107 | } 108 | } 109 | } 110 | Err(e) => match tx.try_send(Err(e)) { 111 | Ok(_) => {} 112 | Err(e) => { 113 | error!("Error sending service notification on channel: {:?}", e); 114 | } 115 | }, 116 | } 117 | } 118 | } 119 | 120 | /// Encapsulates information about a service 121 | #[derive(Debug)] 122 | pub struct DiscoveredService { 123 | /// Name of service, usually a user friendly name 124 | pub name: String, 125 | /// Registration type, i.e. _http._tcp. 126 | pub regtype: String, 127 | /// Interface index (unsure what this is for) 128 | pub interface_index: u32, 129 | /// Domain service is on, typically local. 130 | pub domain: String, 131 | /// Whether this service is being added or not 132 | pub event_type: ServiceEventType, 133 | } 134 | 135 | fn service_from_resolved(discovered: DiscoveredService, resolved: Vec) -> Service { 136 | if resolved.len() > 1 { 137 | warn!("We resolved > 1 services, unsupported. using first"); 138 | } 139 | let (port, hostname, txt_record) = match resolved.into_iter().next() { 140 | Some(resolved) => (resolved.port, resolved.hostname, resolved.txt_record), 141 | None => (0, "".to_string(), None), 142 | }; 143 | Service { 144 | name: discovered.name, 145 | domain: discovered.domain, 146 | regtype: discovered.regtype, 147 | interface_index: Some(discovered.interface_index), 148 | event_type: discovered.event_type, 149 | hostname, 150 | port, 151 | txt_record, 152 | } 153 | } 154 | 155 | fn resolver_thread(rx: Receiver>, tx: SyncSender>) { 156 | std::thread::Builder::new() 157 | .name("astro-dnssd: resolver".into()) 158 | .spawn(move || loop { 159 | match rx.recv_timeout(Duration::from_millis(250)) { 160 | Ok(Ok(service)) => { 161 | trace!("Got new service: {:?}, resolving...", service); 162 | match service.resolve() { 163 | Ok(resolved) => { 164 | trace!("Resolved: {:?}", resolved); 165 | let service = service_from_resolved(service, resolved); 166 | if let Err(_e) = tx.send(Ok(service)) { 167 | error!("Error sending resolved service, disconnected channel, exiting thread"); 168 | break; 169 | } 170 | } 171 | Err(e) => { 172 | error!("Error resolving: {:?}", e); 173 | if let Err(_e) = tx.send(Err(e)) { 174 | error!("Error sending resolved service, disconnected channel, exiting thread"); 175 | break; 176 | } 177 | } 178 | } 179 | } 180 | Ok(Err(e)) => { 181 | if let Err(_e) = tx.send(Err(e)) { 182 | error!("Error sending resolved service, disconnected channel, exiting thread"); 183 | break; 184 | } 185 | } 186 | Err(RecvTimeoutError::Timeout) => {} 187 | Err(RecvTimeoutError::Disconnected) => { 188 | warn!("Resolver channel disconnected, exiting thread as we're likely stopped/dropped"); 189 | break; 190 | } 191 | } 192 | }).expect("Failed to start resolver thread"); 193 | } 194 | 195 | /// Main service browser, calls callback upon discovery of service 196 | pub struct ServiceBrowser { 197 | /// Raw DNS-SD service reference 198 | raw: ffi::DNSServiceRef, 199 | /// Receiver to receive successfully discovered & resolved services from 200 | rx: Receiver>, 201 | /// Raw pointer the browse callback uses for the SyncSender, to use with Box::from_raw() during Drop 202 | raw_tx: *mut SyncSender>, 203 | } 204 | 205 | impl ServiceBrowser { 206 | /// Returns socket to mDNS service, use with select() 207 | pub fn socket(&self) -> i32 { 208 | unsafe { ffi::DNSServiceRefSockFD(self.raw) } 209 | } 210 | 211 | /// Processes a reply from mDNS service, blocking until there is one 212 | fn process_result(&self) -> ffi::DNSServiceErrorType { 213 | // shouldn't get here but to be safe for now 214 | if self.raw.is_null() { 215 | return ffi::kDNSServiceErr_Invalid; 216 | } 217 | unsafe { ffi::DNSServiceProcessResult(self.raw) } 218 | } 219 | 220 | /// returns true if the socket has data and process_result() should be called 221 | fn has_data(&self, timeout: Duration) -> Result { 222 | let socket = unsafe { ffi::DNSServiceRefSockFD(self.raw) } as _; 223 | let r = crate::non_blocking::socket_is_ready(socket, timeout)?; 224 | Ok(r) 225 | } 226 | 227 | /// Starts browser with type & optional domain 228 | fn start(regtype: String, domain: Option) -> Result { 229 | unsafe { 230 | let c_domain: Option; 231 | if let Some(d) = &domain { 232 | c_domain = Some(CString::new(d.as_str()).map_err(|_| BrowseError::InvalidString)?); 233 | } else { 234 | c_domain = None; 235 | } 236 | let service_type = 237 | CString::new(regtype.as_str()).map_err(|_| BrowseError::InvalidString)?; 238 | let (tx, rx) = sync_channel::>(10); 239 | let tx = Box::into_raw(Box::new(tx)); 240 | let mut raw: ffi::DNSServiceRef = ptr::null_mut(); 241 | let r = ffi::DNSServiceBrowse( 242 | &mut raw as _, 243 | 0, 244 | 0, 245 | service_type.as_ptr(), 246 | c_domain.map_or(ptr::null_mut(), |d| d.as_ptr()), 247 | Some(browse_callback), 248 | tx as _, 249 | ); 250 | if r != kDNSServiceErr_NoError { 251 | error!("DNSServiceBrowser error: {}", r); 252 | return Err(BrowseError::ServiceError(r)); 253 | } 254 | let (final_tx, final_rx) = sync_channel::>(10); 255 | resolver_thread(rx, final_tx); 256 | Ok(ServiceBrowser { 257 | raw, 258 | rx: final_rx, 259 | raw_tx: tx, 260 | }) 261 | } 262 | } 263 | /// Returns discovered services if any 264 | pub fn recv_timeout(&self, timeout: Duration) -> Result { 265 | // TODO: do non-blocking check before calling? 266 | if self.has_data(timeout)? { 267 | trace!("Data on socket, processing before checking channel"); 268 | let r = self.process_result(); 269 | if r != kDNSServiceErr_NoError { 270 | return Err(BrowseError::ServiceError(r)); 271 | } 272 | } 273 | 274 | match self.rx.recv_timeout(timeout) { 275 | Ok(service_result) => service_result, 276 | Err(RecvTimeoutError::Timeout) => Err(BrowseError::Timeout), 277 | Err(RecvTimeoutError::Disconnected) => Err(BrowseError::IoError(IoError::from( 278 | ErrorKind::ConnectionReset, 279 | ))), 280 | } 281 | } 282 | } 283 | 284 | impl Drop for ServiceBrowser { 285 | fn drop(&mut self) { 286 | unsafe { 287 | // ensure we cancel browser first by deallocating it... 288 | ffi::DNSServiceRefDeallocate(self.raw); 289 | // then we should be able to safely drop the sender which will signal resolver thread to exit 290 | let _tx = Box::from_raw(self.raw_tx); 291 | } 292 | } 293 | } 294 | 295 | // should be safe to send across threads, just not shared 296 | unsafe impl Send for ServiceBrowser {} 297 | 298 | pub fn browse(builder: ServiceBrowserBuilder) -> Result { 299 | ServiceBrowser::start(builder.regtype, builder.domain) 300 | } 301 | macro_rules! mut_void_ptr { 302 | ($var:expr) => { 303 | $var as *mut _ as *mut c_void 304 | }; 305 | } 306 | impl DiscoveredService { 307 | fn resolve(&self) -> Result> { 308 | let mut sdref: ffi::DNSServiceRef = unsafe { std::mem::zeroed() }; 309 | let regtype = 310 | CString::new(self.regtype.as_str()).map_err(|_| BrowseError::InvalidString)?; 311 | let name = CString::new(self.name.as_str()).map_err(|_| BrowseError::InvalidString)?; 312 | let domain = CString::new(self.domain.as_str()).map_err(|_| BrowseError::InvalidString)?; 313 | let mut pending_resolution: PendingResolution = Default::default(); 314 | unsafe { 315 | let r = ffi::DNSServiceResolve( 316 | &mut sdref, 317 | 0, 318 | self.interface_index, 319 | name.as_ptr(), 320 | regtype.as_ptr(), 321 | domain.as_ptr(), 322 | Some(resolve_callback), 323 | mut_void_ptr!(&mut pending_resolution), 324 | ); 325 | if r != kDNSServiceErr_NoError { 326 | return Err(BrowseError::ServiceError(r)); 327 | } 328 | #[allow(clippy::while_immutable_condition)] 329 | while pending_resolution.more_coming { 330 | ffi::DNSServiceProcessResult(sdref); 331 | } 332 | ffi::DNSServiceRefDeallocate(sdref); 333 | } 334 | 335 | Ok(pending_resolution.results) 336 | } 337 | } 338 | 339 | struct PendingResolution { 340 | more_coming: bool, 341 | results: Vec, 342 | } 343 | impl Default for PendingResolution { 344 | fn default() -> Self { 345 | PendingResolution { 346 | more_coming: true, // default to true, just as a way to say yes for first entry 347 | results: Vec::with_capacity(1), 348 | } 349 | } 350 | } 351 | 352 | /// Resolved service information, name, hostname, port, & TXT record if any 353 | #[derive(Debug)] 354 | pub struct ResolvedService { 355 | /// Full name of service 356 | #[allow(dead_code)] // TODO: check if we should expose this to public API? 357 | pub full_name: String, 358 | /// Hostname of service, usable with gethostbyname() 359 | pub hostname: String, 360 | /// Port service is on 361 | pub port: u16, 362 | /// TXT record service has if any 363 | pub txt_record: Option>, 364 | } 365 | impl ToSocketAddrs for ResolvedService { 366 | type Iter = std::vec::IntoIter; 367 | /// Leverages Rust's ToSocketAddrs to resolve service hostname & port, host needs integrated bonjour support to work 368 | fn to_socket_addrs(&self) -> std::io::Result { 369 | (self.hostname.as_str(), self.port).to_socket_addrs() 370 | } 371 | } 372 | 373 | unsafe extern "C" fn resolve_callback( 374 | _sd_ref: ffi::DNSServiceRef, 375 | flags: ffi::DNSServiceFlags, 376 | _interface_index: u32, 377 | error_code: ffi::DNSServiceErrorType, 378 | full_name: *const c_char, 379 | host_target: *const c_char, 380 | port: u16, // network byte order 381 | txt_len: u16, 382 | txt_record: *const u8, 383 | context: *mut c_void, 384 | ) { 385 | let context: &mut PendingResolution = &mut *(context as *mut PendingResolution); 386 | if error_code != kDNSServiceErr_NoError { 387 | error!("Error resolving service: {}", error_code); 388 | context.more_coming = false; 389 | return; 390 | } 391 | // flag if we have more records coming so we can fetch them before stopping resolution 392 | context.more_coming = flags & ffi::kDNSServiceFlagsMoreComing as u32 != 0; 393 | let process = || -> Result<(String, String)> { 394 | let c_str: &CStr = CStr::from_ptr(full_name); 395 | let full_name: &str = c_str 396 | .to_str() 397 | .map_err(|_| BrowseError::InternalInvalidString)?; 398 | let c_str: &CStr = CStr::from_ptr(host_target); 399 | let hostname: &str = c_str 400 | .to_str() 401 | .map_err(|_| BrowseError::InternalInvalidString)?; 402 | Ok((full_name.to_owned(), hostname.to_owned())) 403 | }; 404 | let txt_record = if txt_len > 0 { 405 | let data = std::slice::from_raw_parts(txt_record, txt_len as usize); 406 | match hash_from_txt(data) { 407 | Ok(hash) if !hash.is_empty() => Some(hash), 408 | Ok(_hash) => None, 409 | Err(e) => { 410 | error!("Failed to get TXT record: {:?}", e); 411 | None 412 | } 413 | } 414 | } else { 415 | None 416 | }; 417 | match process() { 418 | Ok((full_name, hostname)) => { 419 | let service = ResolvedService { 420 | full_name, 421 | hostname, 422 | port: u16::from_be(port), 423 | txt_record, 424 | }; 425 | context.results.push(service); 426 | } 427 | Err(e) => { 428 | error!("Error resolving service: {:?}", e); 429 | } 430 | } 431 | } 432 | 433 | fn hash_from_txt(data: &[u8]) -> Result> { 434 | let slice = data; 435 | let txt_len = slice.len() as u16; 436 | let txt_bytes = slice.as_ptr() as *const c_void; 437 | 438 | unsafe { 439 | // SAFETY: generated txt_bytes from rust slice with guarantees 440 | let total_keys = ffi::TXTRecordGetCount(txt_len, txt_bytes); 441 | let mut hash: HashMap = HashMap::with_capacity(total_keys as _); 442 | for i in 0..total_keys { 443 | let mut key: [c_char; 256] = std::mem::zeroed(); 444 | let mut value = std::mem::zeroed(); 445 | let mut value_len: u8 = 0; 446 | let err = ffi::TXTRecordGetItemAtIndex( 447 | txt_len, 448 | txt_bytes, 449 | i, 450 | key.len() as u16, 451 | key.as_mut_ptr(), 452 | &mut value_len, 453 | &mut value, 454 | ); 455 | if err == kDNSServiceErr_NoError { 456 | let c_str: &CStr = CStr::from_ptr(key.as_ptr()); 457 | let key: &str = c_str.to_str().unwrap(); 458 | // From the API documentation 459 | // For keys with no value, *value is set to NULL and *valueLen is zero. 460 | if value.is_null() { 461 | if !key.is_empty() { 462 | // use html notion of key==value for key only term to be able to represent 463 | // it in the hashmap without having to change the API 464 | hash.insert(key.to_owned(), key.to_owned()); 465 | } else { 466 | trace!("Discarding TXT key with empty key & null value"); 467 | } 468 | } else { 469 | // SAFETY: guarding against null value above, otherwise trusting the C API 470 | let data = std::slice::from_raw_parts(value as *mut u8, value_len as _); 471 | match std::str::from_utf8(data) { 472 | Ok(value) if !key.is_empty() && !value.is_empty() => { 473 | hash.insert(key.to_owned(), value.to_owned()); 474 | } 475 | Ok(_value) => { 476 | trace!("Discarding TXT key with empty key & value"); 477 | } 478 | Err(e) => { 479 | error!("Error processing TXT value as UTF-8: {}", e); 480 | } 481 | } 482 | } 483 | } 484 | if err == ffi::kDNSServiceErr_Invalid { 485 | error!("Error invalid fetching TXT"); 486 | break; 487 | } 488 | } 489 | Ok(hash) 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/os/apple/register.rs: -------------------------------------------------------------------------------- 1 | //! Registration of dns-sd services 2 | 3 | // use super::txt::TXTRecord; 4 | use crate::ffi::apple::{ 5 | kDNSServiceErr_NoError, DNSServiceErrorType, DNSServiceFlags, DNSServiceProcessResult, 6 | DNSServiceRef, DNSServiceRefDeallocate, DNSServiceRefSockFD, DNSServiceRegister, 7 | }; 8 | use crate::os::apple::txt::TXTRecord; 9 | use crate::{register::Result, DNSServiceBuilder}; 10 | use std::ffi::{c_void, CStr, CString}; 11 | use std::fmt; 12 | use std::os::raw::c_char; 13 | use std::ptr; 14 | use std::ptr::null_mut; 15 | use std::sync::mpsc::{sync_channel, SyncSender}; 16 | use std::time::Duration; 17 | use thiserror::Error; 18 | 19 | const CALLBACK_TIMEOUT: Duration = Duration::from_secs(10); 20 | 21 | /// Common error for DNS-SD service 22 | #[derive(Debug, Error, Copy, Clone, PartialEq, Eq)] 23 | pub enum RegistrationError { 24 | /// Invalid input string 25 | #[error("Invalid string argument, must be C string compatible")] 26 | InvalidString, 27 | /// Unexpected invalid strings from C API 28 | #[error("DNSSD API returned invalid UTF-8 string")] 29 | InternalInvalidString, 30 | /// Error from DNSSD service 31 | #[error("DNSSD Error: {0}")] 32 | ServiceError(i32), 33 | } 34 | 35 | unsafe extern "C" fn register_reply( 36 | _sd_ref: DNSServiceRef, 37 | _flags: DNSServiceFlags, 38 | error_code: DNSServiceErrorType, 39 | name: *const c_char, 40 | regtype: *const c_char, 41 | domain: *const c_char, 42 | context: *mut c_void, 43 | ) { 44 | info!("Got reply"); 45 | // let context: &mut RegisteredDnsService = &mut *(context as *mut RegisteredDnsService); 46 | let process = || -> Result<(String, String, String)> { 47 | let c_str: &CStr = CStr::from_ptr(name); 48 | let service_name: &str = c_str 49 | .to_str() 50 | .map_err(|_| RegistrationError::InternalInvalidString)?; 51 | let c_str: &CStr = CStr::from_ptr(regtype); 52 | let regtype: &str = c_str 53 | .to_str() 54 | .map_err(|_| RegistrationError::InternalInvalidString)?; 55 | let c_str: &CStr = CStr::from_ptr(domain); 56 | let reply_domain: &str = c_str 57 | .to_str() 58 | .map_err(|_| RegistrationError::InternalInvalidString)?; 59 | Ok(( 60 | service_name.to_owned(), 61 | regtype.to_owned(), 62 | reply_domain.to_owned(), 63 | )) 64 | }; 65 | if !context.is_null() { 66 | let tx_ptr: *mut SyncSender> = context as _; 67 | let tx = &*tx_ptr; 68 | trace!("Registration replied"); 69 | match process() { 70 | Ok((name, regtype, domain)) => { 71 | if error_code == kDNSServiceErr_NoError { 72 | let reply = DNSServiceRegisterReply { 73 | regtype, 74 | name, 75 | domain, 76 | }; 77 | tx.send(Ok(reply)).unwrap(); 78 | info!("Reply info sent"); 79 | } else { 80 | error!("Error in reply: {}", error_code); 81 | tx.send(Err(RegistrationError::ServiceError(error_code))) 82 | .unwrap(); 83 | } 84 | } 85 | Err(e) => { 86 | error!("Error in reply: {:?}", e); 87 | tx.send(Err(e)).unwrap(); 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// DNS-SD Service for registration use 94 | pub struct RegisteredDnsService { 95 | socket: i32, 96 | } 97 | impl fmt::Debug for RegisteredDnsService { 98 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 99 | write!(f, "RegisteredDnsService {{ socket: {} }}", self.socket) 100 | } 101 | } 102 | 103 | /// Reply information upon successful registration 104 | #[derive(Debug)] 105 | #[allow(dead_code)] // TODO: in future look to see if we should use these values? 106 | pub struct DNSServiceRegisterReply { 107 | /// Service type of successfully registered service 108 | pub regtype: String, 109 | /// Name of service 110 | pub name: String, 111 | /// Domain used for successful registration 112 | pub domain: String, 113 | } 114 | 115 | /// Service ref to encapsulate DNSServiceRef to send to a thread & cleanup on drop 116 | struct ServiceRef { 117 | raw: DNSServiceRef, 118 | context: *mut c_void, 119 | } 120 | impl ServiceRef { 121 | fn new(raw: DNSServiceRef, context: *mut c_void) -> Self { 122 | ServiceRef { raw, context } 123 | } 124 | } 125 | unsafe impl Send for ServiceRef {} 126 | impl Drop for ServiceRef { 127 | fn drop(&mut self) { 128 | unsafe { 129 | trace!("Dropping service"); 130 | if !self.raw.is_null() { 131 | trace!("Deallocating DNSServiceRef"); 132 | DNSServiceRefDeallocate(self.raw); 133 | self.raw = null_mut(); 134 | _ = Box::from_raw(self.context); 135 | } 136 | } 137 | } 138 | } 139 | 140 | impl RegisteredDnsService {} 141 | 142 | // In order to signal the blocked thread, we close its socket to unblock it 143 | impl Drop for RegisteredDnsService { 144 | fn drop(&mut self) { 145 | unsafe { 146 | trace!("Closing socket to signal service cleanup..."); 147 | libc::close(self.socket); 148 | } 149 | } 150 | } 151 | fn run_thread(service: ServiceRef) { 152 | std::thread::spawn(move || loop { 153 | unsafe { 154 | trace!("Processing..."); 155 | let r = DNSServiceProcessResult(service.raw); 156 | if r != kDNSServiceErr_NoError { 157 | error!("Error processing: {}, exiting thread", r); 158 | break; 159 | } 160 | } 161 | }); 162 | } 163 | pub fn register_service(service: DNSServiceBuilder) -> Result { 164 | unsafe { 165 | let c_name: Option; 166 | if let Some(n) = &service.name { 167 | c_name = Some(CString::new(n.as_str()).map_err(|_| RegistrationError::InvalidString)?); 168 | } else { 169 | c_name = None; 170 | } 171 | let c_name = c_name.as_ref(); 172 | let service_type = 173 | CString::new(service.regtype.as_str()).map_err(|_| RegistrationError::InvalidString)?; 174 | let txt = service.txt.map(TXTRecord::from); 175 | let (txt_record, txt_len) = match &txt { 176 | Some(txt) => (txt.raw_bytes_ptr(), txt.raw_bytes_len()), 177 | None => (ptr::null(), 0), 178 | }; 179 | 180 | let (tx, rx) = sync_channel::>(4); 181 | let tx = Box::into_raw(Box::new(tx)); 182 | 183 | let mut raw: DNSServiceRef = null_mut(); 184 | let result = DNSServiceRegister( 185 | &mut raw, 186 | 0, 187 | 0, 188 | c_name.map_or(null_mut(), |c| c.as_ptr()), 189 | service_type.as_ptr(), 190 | ptr::null(), 191 | ptr::null(), 192 | service.port.to_be(), 193 | txt_len, 194 | txt_record, 195 | Some(register_reply), 196 | tx as _, 197 | ); 198 | if result == kDNSServiceErr_NoError { 199 | // process callback 200 | let socket = DNSServiceRefSockFD(raw); 201 | let service = RegisteredDnsService { socket }; 202 | let raw_service = ServiceRef::new(raw, tx as _); 203 | 204 | // spin a thread that keeps the registration working 205 | run_thread(raw_service); 206 | 207 | match rx.recv_timeout(CALLBACK_TIMEOUT) { 208 | Ok(Ok(_reply)) => Ok(service), 209 | Ok(Err(e)) => Err(e), 210 | Err(e) => { 211 | error!("Error waiting for callback: {:?}", e); 212 | Err(RegistrationError::ServiceError(0)) 213 | } 214 | } 215 | } else { 216 | Err(RegistrationError::ServiceError(result)) 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/os/apple/txt.rs: -------------------------------------------------------------------------------- 1 | //! TXT record creation & handling 2 | #![allow(dead_code)] 3 | 4 | use crate::ffi::apple::{ 5 | kDNSServiceErr_NoError, TXTRecordContainsKey, TXTRecordCreate, TXTRecordDeallocate, 6 | TXTRecordGetBytesPtr, TXTRecordGetCount, TXTRecordGetLength, TXTRecordGetValuePtr, 7 | TXTRecordRef, TXTRecordRemoveValue, TXTRecordSetValue, 8 | }; 9 | use crate::os::apple::register::RegistrationError; 10 | use std::collections::HashMap; 11 | use std::ffi::{c_void, CString}; 12 | use std::mem; 13 | use std::ptr; 14 | use std::slice; 15 | 16 | /// Represents a TXT Record for dns-sd, containing 0 or more key=value pairs 17 | pub struct TXTRecord { 18 | raw: TXTRecordRef, 19 | } 20 | impl Default for TXTRecord { 21 | fn default() -> Self { 22 | TXTRecord::new() 23 | } 24 | } 25 | 26 | impl From> for TXTRecord { 27 | fn from(hash: HashMap) -> Self { 28 | let mut txt = TXTRecord::new(); 29 | for (key, value) in hash { 30 | if let Err(e) = txt.insert(&key, Some(&value)) { 31 | error!("Error inserting {}={} into TXTRecord: {:?}", key, value, e); 32 | } 33 | } 34 | txt 35 | } 36 | } 37 | 38 | impl TXTRecord { 39 | /// Creates a new empty TXT Record with an internally managed buffer 40 | pub fn new() -> TXTRecord { 41 | unsafe { 42 | let mut record = TXTRecord { raw: mem::zeroed() }; 43 | TXTRecordCreate(&mut record.raw, 0, ptr::null_mut()); 44 | record 45 | } 46 | } 47 | 48 | /// Sets a key/value pair 49 | /// 50 | /// **Note:** Only the first 256 bytes of the value will be used. 51 | pub fn insert(&mut self, key: &str, value: Option) -> Result<(), RegistrationError> 52 | where 53 | V: AsRef<[u8]>, 54 | { 55 | let value = value.as_ref().map(|x| x.as_ref()); 56 | let key = CString::new(key).or(Err(RegistrationError::InvalidString))?; 57 | let value_size = value.map_or(0, |x| x.len().min(u8::max_value() as usize) as u8); 58 | let result = unsafe { 59 | TXTRecordSetValue( 60 | &mut self.raw, 61 | key.as_ptr(), 62 | value_size, 63 | value.map_or(ptr::null(), |x| x.as_ptr() as *const c_void), 64 | ) 65 | }; 66 | if result == kDNSServiceErr_NoError { 67 | return Ok(()); 68 | } 69 | Err(RegistrationError::ServiceError(result)) 70 | } 71 | 72 | /// Removes a key/value pair 73 | pub fn remove(&mut self, key: &str) { 74 | let key = match CString::new(key) { 75 | Ok(key) => key, 76 | Err(_) => { 77 | // If the key contains an interior NUL then we know we don't contain it, and 78 | // therefore there's nothing to remove 79 | return; 80 | } 81 | }; 82 | // NB: The only error that TXTRecordRemoveValue can return is one signifying that the key 83 | // did not exist. 84 | unsafe { 85 | TXTRecordRemoveValue(&mut self.raw, key.as_ptr()); 86 | } 87 | } 88 | 89 | /// Checks if a key exists 90 | pub fn contains_key(&self, key: &str) -> bool { 91 | let key = match CString::new(key) { 92 | Ok(key) => key, 93 | Err(_) => { 94 | // If the key contains an interior NUL then we know we don't contain it 95 | return false; 96 | } 97 | }; 98 | unsafe { 99 | TXTRecordContainsKey(self.raw_bytes_len(), self.raw_bytes_ptr(), key.as_ptr()) != 0 100 | } 101 | } 102 | 103 | /// Returns a reference to the value corresponding to the key 104 | /// 105 | /// If the TXT Record contains the key with a null value, this returns `None`. Use 106 | /// `contains_key()` to differentiate between a null value and the key not existing in the TXT 107 | /// Record. 108 | pub fn get(&self, key: &str) -> Option<&[u8]> { 109 | let key = match CString::new(key) { 110 | Ok(key) => key, 111 | Err(_) => { 112 | // If the key contains an interior NUL then we know we don't contain it 113 | return None; 114 | } 115 | }; 116 | let mut value_len = 0u8; 117 | unsafe { 118 | let ptr = TXTRecordGetValuePtr( 119 | self.raw_bytes_len(), 120 | self.raw_bytes_ptr(), 121 | key.as_ptr(), 122 | &mut value_len, 123 | ); 124 | if ptr.is_null() { 125 | None 126 | } else { 127 | Some(slice::from_raw_parts(ptr as *const u8, value_len as usize)) 128 | } 129 | } 130 | } 131 | 132 | /// Returns the number of keys stored in the TXT Record 133 | pub fn len(&self) -> u16 { 134 | unsafe { TXTRecordGetCount(self.raw_bytes_len(), self.raw_bytes_ptr()) } 135 | } 136 | 137 | /// Returns if TXTRecord is empty 138 | pub fn is_empty(&self) -> bool { 139 | self.len() == 0 140 | } 141 | 142 | /// Returns the raw bytes of the TXT Record as a slice 143 | pub fn raw_bytes(&self) -> &[u8] { 144 | unsafe { 145 | slice::from_raw_parts( 146 | self.raw_bytes_ptr() as *const u8, 147 | self.raw_bytes_len() as usize, 148 | ) 149 | } 150 | } 151 | 152 | /// Returns the length in bytes of the TXT Record data 153 | pub fn raw_bytes_len(&self) -> u16 { 154 | unsafe { TXTRecordGetLength(&self.raw) } 155 | } 156 | 157 | /// Returns the raw bytes pointer for the TXT Record 158 | pub fn raw_bytes_ptr(&self) -> *const c_void { 159 | unsafe { TXTRecordGetBytesPtr(&self.raw) } 160 | } 161 | } 162 | 163 | impl Drop for TXTRecord { 164 | fn drop(&mut self) { 165 | unsafe { 166 | TXTRecordDeallocate(&mut self.raw); 167 | } 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | 175 | #[test] 176 | fn txt_creation() { 177 | let mut record = TXTRecord::new(); 178 | assert_eq!(record.insert("test", Some("value1")), Ok(())); 179 | assert_eq!(record.len(), 1); 180 | assert_eq!(record.raw_bytes_len(), 12); 181 | assert_eq!(record.raw_bytes(), b"\x0Btest=value1"); 182 | assert!(record.contains_key("test")); 183 | assert_eq!(record.get("test"), Some(&b"value1"[..])); 184 | assert_eq!(record.insert("test2", Some([1u8, 2, 3])), Ok(())); 185 | assert_eq!(record.len(), 2); 186 | assert_eq!(record.raw_bytes_len(), 22); 187 | assert_eq!(record.raw_bytes(), b"\x0Btest=value1\x09test2=\x01\x02\x03"); 188 | assert!(record.contains_key("test2")); 189 | assert_eq!(record.get("test2"), Some(&[1u8, 2, 3][..])); 190 | assert_eq!(record.insert("test", None::<&str>), Ok(())); 191 | assert_eq!(record.len(), 2); 192 | assert_eq!(record.raw_bytes_len(), 15); 193 | assert_eq!(record.raw_bytes(), b"\x09test2=\x01\x02\x03\x04test"); 194 | assert_eq!(record.get("test"), None); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/os/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(not(feature = "win-bonjour"), target_os = "windows"))] 2 | mod windows; 3 | 4 | #[cfg(all(not(feature = "win-bonjour"), target_os = "windows"))] 5 | pub use windows::{ 6 | browse::{browse, BrowseError, ServiceBrowser}, 7 | register::{register_service, RegisteredDnsService, RegistrationError}, 8 | }; 9 | 10 | #[cfg(any(feature = "win-bonjour", not(target_os = "windows")))] 11 | mod apple; 12 | #[cfg(any(feature = "win-bonjour", not(target_os = "windows")))] 13 | pub use apple::{ 14 | browse::{browse, BrowseError, ServiceBrowser}, 15 | register::{register_service, RegisteredDnsService, RegistrationError}, 16 | }; 17 | -------------------------------------------------------------------------------- /src/os/windows.rs: -------------------------------------------------------------------------------- 1 | use std::os::windows::ffi::OsStrExt; 2 | 3 | pub mod browse; 4 | pub mod register; 5 | pub fn to_utf16>(s: S) -> Vec { 6 | s.as_ref().encode_wide().chain(Some(0u16)).collect() 7 | } 8 | -------------------------------------------------------------------------------- /src/os/windows/browse.rs: -------------------------------------------------------------------------------- 1 | use crate::browse::{Result, Service, ServiceEventType}; 2 | use crate::ffi::windows::{ 3 | DNS_FREE_TYPE_DnsFreeRecordList, DnsFree, DnsServiceBrowse, DnsServiceBrowseCancel, 4 | _DNS_SERVICE_BROWSE_REQUEST__bindgen_ty_1 as BrowseCallbackUnion, DNS_QUERY_REQUEST_VERSION1, 5 | DNS_TYPE_A, DNS_TYPE_AAAA, DNS_TYPE_PTR, DNS_TYPE_SRV, DNS_TYPE_TEXT, DWORD, PDNS_RECORD, 6 | PVOID, _DNS_SERVICE_BROWSE_REQUEST, _DNS_SERVICE_CANCEL, 7 | }; 8 | use crate::os::windows::to_utf16; 9 | use crate::ServiceBrowserBuilder; 10 | use std::collections::HashMap; 11 | use std::convert::TryFrom; 12 | use std::io::{Error as IoError, ErrorKind}; 13 | use std::net::{Ipv4Addr, Ipv6Addr}; 14 | use std::ptr::null_mut; 15 | use std::str::Utf8Error; 16 | use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError, SyncSender}; 17 | use std::time::Duration; 18 | use thiserror::Error; 19 | use widestring::{U16CStr, U16CString}; 20 | use winapi::shared::winerror::DNS_REQUEST_PENDING; 21 | 22 | /// Error while browsing for DNS-SD services 23 | #[derive(Debug, Error)] 24 | pub enum BrowseError { 25 | /// Timeout waiting for more data, there may be no data available at this time 26 | #[error("Timeout waiting for data")] 27 | Timeout, 28 | /// IO Error 29 | #[error("IO Error: {0}")] 30 | IoError(#[from] IoError), 31 | /// Error from DnsService APIs 32 | #[error("Error from DNS Service APIs: {0}")] 33 | DnsError(DWORD), 34 | /// Error processing UTF8 string bytes from C API 35 | #[error("Error creating string from UTF8: {0}")] 36 | Utf8StringError(#[from] Utf8Error), 37 | } 38 | enum DnsRecord { 39 | Ptr(String), 40 | Srv { port: u16, hostname: String }, 41 | Txt(HashMap), 42 | A(Ipv4Addr), 43 | Aaaa(Ipv6Addr), 44 | } 45 | 46 | fn process_name(name: &str) -> Option<(String, String, String)> { 47 | // split doesn't do reverse so collect then reverse... 48 | let mut split = name.split('.').collect::>().into_iter().rev(); 49 | let domain = split.next()?; 50 | let ip_protocol = split.next()?; 51 | let protocol = split.next()?; 52 | let name: String = split.collect::>().join("."); 53 | Some((name, format!("{}.{}", protocol, ip_protocol), domain.into())) 54 | } 55 | 56 | fn services_from_record_list(start_record: PDNS_RECORD) -> Result { 57 | let mut service = Service { 58 | name: "".to_string(), 59 | regtype: "".to_string(), 60 | interface_index: None, 61 | domain: "".to_string(), 62 | event_type: ServiceEventType::Added, 63 | hostname: "".to_string(), 64 | port: 0, 65 | txt_record: None, 66 | }; 67 | let mut current_record = start_record; 68 | while !current_record.is_null() { 69 | match DnsRecord::try_from(current_record) { 70 | Ok(DnsRecord::Ptr(name)) => { 71 | if let Some((name, regtype, domain)) = process_name(&name) { 72 | service.name = name; 73 | service.regtype = regtype; 74 | service.domain = domain; 75 | } 76 | } 77 | Ok(DnsRecord::Srv { port, hostname }) => { 78 | service.port = port; 79 | service.hostname = hostname; 80 | } 81 | Ok(DnsRecord::Txt(hash)) => { 82 | if !hash.is_empty() { 83 | service.txt_record = Some(hash); 84 | } 85 | } 86 | Ok(DnsRecord::A(_ip)) => {} 87 | Ok(DnsRecord::Aaaa(_ip)) => {} 88 | Err(e) => { 89 | error!("Error processing DNS record, skipping it: {:?}", e); 90 | } 91 | } 92 | unsafe { 93 | current_record = (*current_record).pNext; 94 | } 95 | } 96 | Ok(service) 97 | } 98 | 99 | impl TryFrom for DnsRecord { 100 | type Error = BrowseError; 101 | 102 | fn try_from(record: PDNS_RECORD) -> std::result::Result { 103 | if record.is_null() { 104 | return Err(IoError::from(ErrorKind::InvalidData).into()); 105 | } 106 | let t = unsafe { (*record).wType } as u32; // what type of record 107 | // let mut ptr_name = String::from("Unknown"); 108 | match t { 109 | DNS_TYPE_PTR => unsafe { 110 | let data = (*record).Data.Ptr; 111 | let name = U16CString::from_ptr_str(data.pNameHost); 112 | let name = name.to_string_lossy(); 113 | trace!("PTR Name: {}", name); 114 | Ok(DnsRecord::Ptr(name)) 115 | }, 116 | DNS_TYPE_SRV => unsafe { 117 | let data = (*record).Data.Srv; 118 | let port = data.wPort; 119 | let hostname = U16CString::from_ptr_str(data.pNameTarget).to_string_lossy(); 120 | trace!("SRV Port: {}", port); 121 | trace!("SRV Target: {hostname}"); 122 | Ok(DnsRecord::Srv { port, hostname }) 123 | }, 124 | DNS_TYPE_TEXT => unsafe { 125 | let strings = std::slice::from_raw_parts( 126 | (*record).Data.Txt.pStringArray.as_ptr(), 127 | (*record).Data.Txt.dwStringCount as _, 128 | ); 129 | let mut hash = HashMap::with_capacity(strings.len()); 130 | for str_ptr in strings { 131 | match U16CStr::from_ptr_str(*str_ptr).to_string() { 132 | Ok(s) => { 133 | let mut split = s.split('='); 134 | match (split.next(), split.next()) { 135 | (Some(k), Some(v)) => { 136 | hash.insert(k.to_string(), v.to_string()); 137 | } 138 | _ => { 139 | warn!("Failed to get key=value from TXT string: {}", s); 140 | } 141 | } 142 | } 143 | Err(e) => { 144 | error!("Error parsing TXT string: {:?}", e); 145 | } 146 | } 147 | } 148 | 149 | Ok(DnsRecord::Txt(hash)) 150 | }, 151 | DNS_TYPE_A => unsafe { 152 | let data = (*record).Data.A; 153 | let ip = Ipv4Addr::from(data.IpAddress.to_le_bytes()); 154 | trace!("IP Address: {}", ip); 155 | Ok(DnsRecord::A(ip)) 156 | }, 157 | DNS_TYPE_AAAA => unsafe { 158 | let data = (*record).Data.AAAA; 159 | let addr = data.Ip6Address; // TODO: bytes are wrong order here 160 | let ip = Ipv6Addr::from(addr.IP6Word); 161 | trace!("IPv6 Address: {}", ip); 162 | Ok(DnsRecord::Aaaa(ip)) 163 | }, 164 | _ => { 165 | warn!("Got record: {:?}, unhandled type", t); 166 | Err(IoError::from(ErrorKind::InvalidData).into()) 167 | } 168 | } 169 | } 170 | } 171 | 172 | pub unsafe extern "C" fn browse_callback(status: DWORD, context: PVOID, record: PDNS_RECORD) { 173 | info!("Browse callback: {}", status); 174 | if status != 0 { 175 | error!("Error in callback: {}", status); 176 | return; 177 | } 178 | if context.is_null() { 179 | error!("Callback has nil context, returning early"); 180 | return; 181 | } 182 | let tx_ptr: *mut SyncSender = context as _; 183 | let tx = &*tx_ptr; 184 | match services_from_record_list(record) { 185 | Ok(service) => { 186 | trace!("{:?}", service); 187 | match tx.send(service) { 188 | Ok(_) => {} 189 | Err(e) => { 190 | error!("Error sending service info: {:?}", e); 191 | } 192 | } 193 | } 194 | Err(e) => { 195 | error!("Error creating services from PDNS_RECORD: {:?}", e); 196 | } 197 | } 198 | DnsFree(record as _, DNS_FREE_TYPE_DnsFreeRecordList); 199 | } 200 | /// Service browser for DNS-SD services 201 | pub struct ServiceBrowser { 202 | cancel: _DNS_SERVICE_CANCEL, 203 | context: *mut SyncSender, 204 | receiver: Receiver, 205 | } 206 | impl Drop for ServiceBrowser { 207 | fn drop(&mut self) { 208 | unsafe { 209 | let r = DnsServiceBrowseCancel(&mut self.cancel); 210 | if r != 0 { 211 | error!("Error canceling service browser: {}", r); 212 | } 213 | self.free_context(); 214 | } 215 | } 216 | } 217 | impl ServiceBrowser { 218 | fn free_context(&mut self) { 219 | if !self.context.is_null() { 220 | _ = unsafe { Box::from_raw(self.context) }; 221 | self.context = null_mut(); 222 | } 223 | } 224 | /// Receives any newly discovered services if any 225 | pub fn recv_timeout(&self, timeout: Duration) -> Result { 226 | match self.receiver.recv_timeout(timeout) { 227 | Ok(service) => Ok(service), 228 | Err(RecvTimeoutError::Timeout) => Err(BrowseError::Timeout), 229 | Err(RecvTimeoutError::Disconnected) => { 230 | Err(BrowseError::IoError(IoError::from(ErrorKind::BrokenPipe))) 231 | } 232 | } 233 | } 234 | } 235 | pub fn browse(builder: ServiceBrowserBuilder) -> Result { 236 | let name = format!("{}.local", builder.regtype); 237 | let mut name = to_utf16(name); 238 | let callback = BrowseCallbackUnion { 239 | pBrowseCallback: Some(browse_callback), 240 | }; 241 | let (tx, rx) = sync_channel::(10); 242 | let tx = Box::into_raw(Box::new(tx)); 243 | let mut request = _DNS_SERVICE_BROWSE_REQUEST { 244 | Version: DNS_QUERY_REQUEST_VERSION1, 245 | InterfaceIndex: 0, 246 | QueryName: name.as_mut_ptr(), 247 | __bindgen_anon_1: callback, 248 | pQueryContext: tx as _, 249 | }; 250 | unsafe { 251 | let mut cancel: _DNS_SERVICE_CANCEL = std::mem::zeroed(); 252 | let r = DnsServiceBrowse(&mut request, &mut cancel) as u32; 253 | if r != DNS_REQUEST_PENDING { 254 | return Err(BrowseError::DnsError(r)); 255 | } 256 | Ok(ServiceBrowser { 257 | cancel, 258 | context: tx, 259 | receiver: rx, 260 | }) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/os/windows/register.rs: -------------------------------------------------------------------------------- 1 | use crate::ffi::windows as ffi; 2 | use crate::ffi::windows::{DWORD, PDNS_SERVICE_INSTANCE, PVOID}; 3 | use crate::os::windows::to_utf16; 4 | use crate::DNSServiceBuilder; 5 | use std::convert::TryFrom; 6 | use std::ffi::OsString; 7 | use std::fmt; 8 | use std::io::{Error as IoError, ErrorKind}; 9 | use std::os::windows::ffi::OsStringExt; 10 | use std::ptr::null_mut; 11 | use std::sync::mpsc::{sync_channel, SyncSender}; 12 | use std::time::Duration; 13 | use thiserror::Error; 14 | use winapi::shared::winerror::DNS_REQUEST_PENDING; 15 | use winapi::um::winbase::GetComputerNameW; 16 | 17 | const CALLBACK_TIMEOUT: Duration = Duration::from_secs(10); 18 | 19 | /// Errors during DNS-SD registration 20 | #[derive(Debug, Error)] 21 | pub enum RegistrationError { 22 | /// IO Error 23 | #[error("IO Error: {0}")] 24 | IoError(#[from] IoError), 25 | /// Error occurred during registration, non-successful DNS return code 26 | #[error("DNS return code error: {0}")] 27 | DnsStatusError(DWORD), 28 | } 29 | 30 | /// Registration result type 31 | pub type Result = std::result::Result; 32 | 33 | struct KeyValues { 34 | keys: Vec>, 35 | _values: Vec>, 36 | keys_ptr: Vec<*mut u16>, 37 | values_ptr: Vec<*mut u16>, 38 | } 39 | 40 | trait DNSServiceExt { 41 | fn host_name(&self) -> String; 42 | fn service_name(&self) -> String; 43 | fn txt_key_values(&self) -> Option; 44 | } 45 | 46 | impl DNSServiceExt for DNSServiceBuilder { 47 | fn host_name(&self) -> String { 48 | let host = self 49 | .host 50 | .clone() 51 | .or(computer_name()) 52 | .unwrap_or_else(|| String::from("Unknown")); 53 | format!("{}.local", host) 54 | } 55 | 56 | fn service_name(&self) -> String { 57 | let name = self 58 | .name 59 | .clone() 60 | .or(computer_name()) 61 | .unwrap_or_else(|| String::from("Unknown")); 62 | format!("{}.{}.local", name, self.regtype) 63 | } 64 | fn txt_key_values(&self) -> Option { 65 | let len = self.txt.as_ref()?.len(); 66 | let mut keys = Vec::with_capacity(len); 67 | let mut values = Vec::with_capacity(len); 68 | for (key, value) in self.txt.as_ref()?.iter() { 69 | keys.push(to_utf16(key)); 70 | values.push(to_utf16(value)); 71 | } 72 | let keys_ptr = keys.iter_mut().map(|k| k.as_mut_ptr()).collect(); 73 | let values_ptr = values.iter_mut().map(|v| v.as_mut_ptr()).collect(); 74 | Some(KeyValues { 75 | keys, 76 | _values: values, 77 | keys_ptr, 78 | values_ptr, 79 | }) 80 | } 81 | } 82 | 83 | fn computer_name() -> Option { 84 | unsafe { 85 | let mut buf = vec![0u16; 1024]; 86 | let mut len = buf.len() as u32; 87 | if GetComputerNameW(buf.as_mut_ptr(), &mut len) != 0 { 88 | return Some( 89 | OsString::from_wide(&buf[0..len as usize]) 90 | .into_string() 91 | .unwrap(), 92 | ); 93 | } 94 | } 95 | None 96 | } 97 | 98 | unsafe extern "C" fn register_callback( 99 | status: DWORD, 100 | context: PVOID, 101 | instance: PDNS_SERVICE_INSTANCE, 102 | ) { 103 | if !context.is_null() { 104 | let tx_ptr: *mut SyncSender = context as _; 105 | let tx = &*tx_ptr; 106 | trace!("Register complete: {} return code", status); 107 | tx.send(status).unwrap(); 108 | } 109 | ffi::DnsServiceFreeInstance(instance); 110 | } 111 | /// Opaque type for a registered DNS-SD service, de-registering on drop 112 | pub struct RegisteredDnsService { 113 | registered: bool, 114 | name: String, 115 | host: String, 116 | request: ffi::_DNS_SERVICE_REGISTER_REQUEST, 117 | service: *mut ffi::_DNS_SERVICE_INSTANCE, 118 | } 119 | impl fmt::Debug for RegisteredDnsService { 120 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 121 | write!(f, "RegisteredDnsService {{ {} {} }}", self.name, self.host) 122 | } 123 | } 124 | impl RegisteredDnsService { 125 | fn free_context(&mut self) { 126 | if !self.request.pQueryContext.is_null() { 127 | _ = unsafe { Box::from_raw(self.request.pQueryContext as *mut SyncSender) }; 128 | self.request.pQueryContext = null_mut(); 129 | } 130 | } 131 | fn register(&mut self) -> Result<()> { 132 | if self.registered { 133 | warn!("Service already registered"); 134 | return Ok(()); 135 | } 136 | trace!( 137 | "Registering: name: {} host: {} port: {}", 138 | self.name, 139 | self.host, 140 | unsafe { (*self.service).wPort } 141 | ); 142 | 143 | let (tx, rx) = sync_channel::(1); 144 | let tx = Box::into_raw(Box::new(tx)); 145 | self.request.pQueryContext = tx as _; 146 | let result = unsafe { ffi::DnsServiceRegister(&mut self.request, std::ptr::null_mut()) }; 147 | if result != DNS_REQUEST_PENDING { 148 | error!("Failed to register: {}", result); 149 | self.free_context(); 150 | return Err(IoError::from_raw_os_error(result as _).into()); 151 | } 152 | 153 | match rx.recv_timeout(CALLBACK_TIMEOUT) { 154 | Ok(0) => { 155 | // DNS_RCODE_NOERROR, from: https://docs.microsoft.com/en-us/windows/win32/dns/dns-constants#dns-response-codes 156 | self.free_context(); 157 | self.registered = true; 158 | Ok(()) 159 | } 160 | Ok(e) => { 161 | error!("Registration callback returned error: {}", e); 162 | self.free_context(); 163 | Err(RegistrationError::DnsStatusError(e)) 164 | } 165 | Err(_e) => { 166 | error!("Timed out waiting for registration callback"); 167 | self.free_context(); 168 | Err( 169 | IoError::new(ErrorKind::TimedOut, "Timed out waiting for async callback") 170 | .into(), 171 | ) 172 | } 173 | } 174 | } 175 | } 176 | impl TryFrom for RegisteredDnsService { 177 | type Error = std::io::Error; 178 | fn try_from(service: DNSServiceBuilder) -> Result { 179 | unsafe { 180 | let original_name = service.service_name(); 181 | let original_host = service.host_name(); 182 | let mut name = to_utf16(&original_name); 183 | let mut host = to_utf16(&original_host); 184 | 185 | let mut kv_store = service.txt_key_values(); 186 | let (property_count, keys_ptr, values_ptr) = match kv_store.as_mut() { 187 | Some(kv) => ( 188 | kv.keys.len(), 189 | kv.keys_ptr.as_mut_ptr(), 190 | kv.values_ptr.as_mut_ptr(), 191 | ), 192 | None => (0, null_mut() as _, null_mut() as _), 193 | }; 194 | // behavior suggests this copies it's arguments, so we can use pointers to rust stack here 195 | let service = ffi::DnsServiceConstructInstance( 196 | name.as_mut_ptr(), 197 | host.as_mut_ptr(), 198 | null_mut(), 199 | null_mut(), 200 | service.port, 201 | 0, 202 | 0, 203 | property_count as _, 204 | keys_ptr as _, 205 | values_ptr as _, 206 | ); 207 | if service.is_null() { 208 | let error = IoError::last_os_error(); 209 | error!("Failed to create service: {:?}", error); 210 | return Err(error); 211 | } 212 | let request = ffi::_DNS_SERVICE_REGISTER_REQUEST { 213 | Version: ffi::DNS_QUERY_REQUEST_VERSION1, 214 | InterfaceIndex: 0, // 0 says all interfaces 215 | pServiceInstance: service, 216 | pRegisterCompletionCallback: Some(register_callback), 217 | pQueryContext: null_mut(), 218 | hCredentials: null_mut(), 219 | unicastEnabled: 0, // false for mDNS protocol to advertise 220 | }; 221 | Ok(RegisteredDnsService { 222 | name: original_name, 223 | host: original_host, 224 | registered: false, 225 | request, 226 | service, 227 | }) 228 | } 229 | } 230 | } 231 | 232 | impl Drop for RegisteredDnsService { 233 | fn drop(&mut self) { 234 | if self.registered { 235 | trace!("De-registering service..."); 236 | let r = unsafe { ffi::DnsServiceDeRegister(&mut self.request, std::ptr::null_mut()) }; 237 | if r != DNS_REQUEST_PENDING { 238 | error!("Failed to de-register service: {}", r); 239 | } 240 | } 241 | 242 | if !self.service.is_null() { 243 | trace!("Freeing service"); 244 | unsafe { ffi::DnsServiceFreeInstance(self.service) }; 245 | self.service = std::ptr::null_mut(); 246 | } 247 | } 248 | } 249 | // should be safe to send across threads, just not access across 250 | unsafe impl Send for RegisteredDnsService {} 251 | 252 | pub fn register_service(service: DNSServiceBuilder) -> Result { 253 | let mut service = RegisteredDnsService::try_from(service)?; 254 | service.register()?; 255 | Ok(service) 256 | } 257 | -------------------------------------------------------------------------------- /src/register.rs: -------------------------------------------------------------------------------- 1 | use crate::os::{register_service, RegisteredDnsService, RegistrationError}; 2 | use std::collections::HashMap; 3 | pub type Result = std::result::Result; 4 | 5 | /// Builder for creating a new DNSService for registration purposes 6 | pub struct DNSServiceBuilder { 7 | pub(crate) regtype: String, 8 | pub(crate) name: Option, 9 | pub(crate) domain: Option, 10 | pub(crate) host: Option, 11 | pub(crate) port: u16, 12 | pub(crate) txt: Option>, 13 | } 14 | impl DNSServiceBuilder { 15 | /// Starts a new service builder with a given type (i.e. _http._tcp) 16 | pub fn new(regtype: &str, port: u16) -> DNSServiceBuilder { 17 | DNSServiceBuilder { 18 | regtype: String::from(regtype), 19 | name: None, 20 | domain: None, 21 | host: None, 22 | port, 23 | txt: None, 24 | } 25 | } 26 | 27 | /// Name to use for service, defaults to hostname 28 | pub fn with_name(mut self, name: &str) -> DNSServiceBuilder { 29 | self.name = Some(String::from(name)); 30 | self 31 | } 32 | 33 | /// Domain to register service on, default is .local+ 34 | pub fn with_domain(mut self, domain: &str) -> DNSServiceBuilder { 35 | self.domain = Some(String::from(domain)); 36 | self 37 | } 38 | 39 | /// Host to use for service, defaults to machine's host 40 | pub fn with_host(mut self, host: &str) -> DNSServiceBuilder { 41 | self.host = Some(String::from(host)); 42 | self 43 | } 44 | 45 | /// Includes a TXT record for the service 46 | pub fn with_txt_record(mut self, txt: HashMap) -> DNSServiceBuilder { 47 | self.txt = Some(txt); 48 | self 49 | } 50 | /// Adds key & value to TXT, creating a map if none yet 51 | pub fn with_key_value(mut self, key: String, value: String) -> DNSServiceBuilder { 52 | let mut kv = self.txt.take().unwrap_or_default(); 53 | kv.insert(key, value); 54 | self.txt = Some(kv); 55 | self 56 | } 57 | /// Registers service, advertising it on the network 58 | pub fn register(self) -> Result { 59 | register_service(self) 60 | } 61 | } 62 | --------------------------------------------------------------------------------