├── .circleci └── config.yml ├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github └── dependabot.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSES ├── Apache-2.0.txt ├── CC-BY-4.0.txt └── CC0-1.0.txt ├── NOTICE ├── README.md ├── REUSE.toml ├── config └── config.exs ├── lib ├── mdns_lite.ex ├── mdns_lite │ ├── application.ex │ ├── cache.ex │ ├── client.ex │ ├── core_monitor.ex │ ├── dns.ex │ ├── dns_bridge.ex │ ├── if_info.ex │ ├── inet_monitor.ex │ ├── info.ex │ ├── options.ex │ ├── responder.ex │ ├── responder_supervisor.ex │ ├── table.ex │ ├── table │ │ └── builder.ex │ ├── table_server.ex │ ├── utilities.ex │ └── vintage_net_monitor.ex └── mix │ └── tasks │ └── mdns_lite.install.ex ├── mix.exs ├── mix.lock ├── src ├── mdns_lite_inet_dns.erl ├── mdns_lite_inet_dns.hrl ├── mdns_lite_inet_dns_record_adts.hrl └── mdns_lite_inet_int.hrl └── test ├── mdns_lite ├── cache_test.exs ├── client_test.exs ├── core_monitor_test.exs ├── dns_test.exs ├── info_test.exs ├── mix │ └── tasks │ │ └── install_test.exs ├── options_test.exs ├── responder_test.exs ├── table │ └── builder_test.exs ├── table_test.exs └── utilities_test.exs ├── mdns_lite_test.exs └── test_helper.exs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | latest: &latest 4 | pattern: "^1.18.*-erlang-27.*$" 5 | 6 | tags: 7 | &tags [ 8 | 1.18.3-erlang-27.3.3-alpine-3.21.3, 9 | 1.17.2-erlang-27.0.1-alpine-3.20.2, 10 | 1.16.0-erlang-26.2.1-alpine-3.18.4, 11 | 1.15.7-erlang-26.1.2-alpine-3.18.4, 12 | 1.14.5-erlang-25.3.2-alpine-3.18.0 13 | ] 14 | 15 | jobs: 16 | check-license: 17 | docker: 18 | - image: fsfe/reuse:latest 19 | steps: 20 | - checkout 21 | - run: reuse lint 22 | 23 | build-test: 24 | parameters: 25 | tag: 26 | type: string 27 | docker: 28 | - image: hexpm/elixir:<< parameters.tag >> 29 | working_directory: ~/repo 30 | environment: 31 | LC_ALL: C.UTF-8 32 | steps: 33 | - run: 34 | name: Install system dependencies 35 | command: apk add --no-cache build-base libmnl-dev libnl3-dev 36 | - checkout 37 | - run: 38 | name: Install hex and rebar 39 | command: | 40 | mix local.hex --force 41 | mix local.rebar --force 42 | - restore_cache: 43 | keys: 44 | - v1-mix-cache-<< parameters.tag >>-{{ checksum "mix.lock" }} 45 | - run: mix deps.get 46 | - run: mix test 47 | - when: 48 | condition: 49 | matches: { <<: *latest, value: << parameters.tag >> } 50 | steps: 51 | - run: mix format --check-formatted 52 | - run: mix deps.unlock --check-unused 53 | - run: mix compile --warnings-as-errors 54 | - run: mix docs 55 | - run: mix hex.build 56 | - run: mix credo -a --strict 57 | - run: mix dialyzer 58 | - save_cache: 59 | key: v1-mix-cache-<< parameters.tag >>-{{ checksum "mix.lock" }} 60 | paths: 61 | - _build 62 | - deps 63 | 64 | automerge: 65 | docker: 66 | - image: alpine:3.18.4 67 | steps: 68 | - run: 69 | name: Install GitHub CLI 70 | command: apk add --no-cache build-base github-cli 71 | - run: 72 | name: Attempt PR automerge 73 | command: | 74 | author=$(gh pr view "${CIRCLE_PULL_REQUEST}" --json author --jq '.author.login' || true) 75 | 76 | if [ "$author" = "app/dependabot" ]; then 77 | gh pr merge "${CIRCLE_PULL_REQUEST}" --auto --rebase || echo "Failed trying to set automerge" 78 | else 79 | echo "Not a dependabot PR, skipping automerge" 80 | fi 81 | 82 | workflows: 83 | checks: 84 | jobs: 85 | - check-license: 86 | filters: 87 | tags: 88 | only: /.*/ 89 | - build-test: 90 | name: << matrix.tag >> 91 | matrix: 92 | parameters: 93 | tag: *tags 94 | 95 | - automerge: 96 | requires: *tags 97 | context: org-global 98 | filters: 99 | branches: 100 | only: /^dependabot.*/ 101 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # .credo.exs 2 | %{ 3 | configs: [ 4 | %{ 5 | name: "default", 6 | strict: true, 7 | checks: [ 8 | {Credo.Check.Design.TagTODO, false}, 9 | {Credo.Check.Refactor.MapInto, false}, 10 | {Credo.Check.Warning.LazyLogging, false}, 11 | {Credo.Check.Readability.LargeNumbers, only_greater_than: 86400}, 12 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true}, 13 | {Credo.Check.Readability.Specs, tags: []}, 14 | {Credo.Check.Readability.StrictModuleLayout, tags: []} 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | # Run `mix dialyzer --format short` for strings 2 | [ 3 | {"lib/mdns_lite/dns.ex:34:contract_supertype Type specification for decode is a supertype of the success typing."} 4 | ] 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter,.dialyzer_ignore,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | mdns_lite-*.tar 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## v0.9.0 - 2025-05-30 6 | 7 | This release adds experimental support for using 8 | [igniter](https://hex.pm/packages/igniter) for installation. This currently 9 | isn't needed since `mix nerves.new` adds it by default, but that may change in 10 | the future. Igniter is not included at runtime. 11 | 12 | * Changes 13 | * Add Igniter installation support (@wln) 14 | * Clarify copyright and licensing for [REUSE](https://reuse.software/) compliance 15 | 16 | ## v0.8.11 - 2024-09-10 17 | 18 | * Bug fixes 19 | * Fix `MdnsLite.InetMonitor` to properly remove interfaces that don't exist in a subsequent update. (@kevinschweikert) 20 | * Exclude "__unknown" interfaces to ignore interfaces that VintageNet can't get link info on. (@ConnorRigby) 21 | * Don't crash on other Unix-like OSes (@mneumann) 22 | 23 | ## v0.8.10 - 2024-02-26 24 | 25 | * Bug fixes 26 | * Really fix crash when cleaning up responders when a network goes down. The 27 | fix in v0.8.9 had an issue that prevented it from working. 28 | 29 | ## v0.8.9 - 2024-02-02 30 | 31 | * Bug fixes 32 | * Switch bridge recursive lookup to default to false. The issue that this 33 | works around has been fixed since OTP 24.1. 34 | * Handle crash when cleaning up responders when a network goes down. 35 | 36 | ## v0.8.8 - 2023-05-26 37 | 38 | * New feature 39 | * IPv6 queries are now supported. Responding to IPv6 isn't supported yet. To 40 | use this, be sure to set `ipv4_only: false` since this isn't the default. 41 | Thanks to @bjyoungblood for this feature. 42 | 43 | ## v0.8.7 - 2023-02-12 44 | 45 | * Fixed 46 | * Fix Elixir 1.15 deprecation warnings 47 | 48 | ## v0.8.6 - 2022-06-14 49 | 50 | * Fixed 51 | * Fixed an issue that caused the DNS bridge to stop working with OTP 25. 52 | 53 | ## v0.8.5 - 2022-04-28 54 | 55 | * Fixed 56 | * If a network interface changes IP addresses, there would be a flurry of 57 | crashes when it was no longer possible to bind to the interface. This stops 58 | behavior and shuts down the responder for the interface. 59 | 60 | ## v0.8.4 - 2021-11-13 61 | 62 | * New feature 63 | * VintageNet is an optional dependency now. This makes it possible to use 64 | MdnsLite outside of Nerves much more easily. 65 | 66 | * Fixed 67 | * Use the new DNS encoder/decoder from OTP 24.1.5. This fixes a regression 68 | with OTP 24.1.2 where the DNS encoder and decoder was updated to be more 69 | correct in how it handled the DNS class. mDNS repurposes the high bit of the 70 | DNS class. Previously we had gotten lucky. OTP 24.1.5 adds support for the 71 | bit. To make sure that MdnsLite can work on other OTP versions, the new 72 | OTP code has been vendored and included with MdnsLite. 73 | 74 | ## v0.8.3 - 2021-10-07 75 | 76 | * Fixed 77 | * Added configuration and runtime support for setting the instance name. This 78 | was incorrectly removed in v0.8.0. By default, MdnsLite will advertise 79 | itself using the hostname. This works, but looks unfriendly in the service 80 | discovery results. Setting the instance name lets you advertise with a nice 81 | human readable name. Thanks to Mat Trudel for both catching this regression 82 | and fixing it. 83 | 84 | ## v0.8.2 - 2021-09-23 85 | 86 | * Fixed 87 | * Fix calls to `:socket.setopt/3` to support OTP 22 and OTP 23. Thanks to 88 | Peter Madsen for finding this and providing a fix. 89 | 90 | ## v0.8.1 - 2021-09-19 91 | 92 | * Fixed 93 | * Fix interface monitor crash when a network interface gets removed. 94 | 95 | ## v0.8.0 96 | 97 | This release is a major update to MdnsLite to support making queries in 98 | addition to responding to queries. The runtime API is not backwards compatible. 99 | If you're only using the application environment to configure MdnsLite, you 100 | should be ok. 101 | 102 | * New features 103 | * Make mDNS requests 104 | * Add a DNS bridge for Erlang's DNS resolver. This enables Erlang 105 | distribution and `:gen_tcp` users to be passed `.local` hostnames. See docs 106 | for how to configure 107 | * mDNS record caching 108 | * mDNS record inspection - both for ones MdnsLite advertises and for ones in 109 | the caches 110 | * AAAA record support - Proper IPv6 support is still not available 111 | 112 | * Bug fixes 113 | * MdnsLite now uses `:socket` to send and receive mDNS messages. This fixes 114 | several issues where multicast packets were being mixed up between network 115 | interfaces. 116 | 117 | ## v0.7.0 118 | 119 | * Breaking change 120 | * Change optional dependency on VintageNet to a mandatory one. Probably all 121 | `:mdns_lite` users were already using VintageNet and since Mix releases 122 | doesn't support optional dependencies yet, some users got errors when the 123 | release misordered them. This avoids the problem. 124 | 125 | * Improvements 126 | * Removed the `:dns` package dependency. There as an Erlang crypto API call in 127 | a dependency of `:dns` that was removed in OTP 24. This change makes it 128 | possible to use `:mdns_lite` on OTP 24 without worrying about a missing 129 | crypto API call. 130 | 131 | ## v0.6.7 132 | 133 | * Improvements 134 | * Exclude `"wwan0"` by default. These interfaces are cellular links like ppp 135 | and it's not appropriate to respond to mDNS on them either. 136 | 137 | ## v0.6.6 138 | 139 | * Bug fixes 140 | * Advertise services based on service names & not hostname. Thanks to Matt 141 | Trudel for this fix. 142 | 143 | ## v0.6.5 144 | 145 | * Bug fixes 146 | * Reuse addresses and ports when binding to the multicast socket to coexist 147 | with other mDNS software. Thanks to Eduardo Cunha and Matt Myers for the 148 | updates. 149 | 150 | ## v0.6.4 151 | 152 | * New features 153 | * Support custom TXT record contents. See the `:txt_payload`. Thanks to 154 | Eduardo Cunha for adding this. 155 | 156 | ## v0.6.3 157 | 158 | * Bug fixes 159 | * Update default so that ppp interfaces are ignored. This prevents surprises 160 | of having a responder run on a cellular link. 161 | 162 | ## v0.6.2 163 | 164 | * Bug fixes 165 | * Fix crash when handling undecodable mDNS messages 166 | 167 | ## v0.6.1 168 | 169 | * Handle nil from VintageNet reports 170 | 171 | ## v0.6.0 172 | 173 | * Allow mdns host to be change at runtime 174 | * New network monitor: VintageNetMonitor 175 | 176 | ## v0.5.0 177 | 178 | * Allow services to be added and removed at runtime. 179 | 180 | ## v0.4.3 181 | 182 | * Correct typos and white space 183 | * Comment out logger messages 184 | 185 | ## v0.4.2 186 | 187 | * Remove un-helpful Logger.debug statements - Issue #49 188 | * Put this file into the proper order. 189 | 190 | ## v0.4.1 191 | 192 | * Correct bad tag in README.md and correct grammar. 193 | * Correct documentation of the MdnsLite module 194 | 195 | ## v0.4.0 196 | 197 | * The value of host in the configuration file can have two values. The second can serve as an alias for the first. 198 | * Updated documentation and comments. 199 | * Created a new test. 200 | 201 | ## v0.3.0 202 | 203 | * Remove a superfluous map from the config. 204 | 205 | ## v0.2.1 206 | 207 | * Update README to reflect changes in previous version. 208 | 209 | ## v0.2.0 210 | 211 | * Much better alignment with RFC 6763 - DNS Service-based discovery. 212 | * Affects handling of SRV and PTR queries. 213 | 214 | ## v0.1.0 215 | 216 | * Initial release 217 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 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, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. 10 | 11 | Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. 12 | 13 | Creative Commons Attribution 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | Section 1 – Definitions. 18 | 19 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 24 | 25 | d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 26 | 27 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | 29 | f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | 31 | g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 32 | 33 | h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 34 | 35 | i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 36 | 37 | j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 38 | 39 | k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 40 | 41 | Section 2 – Scope. 42 | 43 | a. License grant. 44 | 45 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 46 | 47 | A. reproduce and Share the Licensed Material, in whole or in part; and 48 | 49 | B. produce, reproduce, and Share Adapted Material. 50 | 51 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 52 | 53 | 3. Term. The term of this Public License is specified in Section 6(a). 54 | 55 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 56 | 57 | 5. Downstream recipients. 58 | 59 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 60 | 61 | B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 62 | 63 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 64 | 65 | b. Other rights. 66 | 67 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 68 | 69 | 2. Patent and trademark rights are not licensed under this Public License. 70 | 71 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 72 | 73 | Section 3 – License Conditions. 74 | 75 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 76 | 77 | a. Attribution. 78 | 79 | 1. If You Share the Licensed Material (including in modified form), You must: 80 | 81 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 82 | 83 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 84 | 85 | ii. a copyright notice; 86 | 87 | iii. a notice that refers to this Public License; 88 | 89 | iv. a notice that refers to the disclaimer of warranties; 90 | 91 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 92 | 93 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 94 | 95 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 96 | 97 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 98 | 99 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 100 | 101 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 102 | 103 | Section 4 – Sui Generis Database Rights. 104 | 105 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 106 | 107 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 108 | 109 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 110 | 111 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 112 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 113 | 114 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 115 | 116 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 117 | 118 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 119 | 120 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 121 | 122 | Section 6 – Term and Termination. 123 | 124 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 125 | 126 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 127 | 128 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 129 | 130 | 2. upon express reinstatement by the Licensor. 131 | 132 | c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 133 | 134 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 135 | 136 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 137 | 138 | Section 7 – Other Terms and Conditions. 139 | 140 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 141 | 142 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 143 | 144 | Section 8 – Interpretation. 145 | 146 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 147 | 148 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 149 | 150 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 151 | 152 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 153 | 154 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 155 | 156 | Creative Commons may be contacted at creativecommons.org. 157 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | MdnsLite is open-source software licensed under the Apache License, Version 2 | 2.0. 3 | 4 | Copyright holders include Ericsson AB, Frank Hunleth, Jon Carstens, Peter C. 5 | Marks, Eduardo Cunha, Connor Rigby, Mat Trudel, Peter Madsen-mygdal, Ben 6 | Youngblood, Kevin Schweikert and Michael Neumann. 7 | 8 | Authoritative REUSE-compliant copyright and license metadata available at 9 | https://hex.pm/packages/mdns_lite. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MdnsLite 2 | 3 | [![Hex version](https://img.shields.io/hexpm/v/mdns_lite.svg "Hex version")](https://hex.pm/packages/mdns_lite) 4 | [![API docs](https://img.shields.io/hexpm/v/mdns_lite.svg?label=hexdocs "API docs")](https://hexdocs.pm/mdns_lite/MdnsLite.html) 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/nerves-networking/mdns_lite/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/nerves-networking/mdns_lite/tree/main) 6 | [![REUSE status](https://api.reuse.software/badge/github.com/nerves-networking/mdns_lite)](https://api.reuse.software/info/github.com/nerves-networking/mdns_lite) 7 | 8 | MdnsLite is a simple, limited, no frills implementation of an 9 | [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) (Multicast Domain Name 10 | System) client and server. It operates like DNS, but uses multicast instead of 11 | unicast so that any computer on a LAN can help resolve names. In particular, it 12 | resolves hostnames that end in `.local` and provides a way to advertise and 13 | discovery service. 14 | 15 | MdnsLite is intended for environments like on Nerves devices that do not already 16 | have an `mDNS` service. If you're running on desktop Linux or on MacOS, you 17 | already have `mDNS` support and do not need MdnsLite. 18 | 19 | Features of MdnsLite: 20 | 21 | * Advertise `.local` and aliases for ease of finding devices 22 | * Static (application config) and dynamic service registration 23 | * Support for multi-homed devices. For example, mDNS responses sent on a network 24 | interface have the expected IP addresses for that interface. 25 | * DNS bridging so that Erlang's built-in DNS resolver can look up `.local` names 26 | via mDNS. 27 | * Caching of results and advertisements seen on the network 28 | * Integration with 29 | [VintageNet](https://github.com/nerves-networking/vintage_net) and Erlang's 30 | `:inet` application for network interface monitoring 31 | * Easy inspection of mDNS record tables to help debug service discovery issues 32 | 33 | MdnsLite is included in [NervesPack](https://hex.pm/packages/nerves_pack) so you 34 | might already have it! 35 | 36 | ## Configuration 37 | 38 | A typical configuration in the `config.exs` file looks like: 39 | 40 | ```elixir 41 | config :mdns_lite, 42 | # Advertise `hostname.local` on the LAN 43 | hosts: [:hostname], 44 | # If instance_name is not defined it defaults to the first hostname 45 | instance_name: "Awesome Device", 46 | services: [ 47 | # Advertise an HTTP server running on port 80 48 | %{ 49 | id: :web_service, 50 | protocol: "http", 51 | transport: "tcp", 52 | port: 80, 53 | }, 54 | # Advertise an SSH daemon on port 22 55 | %{ 56 | id: :ssh_daemon, 57 | protocol: "ssh", 58 | transport: "tcp", 59 | port: 22, 60 | } 61 | ] 62 | ``` 63 | 64 | The `services` section lists the services that the host offers, such as 65 | providing an HTTP server. Specifying a `protocol`, `transport` and `port` is 66 | usually the easiest way. The `protocol` and `transport` get combined to form the 67 | service type that's actually advertised on the network. For example, a "tcp" 68 | transport and "ssh" protocol will end up as `"_ssh._tcp"` in the advertisement. 69 | If you need something custom, specify `:type` directly. Optional fields include 70 | `:id`, `:weight`, `:priority`, `:instance_name` and `:txt_payload`. An `:id` is 71 | needed to remove the service advertisement at runtime. If not specified, 72 | `:instance_name` is inherited from the top-level config. A `:txt_payload` is a 73 | list of `"="` string that will be advertised in a TXT DNS record 74 | corresponding to the service. 75 | 76 | See [`MdnsLite.Options`](https://hexdocs.pm/mdns_lite/MdnsLite.Options.html) for 77 | information about all application environment options. 78 | 79 | It's possible to change the advertised hostnames, instance names and services at 80 | runtime. For example, to change the list of advertised hostnames, run: 81 | 82 | ```elixir 83 | iex> MdnsLite.set_hosts([:hostname, "nerves"]) 84 | :ok 85 | ``` 86 | 87 | To change the advertised instance name: 88 | 89 | ```elixir 90 | iex> MdnsLite.set_instance_name("My Other Awesome Device") 91 | :ok 92 | ``` 93 | 94 | Here's how to add and remove a service at runtime: 95 | 96 | ```elixir 97 | iex> MdnsLite.add_mdns_service(%{ 98 | id: :my_web_server, 99 | protocol: "http", 100 | transport: "tcp", 101 | port: 80, 102 | }) 103 | :ok 104 | iex> MdnsLite.remove_mdns_service(:my_web_server) 105 | :ok 106 | ``` 107 | 108 | ## Client 109 | 110 | `MdnsLite.gethostbyname/1` uses mDNS to resolve hostnames. Here's an example: 111 | 112 | ```elixir 113 | iex> MdnsLite.gethostbyname("my-laptop.local") 114 | {:ok, {172, 31, 112, 98}} 115 | ``` 116 | 117 | If you just want mDNS to "just work" with Erlang, you'll need to enable 118 | MdnsLite's DNS Bridge feature and configure Erlang's DNS resolver to use it. See 119 | the DNS Bridge section for details. 120 | 121 | Service discovery docs TBD... 122 | 123 | ## DNS Bridge configuration 124 | 125 | `MdnsLite` can start a DNS server to respond to `.local` queries. This enables 126 | code that has no knowledge of mDNS to resolve mDNS queries. For example, 127 | Erlang/OTP's built-in DNS resolver doesn't know about mDNS. It's used to resolve 128 | hosts for Erlang distribution and pretty much any code using `:gen_tcp` and 129 | `:gen_udp`. `MdnsLite`'s DNS bridge feature makes `.local` hostname lookups work 130 | for all of this. No code modifications required. 131 | 132 | Note that this feature is useful on Nerves devices. Erlang/OTP can use the 133 | system name resolver on desktop Linux and MacOS. The system name resolver should 134 | already be hooked up to an mDNS resolver there. 135 | 136 | To set this up, you'll need to enable the DNS bridge on `MdnsLite` and then set 137 | up the DNS resolver to use it first. Here are the options for the application 138 | environment: 139 | 140 | ```elixir 141 | config :mdns_lite, 142 | dns_bridge_enabled: true, 143 | dns_bridge_ip: {127, 0, 0, 53}, 144 | dns_bridge_port: 53 145 | 146 | config :vintage_net, 147 | additional_name_servers: [{127, 0, 0, 53}] 148 | ``` 149 | 150 | The choice of running the DNS bridge on 127.0.0.53:53 is mostly arbitrary. This 151 | is the default. 152 | 153 | > #### Info {: .info} 154 | > 155 | > If you're using a version of Erlang/OTP before 24.1, you'll be affected by 156 | > [OTP #5092](https://github.com/erlang/otp/issues/5092). The workaround is to 157 | > add the `dns_bridge_recursive: true` option to the `:mdns_lite` config. 158 | 159 | ## Debugging 160 | 161 | `MdnsLite` maintains a table of records that it advertises and a cache per 162 | network interface. The table of records that it advertises is based solely off 163 | its configuration. Review it by running: 164 | 165 | ```elixir 166 | iex> MdnsLite.Info.dump_records 167 | .in-addr.arpa: type PTR, class IN, ttl 120, nerves-2e6d.local 168 | .ip6.arpa: type PTR, class IN, ttl 120, nerves-2e6d.local 169 | _epmd._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._epmd._tcp.local 170 | _services._dns-sd._udp.local: type PTR, class IN, ttl 120, _epmd._tcp.local 171 | _services._dns-sd._udp.local: type PTR, class IN, ttl 120, _sftp-ssh._tcp.local 172 | _services._dns-sd._udp.local: type PTR, class IN, ttl 120, _ssh._tcp.local 173 | _sftp-ssh._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._sftp-ssh._tcp.local 174 | _ssh._tcp.local: type PTR, class IN, ttl 120, nerves-2e6d._ssh._tcp.local 175 | nerves-2e6d._epmd._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 4369, nerves-2e6d.local. 176 | nerves-2e6d._epmd._tcp.local: type TXT, class IN, ttl 120, 177 | nerves-2e6d._sftp-ssh._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 22, nerves-2e6d.local. 178 | nerves-2e6d._sftp-ssh._tcp.local: type TXT, class IN, ttl 120, 179 | nerves-2e6d._ssh._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 22, nerves-2e6d.local. 180 | nerves-2e6d._ssh._tcp.local: type TXT, class IN, ttl 120, 181 | nerves-2e6d.local: type A, class IN, ttl 120, addr 182 | nerves-2e6d.local: type AAAA, class IN, ttl 120, addr 183 | ``` 184 | 185 | Note that some addresses have not been filled in. They depend on which network 186 | interface receives the query. The idea is that if a computer is looking for you 187 | on the Ethernet interface, you should give records with that Ethernet's 188 | interface rather than, say, the IP address of the WiFi interface. 189 | 190 | `MdnsLite`'s cache is filled with records that it sees advertised. It's 191 | basically the same, but can be quite large depending on the mDNS activity on a 192 | link. It looks like this: 193 | 194 | ```elixir 195 | iex> MdnsLite.Info.dump_caches 196 | Responder: 172.31.112.97 197 | ... 198 | Responder: 192.168.1.58 199 | ... 200 | ``` 201 | 202 | ## In memory 203 | 204 | [Peter Marks](https://github.com/pcmarks/) wrote and maintained the original 205 | version of `mdns_lite`. 206 | 207 | ## License 208 | 209 | Copyright (C) 2019-21 SmartRent 210 | 211 | Licensed under the Apache License, Version 2.0 (the "License"); 212 | you may not use this file except in compliance with the License. 213 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 214 | 215 | Unless required by applicable law or agreed to in writing, software 216 | distributed under the License is distributed on an "AS IS" BASIS, 217 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 218 | See the License for the specific language governing permissions and 219 | limitations under the License. 220 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[annotations]] 4 | path = [ 5 | ".circleci/config.yml", 6 | ".credo.exs", 7 | ".dialyzer_ignore.exs", 8 | ".formatter.exs", 9 | ".github/dependabot.yml", 10 | ".gitignore", 11 | "CHANGELOG.md", 12 | "NOTICE", 13 | "REUSE.toml", 14 | "mix.exs", 15 | "mix.lock" 16 | ] 17 | precedence = "aggregate" 18 | SPDX-FileCopyrightText = "None" 19 | SPDX-License-Identifier = "CC0-1.0" 20 | 21 | [[annotations]] 22 | path = [ 23 | "README.md" 24 | ] 25 | precedence = "aggregate" 26 | SPDX-FileCopyrightText = "2019 Frank Hunleth" 27 | SPDX-License-Identifier = "CC-BY-4.0" 28 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2019 Jon Carstens 3 | # SPDX-FileCopyrightText: 2019 Peter C. Marks 4 | # SPDX-FileCopyrightText: 2020 Eduardo Cunha 5 | # SPDX-FileCopyrightText: 2021 Mat Trudel 6 | # 7 | # SPDX-License-Identifier: Apache-2.0 8 | # 9 | import Config 10 | 11 | config :mdns_lite, 12 | # Use these values to construct the DNS resource record responses 13 | # to a DNS query. 14 | # host can be one of the values: hostname1, [hostname1], or [hostname1, hostname2] 15 | # where hostname1 is the atom :hostname in which case it is replaced with the 16 | # value of :inet.gethostname() or a string and hostname2 is a string value. 17 | # Example: [:hostname, "nerves"] 18 | 19 | hosts: [:hostname, "nerves"], 20 | ttl: 120, 21 | 22 | # instance_name is a user friendly name that will be used as the name for this 23 | # device's advertised service(s). Per RFC6763 Appendix C, this should describe 24 | # the user-facing purpose or description of the device, and should not be 25 | # considered a unique identifier. For example, 'Nerves Device' and 'MatCo 26 | # Laser Printer Model CRM-114' are good choices here. If instance_name is not 27 | # defined it defaults to the first entry in the `hosts` list above 28 | # 29 | # instance_name: "mDNS Lite Device", 30 | 31 | # A list of this host's services. NB: There are two other mDNS values: weight 32 | # and priority that both default to zero unless included in the service below. 33 | # The txt_payload value is optional and can be used to define the data in TXT 34 | # DNS resource records, it should be a list of strings containing a key and 35 | # value separated by a '='. 36 | services: [ 37 | %{ 38 | id: :web_server, 39 | protocol: "http", 40 | transport: "tcp", 41 | port: 80, 42 | txt_payload: ["key=value"] 43 | }, 44 | %{ 45 | id: :ssh_daemon, 46 | protocol: "ssh", 47 | transport: "tcp", 48 | port: 22 49 | } 50 | ], 51 | if_monitor: MdnsLite.InetMonitor 52 | 53 | # Overrides for debugging and testing 54 | # 55 | # * udhcpc_handler: capture whatever happens with udhcpc 56 | # * resolvconf: don't update the real resolv.conf 57 | # * persistence_dir: use the current directory 58 | # * bin_ip: just fail if anything calls ip rather that run it 59 | config :vintage_net, 60 | udhcpc_handler: VintageNetTest.CapturingUdhcpcHandler, 61 | resolvconf: "/dev/null", 62 | persistence_dir: "./test_tmp/persistence", 63 | bin_ip: "false" 64 | 65 | if Mix.env() == :test do 66 | # Allow Responders to still be created, but skip starting gen_udp 67 | # so tests can pass 68 | config :mdns_lite, 69 | skip_udp: true 70 | end 71 | -------------------------------------------------------------------------------- /lib/mdns_lite.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Peter C. Marks 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2021 Mat Trudel 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule MdnsLite do 8 | @moduledoc """ 9 | MdnsLite is a simple, limited, no frills mDNS implementation 10 | 11 | Advertising hostnames and services is generally done using the application 12 | config. See `MdnsLite.Options` for documentation. 13 | 14 | To change the advertised hostnames or services at runtime, see `set_host/1`, 15 | `add_mdns_service/1` and `remove_mdns_service/1`. 16 | 17 | MdnsLite's mDNS record tables and caches can be inspected using 18 | `MdnsLite.Info` if you're having trouble. 19 | 20 | Finally, check out the MdnsLite `README.md` for more information. 21 | """ 22 | 23 | import MdnsLite.DNS 24 | 25 | alias MdnsLite.DNS 26 | alias MdnsLite.Options 27 | alias MdnsLite.TableServer 28 | 29 | require Logger 30 | 31 | @typedoc """ 32 | A user-specified ID for referring to a service 33 | 34 | Atoms are recommended, but binaries are still supported since they were used 35 | in the past. 36 | """ 37 | @type service_id() :: atom() | binary() 38 | 39 | @typedoc """ 40 | A user-visible name for a service advertisement 41 | """ 42 | @type instance_name() :: String.t() | :unspecified 43 | 44 | @typedoc """ 45 | mDNS service description 46 | 47 | Keys include: 48 | 49 | * `:id` - an atom for referring to this service (only required if you want to 50 | reference the service at runtime) 51 | * `:port` - the TCP/UDP port number for the service (required) 52 | * `:transport` - the transport protocol. E.g., `"tcp"` (specify this and 53 | `:protocol`, or `:type`) * `:protocol` - the application protocol. E.g., 54 | `"ssh"` (specify this and `:transport`, or `:type`) 55 | * `:type` - the transport/protocol to advertize. E.g., `"_ssh._tcp"` (only 56 | needed if `:protocol` and `:transport` aren't specified) 57 | * `:weight` - the service weight. Defaults to `0`. (optional) 58 | * `:priority` - the service priority. Defaults to `0`. (optional) 59 | * `:txt_payload` - a list of strings to advertise 60 | 61 | Example: 62 | 63 | ``` 64 | %{id: :my_ssh, port: 22, protocol: "ssh", transport: "tcp"} 65 | ``` 66 | """ 67 | @type service() :: %{ 68 | :id => service_id(), 69 | :instance_name => instance_name(), 70 | :port => 1..65535, 71 | optional(:txt_payload) => [String.t()], 72 | optional(:priority) => 0..255, 73 | optional(:protocol) => String.t(), 74 | optional(:transport) => String.t(), 75 | optional(:type) => String.t(), 76 | optional(:weight) => 0..255 77 | } 78 | 79 | @local_if_info %MdnsLite.IfInfo{ipv4_address: {127, 0, 0, 1}} 80 | @default_timeout 500 81 | 82 | @doc """ 83 | Set the list of host names 84 | 85 | This replaces the list of hostnames that MdnsLite will respond to. The first 86 | hostname in the list is special. Service advertisements will use it. The 87 | remainder are aliases. 88 | 89 | Hostnames should not have the ".local" extension. MdnsLite will add it. 90 | 91 | To specify the hostname returned by `:inet.gethostname/0`, use `:hostname`. 92 | 93 | To make MdnsLite respond to queries for ".local" and 94 | "nerves.local", run this: 95 | 96 | ```elixir 97 | iex> MdnsLite.set_hosts([:hostname, "nerves"]) 98 | :ok 99 | ``` 100 | """ 101 | @spec set_hosts([:hostname | String.t()]) :: :ok 102 | def set_hosts(hosts) do 103 | TableServer.update_options(&Options.set_hosts(&1, hosts)) 104 | end 105 | 106 | @doc """ 107 | Updates the advertised instance name for service records 108 | 109 | To specify the first hostname specified in `hosts`, use `:unspecified` 110 | """ 111 | @spec set_instance_name(instance_name()) :: :ok 112 | def set_instance_name(instance_name) do 113 | TableServer.update_options(&Options.set_instance_name(&1, instance_name)) 114 | end 115 | 116 | @doc """ 117 | Start advertising a service 118 | 119 | Services can be added at compile-time via the `:services` key in the `mdns_lite` 120 | application environment or they can be added at runtime using this function. 121 | See the `service` type for information on what's needed. 122 | 123 | Example: 124 | 125 | ```elixir 126 | iex> service = %{ 127 | id: :my_web_server, 128 | protocol: "http", 129 | transport: "tcp", 130 | port: 80 131 | } 132 | iex> MdnsLite.add_mdns_service(service) 133 | :ok 134 | ``` 135 | """ 136 | @spec add_mdns_service(service()) :: :ok 137 | def add_mdns_service(service) do 138 | TableServer.update_options(&Options.add_service(&1, service)) 139 | end 140 | 141 | @doc """ 142 | Stop advertising a service 143 | 144 | Example: 145 | 146 | ```elixir 147 | iex> MdnsLite.remove_mdns_service(:my_ssh) 148 | :ok 149 | ``` 150 | """ 151 | @spec remove_mdns_service(service_id()) :: :ok 152 | def remove_mdns_service(id) do 153 | TableServer.update_options(&Options.remove_service_by_id(&1, id)) 154 | end 155 | 156 | @doc """ 157 | Lookup a hostname using mDNS 158 | 159 | The hostname should be a .local name since the query only goes out via mDNS. 160 | On success, an IP address is returned. 161 | """ 162 | @spec gethostbyname(String.t(), non_neg_integer()) :: 163 | {:ok, :inet.ip_address()} | {:error, any()} 164 | def gethostbyname(hostname, timeout \\ @default_timeout) do 165 | q = dns_query(class: :in, type: :a, domain: to_charlist(hostname)) 166 | 167 | case query(q, timeout) do 168 | %{answer: [first | _]} -> 169 | ip = first |> dns_rr(:data) |> to_addr() 170 | {:ok, ip} 171 | 172 | %{answer: []} -> 173 | {:error, :nxdomain} 174 | end 175 | end 176 | 177 | defp to_addr(addr) when is_tuple(addr), do: addr 178 | defp to_addr(<>), do: {a, b, c, d} 179 | 180 | defp to_addr(<>), 181 | do: {a, b, c, d, e, f, g, h} 182 | 183 | @doc false 184 | @spec query(DNS.dns_query(), non_neg_integer()) :: %{ 185 | answer: [DNS.dns_rr()], 186 | additional: [DNS.dns_rr()] 187 | } 188 | def query(dns_query() = q, timeout \\ @default_timeout) do 189 | # 1. Try our configured records 190 | # 2. Try the caches 191 | # 3. Send the query 192 | # 4. Wait for response to collect and return the matchers 193 | with %{answer: []} <- MdnsLite.TableServer.query(q, @local_if_info), 194 | %{answer: []} <- MdnsLite.Responder.query_all_caches(q) do 195 | MdnsLite.Responder.multicast_all(q) 196 | Process.sleep(timeout) 197 | MdnsLite.Responder.query_all_caches(q) 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/mdns_lite/application.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Application do 6 | @moduledoc false 7 | 8 | use Application 9 | 10 | @impl Application 11 | def start(_type, _args) do 12 | config = Application.get_all_env(:mdns_lite) |> MdnsLite.Options.new() 13 | 14 | children = [ 15 | {MdnsLite.TableServer, config}, 16 | {Registry, keys: :unique, name: MdnsLite.ResponderRegistry}, 17 | {Registry, keys: :duplicate, name: MdnsLite.Responders}, 18 | {MdnsLite.ResponderSupervisor, []}, 19 | {MdnsLite.DNSBridge, config}, 20 | {config.if_monitor, excluded_ifnames: config.excluded_ifnames, ipv4_only: config.ipv4_only} 21 | ] 22 | 23 | opts = [strategy: :rest_for_one, name: MdnsLite.Supervisor] 24 | Supervisor.start_link(children, opts) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mdns_lite/cache.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Cache do 6 | @moduledoc """ 7 | Cache for records received over mDNS 8 | """ 9 | import MdnsLite.DNS 10 | alias MdnsLite.DNS 11 | 12 | # NOTE: This implementation is not efficient at all. This shouldn't 13 | # matter that much since it's not expected that there will be many 14 | # records and it won't be called that often. 15 | 16 | @typedoc "Timestamp in seconds (assumed monotonic)" 17 | @type timestamp() :: integer() 18 | 19 | # Don't allow records to last more than 75 minutes 20 | @max_ttl 75 * 60 21 | 22 | # Only cache a subset of record types 23 | @cache_types [:a, :aaaa, :ptr, :txt, :srv] 24 | 25 | # Restrict how many records are cached 26 | @max_records 200 27 | 28 | defstruct last_gc: -2_147_483_648, records: [] 29 | @type t() :: %__MODULE__{last_gc: timestamp(), records: [DNS.dns_rr()]} 30 | 31 | @doc """ 32 | Start an empty cache 33 | """ 34 | @spec new() :: %__MODULE__{last_gc: -2_147_483_648, records: []} 35 | def new() do 36 | %__MODULE__{} 37 | end 38 | 39 | @doc """ 40 | Run a query against the cache 41 | 42 | IMPORTANT: The cache is not garbage collected, so it can return stale entries. 43 | Call `gc/2` first to expire old entries. 44 | """ 45 | @spec query(t(), DNS.dns_query()) :: %{answer: [DNS.dns_rr()], additional: [DNS.dns_rr()]} 46 | def query(cache, query) do 47 | answer = MdnsLite.Table.query(cache.records, query, %MdnsLite.IfInfo{}) 48 | additional = MdnsLite.Table.additional_records(cache.records, answer, %MdnsLite.IfInfo{}) 49 | %{answer: answer, additional: additional} 50 | end 51 | 52 | @doc """ 53 | Remove any expired entries 54 | """ 55 | @spec gc(t(), timestamp()) :: t() 56 | def gc(%{last_gc: last_time} = cache, time) when time > last_time do 57 | seconds_elapsed = time - last_time 58 | new_records = Enum.reduce(cache.records, [], &gc_record(&1, &2, seconds_elapsed)) 59 | %{cache | records: new_records, last_gc: time} 60 | end 61 | 62 | def gc(cache, _time) do 63 | cache 64 | end 65 | 66 | defp gc_record(record, acc, seconds_elapsed) do 67 | new_ttl = dns_rr(record, :ttl) - seconds_elapsed 68 | 69 | if new_ttl > 0 do 70 | [dns_rr(record, ttl: new_ttl) | acc] 71 | else 72 | acc 73 | end 74 | end 75 | 76 | @doc """ 77 | Insert a record into the cache 78 | """ 79 | @spec insert(t(), timestamp(), DNS.dns_rr()) :: t() 80 | def insert(cache, time, record) do 81 | insert_many(cache, time, [record]) 82 | end 83 | 84 | @doc """ 85 | Insert several record into the cache 86 | """ 87 | @spec insert_many(t(), timestamp(), [DNS.dns_rr()]) :: t() 88 | def insert_many(cache, time, records) do 89 | records = records |> Enum.filter(&valid_record?/1) |> Enum.map(&normalize_record/1) 90 | 91 | if records != [] do 92 | cache 93 | |> gc(time) 94 | |> drop_if_full(Enum.count(records)) 95 | |> do_insert_many(records) 96 | else 97 | cache 98 | end 99 | end 100 | 101 | defp do_insert_many(cache, records) do 102 | Enum.reduce(records, cache, &do_insert(&2, &1)) 103 | end 104 | 105 | defp do_insert(cache, record) do 106 | %{cache | records: insert_or_update(cache.records, record, [])} 107 | end 108 | 109 | defp insert_or_update([], new_rr, result) do 110 | [new_rr | result] 111 | end 112 | 113 | defp insert_or_update( 114 | [dns_rr(class: c, type: t, domain: d, data: x) | rest], 115 | dns_rr(class: c, type: t, domain: d, data: x) = new_rr, 116 | result 117 | ) do 118 | [new_rr | rest] ++ result 119 | end 120 | 121 | defp insert_or_update([rr | rest], new_rr, result) do 122 | insert_or_update(rest, new_rr, [rr | result]) 123 | end 124 | 125 | defp normalize_record(dns_rr(ttl: ttl) = record) do 126 | dns_rr(record, ttl: normalize_ttl(ttl)) 127 | end 128 | 129 | defp normalize_ttl(ttl) when ttl > @max_ttl, do: @max_ttl 130 | defp normalize_ttl(ttl) when ttl < 1, do: 1 131 | defp normalize_ttl(ttl), do: ttl 132 | 133 | defp valid_record?(dns_rr(type: t)) when t in @cache_types, do: true 134 | defp valid_record?(_other), do: false 135 | 136 | defp drop_if_full(cache, count) do 137 | %{cache | records: Enum.take(cache.records, @max_records - count)} 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/mdns_lite/client.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Client do 6 | @moduledoc false 7 | 8 | import MdnsLite.DNS 9 | alias MdnsLite.DNS 10 | 11 | @doc """ 12 | Helper for creating an A-record query 13 | """ 14 | @spec query_a(String.t()) :: DNS.dns_query() 15 | def query_a(hostname) do 16 | dns_query(class: :in, type: :a, domain: to_charlist(hostname)) 17 | end 18 | 19 | @doc """ 20 | Helper for creating a AAAA-record query 21 | """ 22 | @spec query_aaaa(String.t()) :: DNS.dns_query() 23 | def query_aaaa(hostname) do 24 | dns_query(class: :in, type: :aaaa, domain: to_charlist(hostname)) 25 | end 26 | 27 | @spec encode(DNS.dns_query(), unicast: boolean()) :: binary() 28 | def encode(dns_query() = query, options \\ []) do 29 | dns_rec( 30 | # RFC6762: Query ID SHOULD be set to zero 31 | header: dns_header(id: 0, qr: false, aa: false, rcode: 0), 32 | # Query list. Must be empty according to RFC 6762 Section 6. 33 | qdlist: [request_unicast(query, options[:unicast])], 34 | # A list of answer entries. Can be empty. 35 | anlist: [], 36 | # nslist Can be empty. 37 | nslist: [], 38 | # arlist A list of resource entries. Can be empty. 39 | arlist: [] 40 | ) 41 | |> DNS.encode() 42 | end 43 | 44 | defp request_unicast(query, value) do 45 | dns_query(query, unicast_response: !!value) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/mdns_lite/core_monitor.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2024 Kevin Schweikert 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.CoreMonitor do 7 | @moduledoc """ 8 | Core logic for network monitors 9 | 10 | This module contains most of the logic needed for writing a network monitor. 11 | It's only intended to be called from `MdnsLite.InetMonitor` and 12 | `MdnsLite.VintageNetMonitor`. 13 | """ 14 | 15 | @typedoc """ 16 | Monitor options 17 | 18 | * `:excluded_ifnames` - a list of network interface names to ignore 19 | * `:ipv4_only` - set to `true` to ignore all IPv6 addresses 20 | """ 21 | @type option() :: 22 | {:excluded_ifnames, [String.t()]} 23 | | {:ipv4_only, boolean()} 24 | 25 | @typedoc false 26 | @type state() :: %{ 27 | excluded_ifnames: [String.t()], 28 | ip_list: %{String.t() => [:inet.ip_address()]}, 29 | filter: ([:inet.ip_address()] -> [:inet.ip_address()]), 30 | todo: [mfa()] 31 | } 32 | 33 | @spec init([option()]) :: state() 34 | def init(opts) do 35 | excluded_ifnames = Keyword.get(opts, :excluded_ifnames, []) 36 | filter = if Keyword.get(opts, :ipv4_only, true), do: &filter_by_ipv4/1, else: &no_filter/1 37 | 38 | %{ 39 | excluded_ifnames: excluded_ifnames, 40 | ip_list: %{}, 41 | filter: filter, 42 | todo: [] 43 | } 44 | end 45 | 46 | @spec set_ip_list(state(), String.t(), [:inet.ip_address()]) :: state() 47 | def set_ip_list(%{} = state, ifname, ip_list) do 48 | if ifname in state.excluded_ifnames do 49 | # Ignore excluded interface 50 | state 51 | else 52 | current_list = Map.get(state.ip_list, ifname, []) 53 | new_list = state.filter.(ip_list) 54 | 55 | {to_remove, to_add} = compute_delta(current_list, new_list) 56 | 57 | new_todo = 58 | state.todo ++ 59 | Enum.map(to_remove, &{MdnsLite.ResponderSupervisor, :stop_child, [ifname, &1]}) ++ 60 | Enum.map(to_add, &{MdnsLite.ResponderSupervisor, :start_child, [ifname, &1]}) 61 | 62 | %{state | todo: new_todo, ip_list: Map.put(state.ip_list, ifname, new_list)} 63 | end 64 | end 65 | 66 | @spec flush_todo_list(state()) :: state() 67 | def flush_todo_list(state) do 68 | Enum.each(state.todo, fn {m, f, a} -> apply(m, f, a) end) 69 | 70 | %{state | todo: []} 71 | end 72 | 73 | @spec unset_remaining_ifnames(state(), [String.t()]) :: state() 74 | def unset_remaining_ifnames(state, new_ifnames) do 75 | Enum.reduce(known_ifnames(state), state, fn ifname, state -> 76 | if ifname in new_ifnames do 77 | state 78 | else 79 | set_ip_list(state, ifname, []) 80 | end 81 | end) 82 | end 83 | 84 | defp known_ifnames(state) do 85 | Map.keys(state.ip_list) 86 | end 87 | 88 | defp compute_delta(old_list, new_list) do 89 | {old_list -- new_list, new_list -- old_list} 90 | end 91 | 92 | defp no_filter(ip_list) do 93 | ip_list 94 | end 95 | 96 | defp filter_by_ipv4(ip_list) do 97 | Enum.filter(ip_list, &(MdnsLite.Utilities.ip_family(&1) == :inet)) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/mdns_lite/dns.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.DNS do 6 | @moduledoc """ 7 | Bring Erlang's DNS record definitions into Elixir 8 | """ 9 | import Record, only: [defrecord: 2] 10 | 11 | @inet_dns "src/mdns_lite_inet_dns.hrl" 12 | 13 | defrecord :dns_rec, Record.extract(:dns_rec, from: @inet_dns) 14 | defrecord :dns_header, Record.extract(:dns_header, from: @inet_dns) 15 | defrecord :dns_query, Record.extract(:dns_query, from: @inet_dns) 16 | defrecord :dns_rr, Record.extract(:dns_rr, from: @inet_dns) 17 | 18 | @type dns_query :: record(:dns_query, []) 19 | @type dns_rr :: record(:dns_rr, []) 20 | @type dns_rec :: record(:dns_rec, []) 21 | 22 | @doc """ 23 | Encode a DNS record 24 | """ 25 | @spec encode(dns_rec()) :: binary() 26 | def encode(rec) do 27 | # Use the new version of :inet_dns that supports RFC 6762 28 | :mdns_lite_inet_dns.encode(rec) 29 | end 30 | 31 | @doc """ 32 | Decode a packet that contains a DNS message 33 | """ 34 | @spec decode(binary()) :: {:ok, dns_rec()} | {:error, any()} 35 | def decode(packet) do 36 | :mdns_lite_inet_dns.decode(packet) 37 | end 38 | 39 | @doc """ 40 | Format a DNS record as a nice string for the user 41 | """ 42 | @spec pretty(dns_rr()) :: String.t() 43 | def pretty(dns_rr(domain: domain, type: :a, class: :in, ttl: ttl, data: data)) do 44 | "#{domain}: type A, class IN, ttl #{ttl}, addr #{ntoa(data)}" 45 | end 46 | 47 | def pretty(dns_rr(domain: domain, type: :aaaa, class: :in, ttl: ttl, data: data)) do 48 | "#{domain}: type AAAA, class IN, ttl #{ttl}, addr #{ntoa(data)}" 49 | end 50 | 51 | def pretty(dns_rr(domain: domain, type: :ptr, class: :in, ttl: ttl, data: data)) do 52 | "#{ptr_domain(domain)}: type PTR, class IN, ttl #{ttl}, #{data}" 53 | end 54 | 55 | def pretty(dns_rr(domain: domain, type: :txt, class: :in, ttl: ttl, data: data)) do 56 | formatted_data = if data == [], do: "", else: ", #{Enum.join(data, ", ")}" 57 | 58 | "#{domain}: type TXT, class IN, ttl #{ttl}" <> formatted_data 59 | end 60 | 61 | def pretty( 62 | dns_rr( 63 | domain: domain, 64 | type: :srv, 65 | class: :in, 66 | ttl: ttl, 67 | data: {priority, weight, port, target} 68 | ) 69 | ) do 70 | "#{domain}: type SRV, class IN, ttl #{ttl}, priority #{priority}, weight #{weight}, port #{port}, #{target}" 71 | end 72 | 73 | def pretty(dns_rr(domain: domain, type: type, class: class, ttl: ttl)) do 74 | "#{domain}: type #{type}, class #{class}, ttl #{ttl}" 75 | end 76 | 77 | defp ntoa(:ipv4_address), do: "" 78 | defp ntoa(:ipv6_address), do: "" 79 | defp ntoa(addr) when is_tuple(addr), do: :inet.ntoa(addr) 80 | defp ntoa(<>), do: :inet.ntoa({a, b, c, d}) 81 | 82 | defp ntoa(<>), 83 | do: :inet.ntoa({a, b, c, d, e, f, g, h}) 84 | 85 | defp ptr_domain(:ipv4_arpa_address), do: ".in-addr.arpa" 86 | defp ptr_domain(:ipv6_arpa_address), do: ".ip6.arpa" 87 | defp ptr_domain(other), do: other 88 | end 89 | -------------------------------------------------------------------------------- /lib/mdns_lite/dns_bridge.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.DNSBridge do 6 | @moduledoc """ 7 | DNS server that responds to mDNS queries 8 | 9 | This is a simple DNS server that can be used to resolve mDNS queries 10 | so that the rest of Erlang and Elixir can seamlessly use mDNS. To use 11 | this, you must enable the `:dns_bridge_enabled` option and then make the 12 | first DNS server be this server's IP address and port. 13 | 14 | This DNS server can either return an error or recursively look up a non-mDNS record 15 | depending on how it's configured. Erlang's DNS resolver currently has an issue 16 | with the error strategy so it can't be used. 17 | 18 | Configure this using the following application environment options: 19 | 20 | * `:dns_bridge_enabled` - set to true to enable the bridge 21 | * `:dns_bridge_ip` - IP address in tuple form for server (defaults to `{127, 0, 0, 53}`) 22 | * `:dns_bridge_port` - UDP port for server (defaults to 53) 23 | * `:dns_bridge_recursive` - set to true to recursively look up non-mDNS queries 24 | """ 25 | 26 | use GenServer 27 | 28 | import MdnsLite.DNS 29 | alias MdnsLite.DNS 30 | alias MdnsLite.Options 31 | require Logger 32 | 33 | @doc false 34 | @spec start_link(MdnsLite.Options.t()) :: GenServer.on_start() 35 | def start_link(%Options{} = init_args) do 36 | GenServer.start_link(__MODULE__, init_args, name: __MODULE__) 37 | end 38 | 39 | ############################################################################## 40 | # GenServer callbacks 41 | ############################################################################## 42 | @impl GenServer 43 | def init(opts) do 44 | if opts.dns_bridge_enabled do 45 | {:ok, udp} = :gen_udp.open(opts.dns_bridge_port, udp_options(opts)) 46 | 47 | {:ok, 48 | %{ 49 | udp: udp, 50 | recursive: opts.dns_bridge_recursive, 51 | our_ip_port: {opts.dns_bridge_ip, opts.dns_bridge_port} 52 | }} 53 | else 54 | :ignore 55 | end 56 | end 57 | 58 | @impl GenServer 59 | def handle_info({:udp, _socket, src_ip, src_port, packet}, state) do 60 | # Decode the UDP packet 61 | with {:ok, dns_record} <- DNS.decode(packet), 62 | dns_rec(header: header, qdlist: qdlist) = dns_record, 63 | # qr is the query/response flag; false (0) = query, true (1) = response 64 | dns_header(qr: false) <- header do 65 | # only respond to the first query 66 | 67 | result = MdnsLite.query(hd(qdlist)) 68 | 69 | send_response(qdlist, result, dns_record, {src_ip, src_port}, state) 70 | else 71 | _ -> 72 | # Silently drop any unexpected packets 73 | :ok 74 | end 75 | 76 | {:noreply, state} 77 | end 78 | 79 | @impl GenServer 80 | def handle_info(_msg, state) do 81 | {:noreply, state} 82 | end 83 | 84 | ############################################################################## 85 | # Private functions 86 | ############################################################################## 87 | defp send_response( 88 | qdlist, 89 | %{answer: []}, 90 | dns_rec(header: dns_header(id: id)), 91 | {dest_address, dest_port}, 92 | state 93 | ) do 94 | result = 95 | if state.recursive do 96 | try_recursive_lookup(id, qdlist, state.our_ip_port) 97 | else 98 | lookup_failure(id, qdlist) 99 | end 100 | 101 | packet = DNS.encode(result) 102 | _ = :gen_udp.send(state.udp, dest_address, dest_port, packet) 103 | 104 | :ok 105 | end 106 | 107 | defp send_response( 108 | qdlist, 109 | result, 110 | dns_rec(header: dns_header(id: id, opcode: opcode, rd: rd)), 111 | {dest_address, dest_port}, 112 | state 113 | ) do 114 | packet = 115 | dns_rec( 116 | header: dns_header(id: id, qr: true, opcode: opcode, aa: true, rd: rd, rcode: 0), 117 | # Query list. Must be empty according to RFC 6762 Section 6. 118 | qdlist: qdlist, 119 | # A list of answer entries. Can be empty. 120 | anlist: result.answer, 121 | # nslist Can be empty. 122 | nslist: [], 123 | # arlist A list of resource entries. Can be empty. 124 | arlist: result.additional 125 | ) 126 | 127 | dns_record = DNS.encode(packet) 128 | # Best effort send 129 | _ = :gen_udp.send(state.udp, dest_address, dest_port, dns_record) 130 | :ok 131 | end 132 | 133 | defp udp_options(opts) do 134 | [ 135 | :binary, 136 | active: true, 137 | ip: opts.dns_bridge_ip, 138 | reuseaddr: true 139 | ] 140 | end 141 | 142 | defp try_recursive_lookup(id, qdlist, our_ip_port) do 143 | dns_query(domain: domain, class: class, type: type) = hd(qdlist) 144 | 145 | case :inet_res.resolve(domain, class, type, nameservers: nameservers(our_ip_port)) do 146 | {:ok, result} -> 147 | header = dns_rec(result, :header) 148 | 149 | dns_rec(result, header: dns_header(header, id: id)) 150 | 151 | {:error, _reason} -> 152 | lookup_failure(id, qdlist) 153 | end 154 | end 155 | 156 | defp nameservers(our_ip_port) do 157 | :inet_db.res_option(:nameservers) 158 | |> List.delete(our_ip_port) 159 | end 160 | 161 | defp lookup_failure(id, qdlist) do 162 | dns_rec( 163 | header: dns_header(id: id, qr: 1, aa: 0, tc: 0, rd: true, ra: 0, pr: 0, rcode: 5), 164 | qdlist: qdlist 165 | ) 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/mdns_lite/if_info.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.IfInfo do 6 | @moduledoc false 7 | 8 | # TODO: Delete module 9 | defstruct ipv4_address: nil, ipv6_addresses: [] 10 | 11 | @type t() :: %__MODULE__{ 12 | ipv4_address: :inet.ip4_address() | nil, 13 | ipv6_addresses: [:inet.ip6_address()] 14 | } 15 | end 16 | -------------------------------------------------------------------------------- /lib/mdns_lite/inet_monitor.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2024 Kevin Schweikert 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.InetMonitor do 7 | @moduledoc """ 8 | Network monitor that uses Erlang's :inet functions 9 | 10 | Use this network monitor to detect new network interfaces and their 11 | IP addresses when not using Nerves. It regularly polls the system 12 | for changes so it's not as fast at starting mDNS responders as 13 | the `MdnsLite.VintageNetMonitor` is. However, it works everywhere. 14 | 15 | See `MdnsLite.Options` for how to set your `config.exs` to use it. 16 | """ 17 | 18 | use GenServer 19 | 20 | alias MdnsLite.CoreMonitor 21 | require Logger 22 | 23 | @scan_interval 10000 24 | 25 | # Watch :inet.getifaddrs/0 for IP address changes and update the active responders. 26 | 27 | @doc false 28 | @spec start_link([CoreMonitor.option()]) :: GenServer.on_start() 29 | def start_link(init_args) do 30 | GenServer.start_link(__MODULE__, init_args, name: __MODULE__) 31 | end 32 | 33 | @impl GenServer 34 | def init(args) do 35 | {:ok, CoreMonitor.init(args), 1} 36 | end 37 | 38 | @impl GenServer 39 | def handle_info(:timeout, state) do 40 | {:noreply, update(state), @scan_interval} 41 | end 42 | 43 | defp update(state) do 44 | ifname_addresses = get_all_ip_addrs() 45 | ifnames = only_ifnames(ifname_addresses) 46 | 47 | ifname_addresses 48 | |> Enum.reduce(state, fn {ifname, ip_list}, state -> 49 | CoreMonitor.set_ip_list(state, ifname, ip_list) 50 | end) 51 | |> CoreMonitor.unset_remaining_ifnames(ifnames) 52 | |> CoreMonitor.flush_todo_list() 53 | end 54 | 55 | defp get_all_ip_addrs() do 56 | case :inet.getifaddrs() do 57 | {:ok, ifaddrs} -> 58 | Enum.map(ifaddrs, &ifaddr_to_ip_list/1) 59 | 60 | _error -> 61 | [] 62 | end 63 | end 64 | 65 | defp ifaddr_to_ip_list({ifname, info}) do 66 | addrs = for addr <- Keyword.get_values(info, :addr), do: addr 67 | {to_string(ifname), addrs} 68 | end 69 | 70 | defp only_ifnames(ifname_addresses) do 71 | Enum.map(ifname_addresses, &elem(&1, 0)) |> Enum.uniq() 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/mdns_lite/info.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Info do 6 | @moduledoc """ 7 | Inspect internal MdnsLite state 8 | 9 | Functions in this module are intended for debugging mDNS issues. 10 | """ 11 | 12 | alias MdnsLite.Responder 13 | alias MdnsLite.TableServer 14 | 15 | @doc """ 16 | Dump the records that mDNSLite advertises 17 | """ 18 | @spec dump_records() :: :ok 19 | def dump_records() do 20 | TableServer.get_records() 21 | |> format_rr([], "\n") 22 | |> IO.puts() 23 | end 24 | 25 | @doc """ 26 | Dump the contents of the responder mDNS caches 27 | """ 28 | @spec dump_caches() :: :ok 29 | def dump_caches() do 30 | Responder.get_all_caches() 31 | |> Enum.map(fn %{ifname: ifname, ip: ip, cache: cache} -> 32 | [ 33 | "Responder (", 34 | :inet.ntoa(ip), 35 | "%", 36 | ifname, 37 | "):\n", 38 | format_rr(cache.records, " ", "\n") 39 | ] 40 | end) 41 | |> IO.puts() 42 | end 43 | 44 | defp format_rr(rr, prefix, postfix) do 45 | rr 46 | |> Enum.sort() 47 | |> Enum.map(fn rec -> [prefix, MdnsLite.DNS.pretty(rec), postfix] end) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/mdns_lite/options.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2021 Mat Trudel 3 | # SPDX-FileCopyrightText: 2024 Connor Rigby 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule MdnsLite.Options do 8 | @moduledoc """ 9 | MdnsLite options 10 | 11 | MdnsLite is usually configured in a project's application environment 12 | (`config.exs`) as follows: 13 | 14 | ```elixir 15 | config :mdns_lite, 16 | hosts: [:hostname, "nerves"], 17 | ttl: 120, 18 | 19 | instance_name: "mDNS Lite Device", 20 | 21 | services: [ 22 | %{ 23 | id: :web_server, 24 | protocol: "http", 25 | transport: "tcp", 26 | port: 80, 27 | txt_payload: ["key=value"] 28 | }, 29 | %{ 30 | id: :ssh_daemon, 31 | instance_name: "More particular mDNS Lite Device" 32 | protocol: "ssh", 33 | transport: "tcp", 34 | port: 22 35 | } 36 | ] 37 | ``` 38 | 39 | The configurable keys are: 40 | 41 | * `:hosts` - A list of hostnames to respond to. Normally this would be set to 42 | `:hostname` and `mdns_lite` will advertise the actual hostname with `.local` 43 | appended. 44 | * `:ttl` - The default mDNS record time-to-live. The default of 120 45 | seconds is probably fine for most use. See [RFC 6762 - Multicast 46 | DNS](https://tools.ietf.org/html/rfc6762) for considerations. 47 | * `instance_name` - A user friendly name that will be used as the name for this 48 | device's advertised service(s). Per RFC6763 Appendix C, this should describe 49 | the user-facing purpose or description of the device, and should not be 50 | considered a unique identifier. For example, 'Nerves Device' and 'MatCo 51 | Laser Printer Model CRM-114' are good choices here. If instance_name is not 52 | defined it defaults to the first entry in the `hosts` list 53 | * `:excluded_ifnames` - A list of network interfaces names to ignore. By 54 | default, `mdns_lite` will ignore loopback and cellular network interfaces. 55 | * `:ipv4_only` - Set to `true` to only respond on IPv4 interfaces. Since IPv6 56 | isn't fully supported yet, this is the default. Note that it's still 57 | possible to get AAAA records when using IPv4. 58 | * `:if_monitor` - Set to `MdnsLite.VintageNetMonitor` when using Nerves or 59 | `MdnsLite.InetMonitor` elsewhere. The default is 60 | `MdnsLite.VintageNetMonitor`. 61 | * `:dns_bridge_enabled` - Set to `true` to start a DNS server running that 62 | will bridge DNS to mDNS. 63 | * `:dns_bridge_ip` - The IP address for the DNS server. Defaults to 64 | 127.0.0.53. 65 | * `:dns_bridge_port` - The UDP port for the DNS server. Defaults to 53. 66 | * `:dns_bridge_recursive` - If a regular DNS request comes on the DNS bridge, 67 | forward it to a DNS server rather than returning an error. This is the 68 | default since there's an issue on Linux and Nerves that prevents Erlang's 69 | DNS resolver from checking the next one. 70 | * `:services` - A list of services to advertise. See `MdnsLite.service` for 71 | details. 72 | 73 | Some options are modifiable at runtime. Functions for modifying these are in 74 | the `MdnsLite` module. 75 | """ 76 | 77 | require Logger 78 | 79 | @default_host_name_list [:hostname] 80 | @default_ttl 120 81 | @default_dns_ip {127, 0, 0, 53} 82 | @default_dns_port 53 83 | @default_excluded_ifnames ["lo0", "lo", "ppp0", "wwan0", "__unknown"] 84 | @default_ipv4_only true 85 | 86 | defstruct services: MapSet.new(), 87 | dot_local_names: [], 88 | hosts: [], 89 | ttl: @default_ttl, 90 | instance_name: :unspecified, 91 | dns_bridge_enabled: false, 92 | dns_bridge_ip: @default_dns_ip, 93 | dns_bridge_port: @default_dns_port, 94 | dns_bridge_recursive: false, 95 | if_monitor: nil, 96 | excluded_ifnames: @default_excluded_ifnames, 97 | ipv4_only: @default_ipv4_only 98 | 99 | @typedoc false 100 | @type t :: %__MODULE__{ 101 | services: MapSet.t(map()), 102 | dot_local_names: [String.t()], 103 | hosts: [String.t()], 104 | ttl: pos_integer(), 105 | instance_name: MdnsLite.instance_name(), 106 | dns_bridge_enabled: boolean(), 107 | dns_bridge_ip: :inet.ip_address(), 108 | dns_bridge_port: 1..65535, 109 | dns_bridge_recursive: boolean(), 110 | if_monitor: module(), 111 | excluded_ifnames: [String.t()], 112 | ipv4_only: boolean() 113 | } 114 | 115 | @doc false 116 | @spec new(Enumerable.t()) :: t() 117 | def new(enumerable \\ %{}) do 118 | opts = Map.new(enumerable) 119 | 120 | hosts = get_host_option(opts) 121 | ttl = Map.get(opts, :ttl, @default_ttl) 122 | instance_name = Map.get(opts, :instance_name, :unspecified) 123 | config_services = Map.get(opts, :services, []) |> filter_invalid_services() 124 | dns_bridge_enabled = Map.get(opts, :dns_bridge_enabled, false) 125 | dns_bridge_ip = Map.get(opts, :dns_bridge_ip, @default_dns_ip) 126 | dns_bridge_port = Map.get(opts, :dns_bridge_port, @default_dns_port) 127 | dns_bridge_recursive = Map.get(opts, :dns_bridge_recursive, false) 128 | if_monitor = Map.get(opts, :if_monitor, default_if_monitor()) 129 | ipv4_only = Map.get(opts, :ipv4_only, @default_ipv4_only) 130 | excluded_ifnames = Map.get(opts, :excluded_ifnames, @default_excluded_ifnames) 131 | 132 | %__MODULE__{ 133 | ttl: ttl, 134 | instance_name: instance_name, 135 | dns_bridge_enabled: dns_bridge_enabled, 136 | dns_bridge_ip: dns_bridge_ip, 137 | dns_bridge_port: dns_bridge_port, 138 | dns_bridge_recursive: dns_bridge_recursive, 139 | if_monitor: if_monitor, 140 | excluded_ifnames: excluded_ifnames, 141 | ipv4_only: ipv4_only 142 | } 143 | |> add_hosts(hosts) 144 | |> add_services(config_services) 145 | end 146 | 147 | defp default_if_monitor() do 148 | if has_vintage_net?() do 149 | MdnsLite.VintageNetMonitor 150 | else 151 | MdnsLite.InetMonitor 152 | end 153 | end 154 | 155 | defp has_vintage_net?() do 156 | Application.loaded_applications() 157 | |> Enum.find_value(fn {app, _, _} -> app == :vintage_net end) 158 | end 159 | 160 | # This used to be called :host, but now it's :hosts. It's a list, but be 161 | # nice and wrap rather than crash. 162 | defp get_host_option(%{host: host}) do 163 | Logger.warning("mdns_lite: the :host app environment option is deprecated. Change to :hosts") 164 | List.wrap(host) 165 | end 166 | 167 | defp get_host_option(%{hosts: hosts}), do: List.wrap(hosts) 168 | defp get_host_option(_), do: @default_host_name_list 169 | 170 | @doc false 171 | @spec set_instance_name(t(), MdnsLite.instance_name()) :: t() 172 | def set_instance_name(options, instance_name) do 173 | %{options | instance_name: instance_name} 174 | end 175 | 176 | @doc false 177 | @spec add_service(t(), MdnsLite.service()) :: t() 178 | def add_service(options, service) do 179 | {:ok, normalized_service} = normalize_service(service) 180 | %{options | services: MapSet.put(options.services, normalized_service)} 181 | end 182 | 183 | @doc false 184 | @spec add_services(t(), [MdnsLite.service()]) :: t() 185 | def add_services(%__MODULE__{} = options, services) do 186 | Enum.reduce(services, options, fn service, options -> add_service(options, service) end) 187 | end 188 | 189 | @doc false 190 | @spec filter_invalid_services([MdnsLite.service()]) :: [MdnsLite.service()] 191 | def filter_invalid_services(services) do 192 | Enum.flat_map(services, fn service -> 193 | case normalize_service(service) do 194 | {:ok, normalized_service} -> 195 | [normalized_service] 196 | 197 | {:error, reason} -> 198 | Logger.warning("mdns_lite: ignoring service (#{inspect(service)}): #{reason}") 199 | [] 200 | end 201 | end) 202 | end 203 | 204 | @doc """ 205 | Normalize a service description 206 | 207 | All service descriptions are normalized before use. Call this function if 208 | you're unsure how the service description will be transformed for use. 209 | """ 210 | @spec normalize_service(MdnsLite.service()) :: {:ok, MdnsLite.service()} | {:error, String.t()} 211 | def normalize_service(service) do 212 | with {:ok, id} <- normalize_id(service), 213 | {:ok, instance_name} <- normalize_instance_name(service), 214 | {:ok, port} <- normalize_port(service), 215 | {:ok, type} <- normalize_type(service) do 216 | {:ok, 217 | %{ 218 | id: id, 219 | instance_name: instance_name, 220 | port: port, 221 | type: type, 222 | txt_payload: Map.get(service, :txt_payload, []), 223 | priority: Map.get(service, :priority, 0), 224 | weight: Map.get(service, :weight, 0) 225 | }} 226 | end 227 | end 228 | 229 | defp normalize_id(%{id: id}), do: {:ok, id} 230 | 231 | defp normalize_id(%{name: name}) do 232 | Logger.warning("mdns_lite: names are deprecated now. Specify an :id that's an atom") 233 | {:ok, name} 234 | end 235 | 236 | defp normalize_id(_), do: {:ok, :unspecified} 237 | 238 | defp normalize_instance_name(%{instance_name: instance_name}), do: {:ok, instance_name} 239 | defp normalize_instance_name(_), do: {:ok, :unspecified} 240 | 241 | defp normalize_type(%{type: type}) when is_binary(type) and byte_size(type) > 0 do 242 | {:ok, type} 243 | end 244 | 245 | defp normalize_type(%{protocol: protocol, transport: transport} = service) 246 | when is_binary(protocol) and is_binary(transport) do 247 | {:ok, "_#{service.protocol}._#{service.transport}"} 248 | end 249 | 250 | defp normalize_type(_other) do 251 | {:error, "Specify either 1. :protocol and :transport or 2. :type"} 252 | end 253 | 254 | defp normalize_port(%{port: port}) when port >= 0 and port <= 65535, do: {:ok, port} 255 | defp normalize_port(_), do: {:error, "Specify a port"} 256 | 257 | @doc false 258 | @spec get_services(t()) :: [MdnsLite.service()] 259 | def get_services(%__MODULE__{} = options) do 260 | MapSet.to_list(options.services) 261 | end 262 | 263 | @doc false 264 | @spec remove_service_by_id(t(), MdnsLite.service_id()) :: t() 265 | def remove_service_by_id(%__MODULE__{} = options, service_id) do 266 | services_set = 267 | options.services 268 | |> Enum.reject(&(&1.id == service_id)) 269 | |> MapSet.new() 270 | 271 | %{options | services: services_set} 272 | end 273 | 274 | @doc false 275 | @spec set_hosts(t(), [String.t() | :hostname]) :: t() 276 | def set_hosts(%__MODULE__{} = options, hosts) do 277 | %{options | dot_local_names: [], hosts: []} 278 | |> add_hosts(hosts) 279 | end 280 | 281 | @doc false 282 | @spec add_host(t(), String.t() | :hostname) :: t() 283 | def add_host(%__MODULE__{} = options, host) do 284 | resolved_host = resolve_mdns_name(host) 285 | dot_local_name = "#{resolved_host}.local" 286 | 287 | %{ 288 | options 289 | | dot_local_names: options.dot_local_names ++ [dot_local_name], 290 | hosts: options.hosts ++ [resolved_host] 291 | } 292 | end 293 | 294 | @doc false 295 | @spec add_hosts(t(), [String.t() | :hostname]) :: t() 296 | def add_hosts(%__MODULE__{} = options, hosts) do 297 | Enum.reduce(hosts, options, &add_host(&2, &1)) 298 | end 299 | 300 | defp resolve_mdns_name(:hostname) do 301 | {:ok, hostname} = :inet.gethostname() 302 | to_string(hostname) 303 | end 304 | 305 | defp resolve_mdns_name(mdns_name) when is_binary(mdns_name), do: mdns_name 306 | 307 | defp resolve_mdns_name(_other) do 308 | raise RuntimeError, "Host must be :hostname or a string" 309 | end 310 | end 311 | -------------------------------------------------------------------------------- /lib/mdns_lite/responder.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2019 Jon Carstens 3 | # SPDX-FileCopyrightText: 2019 Peter C. Marks 4 | # SPDX-FileCopyrightText: 2020 Eduardo Cunha 5 | # SPDX-FileCopyrightText: 2021 Connor Rigby 6 | # SPDX-FileCopyrightText: 2021 Peter Madsen-mygdal 7 | # SPDX-FileCopyrightText: 2023 Ben Youngblood 8 | # SPDX-FileCopyrightText: 2024 Michael Neumann 9 | # 10 | # SPDX-License-Identifier: Apache-2.0 11 | # 12 | defmodule MdnsLite.Responder do 13 | @moduledoc false 14 | 15 | # A GenServer that is responsible for responding to a limited number of mDNS 16 | # requests (queries). A UDP port is opened on the mDNS reserved IP/port. Any 17 | # UDP packets will be caught by handle_info() but only a subset of them are 18 | # of interest. The module `MdnsLite.Query does the actual query parsing. 19 | # 20 | # This module is started and stopped dynamically by MdnsLite.ResponderSupervisor 21 | # 22 | # There is one of these servers for every network interface managed by 23 | # MdnsLite. 24 | 25 | use GenServer, restart: :transient 26 | 27 | import MdnsLite.DNS 28 | 29 | alias MdnsLite.Cache 30 | alias MdnsLite.DNS 31 | alias MdnsLite.IfInfo 32 | alias MdnsLite.TableServer 33 | alias MdnsLite.Utilities 34 | 35 | require Logger 36 | 37 | # Reserved IANA ip address and port for mDNS 38 | @mdns_ipv4 {224, 0, 0, 251} 39 | @mdns_ipv6 {0xFF02, 0, 0, 0, 0, 0, 0, 0xFB} 40 | @mdns_port 5353 41 | 42 | @type state() :: %{ 43 | ifname: String.t(), 44 | ip: :inet.ip_address(), 45 | cache: Cache.t(), 46 | udp: :socket.socket(), 47 | select_handle: :socket.select_handle(), 48 | skip_udp: boolean() 49 | } 50 | 51 | ############################################################################## 52 | # Public interface 53 | ############################################################################## 54 | @spec start_link({String.t(), :inet.ip_address()}) :: GenServer.on_start() 55 | def start_link(ifname_address) do 56 | GenServer.start_link(__MODULE__, ifname_address, name: via_name(ifname_address)) 57 | end 58 | 59 | defp via_name(ifname_address) do 60 | {:via, Registry, {MdnsLite.ResponderRegistry, ifname_address}} 61 | end 62 | 63 | @spec get_all_caches() :: [%{ifname: String.t(), ip: :inet.ip_address(), cache: Cache.t()}] 64 | def get_all_caches() do 65 | Registry.lookup(MdnsLite.Responders, __MODULE__) 66 | |> Enum.map(fn {pid, {ifname, ip_address}} -> 67 | %{ifname: ifname, ip: ip_address, cache: get_cache(pid)} 68 | end) 69 | end 70 | 71 | @spec get_cache(GenServer.server()) :: Cache.t() 72 | def get_cache(server) do 73 | GenServer.call(server, :get_cache) 74 | end 75 | 76 | @spec query_all_caches(DNS.dns_query()) :: %{answer: [DNS.dns_rr()], additional: [DNS.dns_rr()]} 77 | def query_all_caches(q) do 78 | Registry.lookup(MdnsLite.Responders, __MODULE__) 79 | |> Enum.reduce(%{answer: [], additional: []}, fn {pid, _}, acc -> 80 | MdnsLite.Table.merge_results(acc, query_cache(pid, q)) 81 | end) 82 | end 83 | 84 | @spec query_cache(GenServer.server(), DNS.dns_query()) :: %{ 85 | answer: [DNS.dns_rr()], 86 | additional: [DNS.dns_rr()] 87 | } 88 | def query_cache(server, q) do 89 | GenServer.call(server, {:query_cache, q}) 90 | end 91 | 92 | @spec multicast_all(DNS.dns_query()) :: :ok 93 | def multicast_all(q) do 94 | Registry.lookup(MdnsLite.Responders, __MODULE__) 95 | |> Enum.each(fn {pid, _} -> multicast(pid, q) end) 96 | end 97 | 98 | @spec multicast(GenServer.server(), DNS.dns_query()) :: :ok 99 | def multicast(server, q) do 100 | GenServer.cast(server, {:multicast, q}) 101 | end 102 | 103 | @doc """ 104 | Leave the mDNS group - close the UDP port. Stop this GenServer. 105 | """ 106 | @spec stop_server(String.t(), :inet.ip_address()) :: :ok 107 | def stop_server(ifname, address) do 108 | GenServer.stop(via_name({ifname, address})) 109 | catch 110 | :exit, {:noproc, _} -> 111 | # Ignore if the server already stopped. It already exited due to the 112 | # network going down. 113 | :ok 114 | end 115 | 116 | ############################################################################## 117 | # GenServer callbacks 118 | ############################################################################## 119 | @impl GenServer 120 | def init({ifname, address}) do 121 | # Join the mDNS multicast group 122 | state = %{ 123 | ifname: ifname, 124 | ip: address, 125 | family: Utilities.ip_family(address), 126 | cache: Cache.new(), 127 | udp: nil, 128 | select_handle: nil, 129 | skip_udp: Application.get_env(:mdns_lite, :skip_udp) 130 | } 131 | 132 | {:ok, _} = Registry.register(MdnsLite.Responders, __MODULE__, {ifname, address}) 133 | 134 | {:ok, state, {:continue, :initialization}} 135 | end 136 | 137 | @impl GenServer 138 | def handle_continue(:initialization, %{skip_udp: true} = state) do 139 | # Used only for testing. 140 | {:noreply, state} 141 | end 142 | 143 | def handle_continue(:initialization, %{family: family} = state) do 144 | Logger.info("mdns_lite #{state.ifname}/#{inspect(state.ip)}") 145 | 146 | option_level = 147 | case family do 148 | :inet -> :ip 149 | :inet6 -> :ipv6 150 | end 151 | 152 | with {:ok, udp} <- :socket.open(family, :dgram, :udp), 153 | :ok <- bindtodevice(udp, state.ifname), 154 | :ok <- :socket.setopt(udp, :socket, :reuseport, true), 155 | :ok <- :socket.setopt(udp, :socket, :reuseaddr, true), 156 | :ok <- :socket.setopt(udp, option_level, :multicast_loop, false), 157 | :ok <- set_multicast_ttl(udp, state), 158 | {:ok, interface} <- get_interface_opt(state), 159 | :ok <- :socket.setopt(udp, option_level, :multicast_if, interface), 160 | :ok <- :socket.bind(udp, %{family: family, port: @mdns_port}), 161 | :ok <- add_membership(udp, interface, family) do 162 | new_state = %{state | udp: udp} |> process_receives() 163 | {:noreply, new_state} 164 | else 165 | {:error, reason} -> 166 | Logger.error("mdns_lite #{state.ifname}/#{inspect(state.ip)} failed: #{inspect(reason)}") 167 | 168 | # Not being able to setup the socket is fatal since it means that the 169 | # interface went away or its IP address changed. 170 | {:stop, :normal, state} 171 | end 172 | end 173 | 174 | @impl GenServer 175 | def handle_call(:get_cache, _from, state) do 176 | new_state = gc_cache(state) 177 | {:reply, new_state.cache, new_state} 178 | end 179 | 180 | def handle_call({:query_cache, q}, _from, state) do 181 | new_state = gc_cache(state) 182 | {:reply, Cache.query(new_state.cache, q), new_state} 183 | end 184 | 185 | @impl GenServer 186 | def handle_cast({:multicast, q}, state) do 187 | message = dns_rec(header: dns_header(id: 0, qr: false, aa: false), qdlist: [q]) 188 | data = DNS.encode(message) 189 | dest = %{family: state.family, port: @mdns_port, addr: multicast_ip(state.family)} 190 | 191 | if state.udp do 192 | case :socket.sendto(state.udp, data, dest) do 193 | {:error, reason} -> 194 | Logger.warning("mdns_lite multicast send failed: #{inspect(reason)}") 195 | 196 | :ok -> 197 | :ok 198 | end 199 | end 200 | 201 | {:noreply, state} 202 | end 203 | 204 | @impl GenServer 205 | def handle_info( 206 | {:"$socket", udp, :select, select_handle}, 207 | %{udp: udp, select_handle: select_handle} = state 208 | ) do 209 | {:noreply, process_receives(state)} 210 | end 211 | 212 | def handle_info(msg, state) do 213 | Logger.error("mdns_lite responder ignoring #{inspect(msg)}, #{inspect(state)}") 214 | {:noreply, state} 215 | end 216 | 217 | ############################################################################## 218 | # Private functions 219 | ############################################################################## 220 | defp process_receives(state) do 221 | case :socket.recvfrom(state.udp, [], :nowait) do 222 | {:ok, {source, data}} -> 223 | state 224 | |> process_packet(source, data) 225 | |> process_receives() 226 | 227 | {:select, {:select_info, _tag, select_handle}} -> 228 | %{state | select_handle: select_handle} 229 | end 230 | end 231 | 232 | defp process_packet(state, source, data) do 233 | case DNS.decode(data) do 234 | {:ok, msg} -> process_dns(state, source, msg) 235 | _ -> state 236 | end 237 | end 238 | 239 | defp process_dns( 240 | state, 241 | source, 242 | dns_rec(header: dns_header(qr: false), qdlist: qdlist) = msg 243 | ) do 244 | # mDNS request message 245 | Enum.each(qdlist, &run_query(&1, msg, source, state)) 246 | 247 | # If the request had any entries, cache them 248 | update_cache(msg, state) 249 | end 250 | 251 | defp process_dns(state, _source, dns_rec(header: dns_header(qr: true)) = msg) do 252 | # A response message or update so cache whatever it contains 253 | update_cache(msg, state) 254 | end 255 | 256 | defp update_cache(dns_rec(anlist: anlist, arlist: arlist), state) do 257 | now = System.monotonic_time(:second) 258 | new_cache = state.cache |> Cache.insert_many(now, anlist) |> Cache.insert_many(now, arlist) 259 | %{state | cache: new_cache} 260 | end 261 | 262 | # TODO: Responding to queries over IPv6 is not supported yet 263 | defp run_query(_qd, _msg, _source, %{family: :inet6}), do: :ok 264 | 265 | defp run_query(dns_query(unicast_response: unicast) = qd, msg, source, state) do 266 | result = TableServer.query(qd, %IfInfo{ipv4_address: state.ip}) 267 | 268 | if unicast do 269 | send_response(result, msg, source, state) 270 | else 271 | send_response(result, msg, mdns_destination(source), state) 272 | end 273 | end 274 | 275 | defp send_response(%{answer: []}, _dns_record, _dest, _state), do: :ok 276 | 277 | defp send_response( 278 | result, 279 | dns_rec(header: dns_header(id: id)), 280 | dest, 281 | state 282 | ) do 283 | # Construct an mDNS response from the query plus answers (resource records) 284 | packet = response_packet(id, result) 285 | 286 | # Logger.debug("Sending DNS response to #{inspect(dest_address)}/#{inspect(dest_port)}") 287 | # Logger.debug("#{inspect(packet)}") 288 | 289 | data = DNS.encode(packet) 290 | _ = :socket.sendto(state.udp, data, dest) 291 | :ok 292 | end 293 | 294 | # A standard mDNS response packet 295 | defp response_packet(id, result), 296 | do: 297 | dns_rec( 298 | # AA (Authoritative Answer) bit MUST be true - RFC 6762 18.4 299 | header: dns_header(id: id, qr: true, aa: true), 300 | # Query list. Must be empty according to RFC 6762 Section 6. 301 | qdlist: [], 302 | # A list of answer entries. Can be empty. 303 | anlist: result.answer, 304 | # nslist Can be empty. 305 | nslist: [], 306 | # arlist A list of resource entries. Can be empty. 307 | arlist: result.additional 308 | ) 309 | 310 | defp mdns_destination(%{family: :inet, port: @mdns_port}), 311 | do: %{family: :inet, port: @mdns_port, addr: @mdns_ipv4} 312 | 313 | defp mdns_destination(%{family: :inet6, port: @mdns_port}), 314 | do: %{family: :inet6, port: @mdns_port, addr: @mdns_ipv6} 315 | 316 | defp mdns_destination(%{family: family} = source) when family in [:inet, :inet6] do 317 | # Legacy Unicast Response 318 | # See RFC 6762 6.7 319 | source 320 | end 321 | 322 | defp gc_cache(state) do 323 | %{state | cache: Cache.gc(state.cache, System.monotonic_time(:second))} 324 | end 325 | 326 | defp bindtodevice(socket, ifname) do 327 | case :os.type() do 328 | {:unix, :linux} -> 329 | :socket.setopt(socket, :socket, :bindtodevice, String.to_charlist(ifname)) 330 | 331 | {:unix, :darwin} -> 332 | # TODO! 333 | :ok 334 | 335 | {:unix, _} -> 336 | # TODO! 337 | :ok 338 | end 339 | end 340 | 341 | # No difference between Linux and macOS for IPv6 342 | defp add_membership(udp, interface, :inet) do 343 | :socket.setopt(udp, :ip, :add_membership, %{ 344 | multiaddr: multicast_ip(:inet), 345 | interface: interface 346 | }) 347 | end 348 | 349 | @ipv6_option_join_group 12 350 | defp add_membership(udp, interface, :inet6) do 351 | case :os.type() do 352 | {:unix, :linux} -> 353 | :socket.setopt(udp, :ipv6, :add_membership, %{ 354 | multiaddr: multicast_ip(:inet6), 355 | interface: interface 356 | }) 357 | 358 | {:unix, :darwin} -> 359 | addr_bin = 360 | for int <- Tuple.to_list(@mdns_ipv6), into: <<>> do 361 | <> 362 | end 363 | 364 | # This is a bit of a hack. See https://stackoverflow.com/a/38386150 365 | :socket.setopt_native( 366 | udp, 367 | {:ipv6, @ipv6_option_join_group}, 368 | addr_bin <> <> 369 | ) 370 | 371 | {:unix, _} -> 372 | # TODO! 373 | :ok 374 | end 375 | end 376 | 377 | # setopt uses the interface address for IPv4 and the interface index for IPv6 378 | defp get_interface_opt(%{family: :inet, ip: ip}), do: {:ok, ip} 379 | 380 | defp get_interface_opt(%{family: :inet6, ifname: ifname}) do 381 | ifname |> String.to_charlist() |> :net.if_name2index() 382 | end 383 | 384 | # IP TTL should be 255. See https://tools.ietf.org/html/rfc6762#section-11 385 | defp set_multicast_ttl(sock, %{family: :inet}), 386 | do: :socket.setopt(sock, :ip, :multicast_ttl, 255) 387 | 388 | defp set_multicast_ttl(sock, %{family: :inet6}), 389 | do: :socket.setopt(sock, :ipv6, :multicast_hops, 255) 390 | 391 | defp multicast_ip(:inet), do: @mdns_ipv4 392 | defp multicast_ip(:inet6), do: @mdns_ipv6 393 | end 394 | -------------------------------------------------------------------------------- /lib/mdns_lite/responder_supervisor.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.ResponderSupervisor do 6 | @moduledoc false 7 | use DynamicSupervisor 8 | 9 | alias MdnsLite.Responder 10 | 11 | @spec start_link(any) :: GenServer.on_start() 12 | def start_link(init_arg) do 13 | DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) 14 | end 15 | 16 | @spec start_child(String.t(), :inet.ip_address()) :: DynamicSupervisor.on_start_child() 17 | def start_child(ifname, address) do 18 | DynamicSupervisor.start_child(__MODULE__, {Responder, {ifname, address}}) 19 | end 20 | 21 | @spec stop_child(String.t(), :inet.ip_address()) :: :ok 22 | def stop_child(ifname, address) do 23 | Responder.stop_server(ifname, address) 24 | end 25 | 26 | @impl DynamicSupervisor 27 | def init(_init_arg) do 28 | DynamicSupervisor.init(strategy: :one_for_one) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/mdns_lite/table.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Table do 6 | @moduledoc false 7 | import MdnsLite.DNS 8 | 9 | alias MdnsLite.DNS 10 | alias MdnsLite.IfInfo 11 | 12 | @type t() :: [DNS.dns_rr()] 13 | 14 | # TODO: make this more consistent 15 | @type tmp_results() :: %{additional: [DNS.dns_rr()], answer: [DNS.dns_rr()]} 16 | 17 | # RFC6762 Section 6: Responding 18 | # 19 | # The determination of whether a given record answers a given question 20 | # is made using the standard DNS rules: the record name must match the 21 | # question name, the record rrtype must match the question qtype unless 22 | # the qtype is "ANY" (255) or the rrtype is "CNAME" (5), and the record 23 | # rrclass must match the question qclass unless the qclass is "ANY" 24 | # (255). 25 | @spec query(t(), DNS.dns_query(), IfInfo.t()) :: [DNS.dns_rr()] 26 | def query(table, query, %IfInfo{} = if_info) do 27 | query 28 | |> normalize_query(if_info) 29 | |> run_query(table) 30 | |> Enum.flat_map(&fixup_rr(&1, if_info)) 31 | end 32 | 33 | @doc """ 34 | Add additional records per RFC 6763 Section 12 35 | 36 | Note: The following text in the RFC indicates that this is optional, 37 | but it really seems based on PRs/issues that it is not. 38 | 39 | >>> 40 | Clients MUST be capable of functioning correctly with DNS servers 41 | (and Multicast DNS Responders) that fail to generate these additional 42 | records automatically, by issuing subsequent queries for any further 43 | record(s) they require. The additional-record generation rules in 44 | this section are RECOMMENDED for improving network efficiency, but 45 | are not required for correctness. 46 | >>> 47 | """ 48 | @spec additional_records(t(), [DNS.dns_rr()], IfInfo.t()) :: [DNS.dns_rr()] 49 | def additional_records(table, rr, %IfInfo{} = if_info) do 50 | rr 51 | |> Enum.reduce([], &add_additional_records(&1, &2, table, if_info)) 52 | |> Enum.uniq() 53 | end 54 | 55 | @spec merge_results(tmp_results(), tmp_results()) :: tmp_results() 56 | def merge_results(%{answer: answer1, additional: add1}, %{answer: answer2, additional: add2}) do 57 | # TODO: compare uniqueness based on the domain, type, class, and data only. 58 | %{answer: Enum.uniq(answer1 ++ answer2), additional: Enum.uniq(add1 ++ add2)} 59 | end 60 | 61 | # RFC 6763 12.3 No additional records for text records 62 | defp add_additional_records(dns_rr(type: :text), acc, _table, _if_info) do 63 | acc 64 | end 65 | 66 | # RFC 6763 12.2 All address records (type "A" and "AAAA") named in the SRV rdata 67 | defp add_additional_records( 68 | dns_rr(type: :srv, data: {_priority, _weight, _port, domain}), 69 | acc, 70 | table, 71 | if_info 72 | ) do 73 | # Remove the trailing dot at the end of the domain 74 | hostname = List.delete_at(domain, -1) 75 | 76 | acc ++ 77 | query(table, dns_query(class: :in, type: :a, domain: hostname), if_info) ++ 78 | query(table, dns_query(class: :in, type: :aaaa, domain: hostname), if_info) 79 | end 80 | 81 | # RFC 6763 12.1 82 | # The SRV record(s) named in the PTR rdata. 83 | # The TXT record(s) named in the PTR rdata. 84 | # All address records (type "A" and "AAAA") named in the SRV rdata. 85 | defp add_additional_records( 86 | dns_rr(type: :ptr, data: domain), 87 | acc, 88 | table, 89 | if_info 90 | ) do 91 | srv_records = query(table, dns_query(class: :in, type: :srv, domain: domain), if_info) 92 | txt_records = query(table, dns_query(class: :in, type: :txt, domain: domain), if_info) 93 | a_records = additional_records(table, srv_records, if_info) 94 | 95 | acc ++ srv_records ++ txt_records ++ a_records 96 | end 97 | 98 | # RFC 6763 12.4 No additional records for other types 99 | defp add_additional_records(_record, acc, _table, _if_info) do 100 | acc 101 | end 102 | 103 | defp run_query(dns_query(class: class, type: type, domain: domain), table) do 104 | Enum.filter(table, fn dns_rr(class: c, type: t, domain: d) -> 105 | c == class and t == type and d == domain 106 | end) 107 | end 108 | 109 | defp normalize_query(dns_query(class: :in, type: :ptr, domain: domain) = q, if_info) do 110 | case test_known_in_addr_arpa(domain, if_info) do 111 | {:ok, value} -> dns_query(q, domain: value) 112 | _ -> q 113 | end 114 | end 115 | 116 | defp normalize_query(query, _if_info) do 117 | query 118 | end 119 | 120 | # TODO: Fate sharing - send IPv6 records when sending IPv4 ones and vice versa 121 | defp fixup_rr(dns_rr(class: :in, type: :a, data: :ipv4_address) = rr, if_info) do 122 | [dns_rr(rr, data: if_info.ipv4_address)] 123 | end 124 | 125 | defp fixup_rr(dns_rr(class: :in, type: :aaaa, data: :ipv6_address) = rr, if_info) do 126 | for address <- if_info.ipv6_addresses do 127 | dns_rr(rr, data: address) 128 | end 129 | end 130 | 131 | defp fixup_rr(dns_rr(class: :in, type: :ptr, domain: :ipv4_arpa_address) = rr, if_info) do 132 | [dns_rr(rr, domain: ipv4_arpa_address(if_info))] 133 | end 134 | 135 | defp fixup_rr(rr, _if_info) do 136 | [rr] 137 | end 138 | 139 | defp parse_in_addr_arpa(name) do 140 | parts = name |> to_string() |> String.split(".") |> Enum.reverse() 141 | 142 | case parts do 143 | ["", "arpa", "in-addr" | ip_parts] -> 144 | ip_parts |> Enum.join(".") |> to_charlist() |> :inet.parse_ipv4_address() 145 | 146 | ["", "arpa", "ip6" | _ip_parts] -> 147 | # See https://datatracker.ietf.org/doc/html/rfc2874 148 | # E.g., 1.8.1.3.3.5.3.A.D.F.3.1.5.6.C.0.7.E.3.0.8.0.0.0.0.7.4.0.1.0.0.2.ip6.arpa 149 | {:error, :implement_ipv6} 150 | 151 | _ -> 152 | {:error, :not_in_addr_arpa} 153 | end 154 | end 155 | 156 | defp normalize_ip_address(address, %{ipv4_address: address}) do 157 | {:ok, :ipv4_arpa_address} 158 | end 159 | 160 | defp normalize_ip_address(address, %{ipv6_addresses: list}) do 161 | if address in list do 162 | {:ok, :ipv6_arpa_address} 163 | else 164 | {:error, :unknown_address} 165 | end 166 | end 167 | 168 | defp test_known_in_addr_arpa(name, if_info) do 169 | with {:ok, address} <- parse_in_addr_arpa(name) do 170 | normalize_ip_address(address, if_info) 171 | end 172 | end 173 | 174 | defp ipv4_arpa_address(if_info) do 175 | # Example ARPA address for IP 192.168.0.112 is 112.0.168.192.in-addr.arpa 176 | arpa_address = 177 | if_info.ipv4_address 178 | |> Tuple.to_list() 179 | |> Enum.reverse() 180 | |> Enum.join(".") 181 | 182 | to_charlist(arpa_address <> ".in-addr.arpa.") 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/mdns_lite/table/builder.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2021 Mat Trudel 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.Table.Builder do 7 | @moduledoc false 8 | 9 | import MdnsLite.DNS 10 | 11 | alias MdnsLite.Options 12 | 13 | @doc """ 14 | Create a table based on the user options 15 | """ 16 | @spec from_options(Options.t()) :: MdnsLite.Table.t() 17 | def from_options(%Options{} = config) do 18 | # TODO: This could be seriously simplified... 19 | [] 20 | |> add_a_records(config) 21 | |> add_ptr_records(config) 22 | |> add_records_for_services(config) 23 | |> add_ptr_records3(config) 24 | |> Enum.uniq() 25 | end 26 | 27 | defp add_a_records(records, config) do 28 | records ++ 29 | for dot_local_name <- config.dot_local_names do 30 | to_dns_rr(:in, :a, dot_local_name, config.ttl, :ipv4_address) 31 | end ++ 32 | for dot_local_name <- config.dot_local_names do 33 | to_dns_rr(:in, :aaaa, dot_local_name, config.ttl, :ipv6_address) 34 | end 35 | end 36 | 37 | defp add_ptr_records(records, %Options{} = config) do 38 | # services._dns-sd._udp.local. is a special name for 39 | # "Service Type Enumeration" which is supposed to find all service 40 | # types on the network. Let them know about ours. 41 | domain = "_services._dns-sd._udp.local" 42 | 43 | resources = 44 | Options.get_services(config) 45 | |> Enum.map(fn service -> 46 | to_dns_rr(:in, :ptr, domain, config.ttl, to_charlist(service.type <> ".local")) 47 | end) 48 | 49 | records ++ resources 50 | end 51 | 52 | defp add_records_for_services(records, config) do 53 | Options.get_services(config) 54 | |> Enum.group_by(fn service -> service.type <> ".local" end) 55 | |> Enum.reduce(records, &records_for_service_type(&1, &2, config)) 56 | end 57 | 58 | defp records_for_service_type({domain, services}, records, config) do 59 | value = Enum.flat_map(services, &service_resources(&1, domain, config)) 60 | value ++ records 61 | end 62 | 63 | defp service_resources(service, domain, config) do 64 | service_instance_name = 65 | case service.instance_name do 66 | :unspecified -> 67 | case config.instance_name do 68 | :unspecified -> 69 | name = hd(config.hosts) 70 | to_charlist("#{name}.#{service.type}.local") 71 | 72 | host_instance_name -> 73 | to_charlist("#{host_instance_name}.#{service.type}.local") 74 | end 75 | 76 | service_instance_name -> 77 | to_charlist("#{service_instance_name}.#{service.type}.local") 78 | end 79 | 80 | first_dot_local_name = hd(config.dot_local_names) 81 | target = first_dot_local_name <> "." 82 | srv_data = {service.priority, service.weight, service.port, to_charlist(target)} 83 | 84 | [ 85 | to_dns_rr(:in, :ptr, domain, config.ttl, service_instance_name), 86 | to_dns_rr( 87 | :in, 88 | :txt, 89 | service_instance_name, 90 | config.ttl, 91 | to_charlist(service.txt_payload) 92 | ), 93 | to_dns_rr(:in, :srv, service_instance_name, config.ttl, srv_data), 94 | to_dns_rr(:in, :a, first_dot_local_name, config.ttl, :ipv4_address) 95 | ] 96 | end 97 | 98 | defp add_ptr_records3(records, %Options{} = config) do 99 | first_dot_local_name = hd(config.dot_local_names) |> to_charlist() 100 | 101 | [ 102 | to_dns_rr(:in, :ptr, :ipv4_arpa_address, config.ttl, first_dot_local_name), 103 | to_dns_rr(:in, :ptr, :ipv6_arpa_address, config.ttl, first_dot_local_name) | records 104 | ] 105 | end 106 | 107 | defp to_dns_rr(class, type, domain, ttl, data) do 108 | dns_rr( 109 | domain: normalize_domain(domain), 110 | class: class, 111 | type: type, 112 | ttl: ttl, 113 | data: data 114 | ) 115 | end 116 | 117 | defp normalize_domain(d) when is_atom(d), do: d 118 | defp normalize_domain(d), do: to_charlist(d) 119 | end 120 | -------------------------------------------------------------------------------- /lib/mdns_lite/table_server.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.TableServer do 6 | @moduledoc false 7 | use GenServer 8 | 9 | alias MdnsLite.DNS 10 | alias MdnsLite.IfInfo 11 | alias MdnsLite.Options 12 | alias MdnsLite.Table 13 | 14 | @spec start_link(Options.t()) :: GenServer.on_start() 15 | def start_link(%Options{} = opts) do 16 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 17 | end 18 | 19 | @spec update_options((Options.t() -> Options.t())) :: :ok 20 | def update_options(fun) do 21 | GenServer.call(__MODULE__, {:update_options, fun}) 22 | end 23 | 24 | @spec options() :: Options.t() 25 | def options() do 26 | GenServer.call(__MODULE__, :options) 27 | end 28 | 29 | @spec query(DNS.dns_query(), IfInfo.t()) :: %{ 30 | answer: [DNS.dns_rr()], 31 | additional: [DNS.dns_rr()] 32 | } 33 | def query(query, if_info) do 34 | GenServer.call(__MODULE__, {:query, query, if_info}) 35 | end 36 | 37 | @spec get_records() :: [DNS.dns_rr()] 38 | def get_records() do 39 | GenServer.call(__MODULE__, :get_records) 40 | end 41 | 42 | ############################################################################## 43 | # GenServer callbacks 44 | ############################################################################## 45 | @impl GenServer 46 | def init(opts) do 47 | {:ok, %{options: opts, table: Table.Builder.from_options(opts)}} 48 | end 49 | 50 | @impl GenServer 51 | def handle_call(:options, _from, state) do 52 | {:reply, state.options, state} 53 | end 54 | 55 | def handle_call({:update_options, fun}, _from, state) do 56 | new_options = fun.(state.options) 57 | 58 | {:reply, :ok, %{options: new_options, table: Table.Builder.from_options(new_options)}} 59 | end 60 | 61 | def handle_call(:get_records, _from, state) do 62 | {:reply, state.table, state} 63 | end 64 | 65 | def handle_call({:query, query, if_info}, _from, state) do 66 | rr_list = Table.query(state.table, query, if_info) 67 | additional = Table.additional_records(state.table, rr_list, if_info) 68 | 69 | {:reply, %{answer: rr_list, additional: additional}, state} 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/mdns_lite/utilities.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Utilities do 6 | @moduledoc false 7 | 8 | @doc """ 9 | Return a network interface's IP addresses 10 | 11 | * `ifaddrs` - the return value from `:inet.getifaddrs/0` 12 | """ 13 | @spec ifaddrs_to_ip_list( 14 | [{ifname :: charlist(), ifopts :: keyword()}], 15 | ifname :: String.t() 16 | ) :: [:inet.ip_address()] 17 | def ifaddrs_to_ip_list(ifaddrs, ifname) do 18 | ifname_cl = to_charlist(ifname) 19 | 20 | case List.keyfind(ifaddrs, ifname_cl, 0) do 21 | nil -> 22 | [] 23 | 24 | {^ifname_cl, params} -> 25 | Keyword.get_values(params, :addr) 26 | end 27 | end 28 | 29 | @doc """ 30 | Return whether the IP address is IPv4 (:inet) or IPv6 (:inet6) 31 | """ 32 | @spec ip_family(:inet.ip_address()) :: :inet | :inet6 33 | def ip_family({_, _, _, _}), do: :inet 34 | def ip_family({_, _, _, _, _, _, _, _}), do: :inet6 35 | end 36 | -------------------------------------------------------------------------------- /lib/mdns_lite/vintage_net_monitor.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Jon Carstens 2 | # SPDX-FileCopyrightText: 2021 Connor Rigby 3 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule MdnsLite.VintageNetMonitor do 8 | @moduledoc """ 9 | Network monitor that using VintageNet 10 | 11 | Use this network monitor to detect new network interfaces and their 12 | IP addresses when using Nerves. It is the default. 13 | """ 14 | use GenServer 15 | 16 | alias MdnsLite.CoreMonitor 17 | 18 | @addresses_topic ["interface", :_, "addresses"] 19 | 20 | @spec start_link([CoreMonitor.option()]) :: GenServer.on_start() 21 | def start_link(opts \\ []) do 22 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 23 | end 24 | 25 | @impl GenServer 26 | def init(opts) do 27 | # VintageNet is an optional dependency. Optional dependencies aren't 28 | # guaranteed to be started in order. See 29 | # https://github.com/elixir-lang/elixir/issues/10433. To work around this, 30 | # try to start it. 31 | _ = Application.ensure_all_started(:vintage_net) 32 | 33 | VintageNet.subscribe(@addresses_topic) 34 | 35 | {:ok, CoreMonitor.init(opts), {:continue, :initialization}} 36 | end 37 | 38 | @impl GenServer 39 | def handle_continue(:initialization, state) do 40 | new_state = 41 | VintageNet.match(@addresses_topic) 42 | |> Enum.reduce(state, &set_vn_address_reducer/2) 43 | |> CoreMonitor.flush_todo_list() 44 | 45 | {:noreply, new_state} 46 | end 47 | 48 | @impl GenServer 49 | def handle_info({VintageNet, ["interface", ifname, "addresses"], _old, new, _}, state) do 50 | new_state = 51 | state 52 | |> set_vn_address(ifname, new) 53 | |> CoreMonitor.flush_todo_list() 54 | 55 | {:noreply, new_state} 56 | end 57 | 58 | defp set_vn_address_reducer({["interface", ifname, "addresses"], addresses}, state) do 59 | set_vn_address(state, ifname, addresses) 60 | end 61 | 62 | defp set_vn_address(state, ifname, nil) do 63 | # nil gets passed when the network interface goes away. 64 | set_vn_address(state, ifname, []) 65 | end 66 | 67 | defp set_vn_address(state, ifname, addresses) do 68 | ip_list = Enum.map(addresses, fn %{address: ip} -> ip end) 69 | CoreMonitor.set_ip_list(state, ifname, ip_list) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/mix/tasks/mdns_lite.install.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Authors of https://github.com/ash-project/igniter 2 | # SPDX-FileCopyrightText: 2025 Lee Nussbaum 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | 7 | defmodule Mix.Tasks.MdnsLite.Install.Docs do 8 | @moduledoc false 9 | 10 | @spec short_doc() :: String.t() 11 | def short_doc() do 12 | "Installs mdns_lite in your Nerves project." 13 | end 14 | 15 | @spec example() :: String.t() 16 | def example() do 17 | "mix mdns_lite.install --hostname my-hostname" 18 | end 19 | 20 | @spec long_doc() :: String.t() 21 | def long_doc() do 22 | """ 23 | #{short_doc()} 24 | 25 | Longer explanation of your task 26 | 27 | ## Example 28 | 29 | ```bash 30 | #{example()} 31 | ``` 32 | 33 | ## Options 34 | 35 | * `--hostname my-hostname` - Set the initial hostname for the device (will be advertised as "my-hostname.local", defaults to "nerves.local") 36 | """ 37 | end 38 | end 39 | 40 | if Code.ensure_loaded?(Igniter) do 41 | defmodule Mix.Tasks.MdnsLite.Install do 42 | @shortdoc "#{__MODULE__.Docs.short_doc()}" 43 | 44 | @moduledoc __MODULE__.Docs.long_doc() 45 | 46 | use Igniter.Mix.Task 47 | alias Igniter.Project.Config 48 | 49 | @impl Igniter.Mix.Task 50 | def info(_argv, _composing_task) do 51 | %Igniter.Mix.Task.Info{ 52 | # Groups allow for overlapping arguments for tasks by the same author 53 | # See the generators guide for more. 54 | group: :mdns_lite, 55 | # *other* dependencies to add 56 | # i.e `{:foo, "~> 2.0"}` 57 | adds_deps: [], 58 | # *other* dependencies to add and call their associated installers, if they exist 59 | # i.e `{:foo, "~> 2.0"}` 60 | installs: [], 61 | # An example invocation 62 | example: __MODULE__.Docs.example(), 63 | # A list of environments that this should be installed in. 64 | only: nil, 65 | # a list of positional arguments, i.e `[:file]` 66 | positional: [], 67 | # Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv 68 | # This ensures your option schema includes options from nested tasks 69 | composes: [], 70 | # `OptionParser` schema 71 | schema: [hostname: :string], 72 | # Default values for the options in the `schema` 73 | defaults: [hostname: "nerves"], 74 | # CLI aliases 75 | aliases: [], 76 | # A list of options in the schema that are required 77 | required: [] 78 | } 79 | end 80 | 81 | @impl Igniter.Mix.Task 82 | def igniter(igniter) do 83 | if Igniter.exists?(igniter, "config/target.exs") do 84 | igniter 85 | |> igniter_nerves("target.exs") 86 | else 87 | igniter 88 | |> igniter_nerves("config.exs") 89 | |> Igniter.add_notice(""" 90 | The defaults for `mix mdns_lite.install` are intended for Nerves projects. Please visit 91 | its README at https://hexdocs.pm/mdns_lite/readme.html for an overview of usage. 92 | """) 93 | end 94 | end 95 | 96 | @spec igniter_nerves(Igniter.t(), String.t()) :: Igniter.t() 97 | def igniter_nerves(igniter, config_file) do 98 | igniter 99 | |> Config.configure_new( 100 | config_file, 101 | :mdns_lite, 102 | [:host], 103 | hostname: igniter.args.options[:hostname] 104 | ) 105 | |> Config.configure_new(config_file, :mdns_lite, [:ttl], 120) 106 | |> Config.configure_new( 107 | config_file, 108 | :mdns_lite, 109 | [:services], 110 | {:code, 111 | Sourceror.parse_string!(""" 112 | [ 113 | %{protocol: "ssh", port: 22, transport: "tcp"}, 114 | %{protocol: "sftp-ssh", port: 22, transport: "tcp"}, 115 | %{protocol: "epmd", port: 4369, transport: "tcp"} 116 | ] 117 | """)} 118 | ) 119 | end 120 | end 121 | else 122 | defmodule Mix.Tasks.MdnsLite.Install do 123 | @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" 124 | 125 | @moduledoc __MODULE__.Docs.long_doc() 126 | 127 | use Mix.Task 128 | 129 | @spec run(list()) :: no_return() 130 | def run(_argv) do 131 | Mix.shell().error(""" 132 | The task 'mdns_lite.install' requires igniter. Please install igniter and try again. 133 | 134 | For more information, see: https://hexdocs.pm/igniter/readme.html#installation 135 | """) 136 | 137 | exit({:shutdown, 1}) 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MdnsLite.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.9.0" 5 | @source_url "https://github.com/nerves-networking/mdns_lite" 6 | 7 | def project do 8 | [ 9 | app: :mdns_lite, 10 | version: @version, 11 | elixir: "~> 1.14", 12 | start_permanent: Mix.env() == :prod, 13 | consolidate_protocols: Mix.env() != :dev, 14 | docs: docs(), 15 | description: description(), 16 | package: package(), 17 | dialyzer: dialyzer(), 18 | deps: deps() 19 | ] 20 | end 21 | 22 | def cli do 23 | [preferred_envs: %{docs: :docs, "hex.publish": :docs, "hex.build": :docs, credo: :test}] 24 | end 25 | 26 | defp description do 27 | "A simple, no frills mDNS implementation in Elixir" 28 | end 29 | 30 | defp package do 31 | %{ 32 | files: [ 33 | "CHANGELOG.md", 34 | "lib", 35 | "LICENSES/*", 36 | "mix.exs", 37 | "NOTICE", 38 | "README.md", 39 | "REUSE.toml", 40 | "src" 41 | ], 42 | licenses: ["Apache-2.0"], 43 | links: %{ 44 | "GitHub" => @source_url, 45 | "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md", 46 | "REUSE Compliance" => 47 | "https://api.reuse.software/info/github.com/elixir-circuits/circuits_gpio" 48 | } 49 | } 50 | end 51 | 52 | def application do 53 | [ 54 | extra_applications: [:logger], 55 | mod: {MdnsLite.Application, []} 56 | ] 57 | end 58 | 59 | defp deps do 60 | [ 61 | {:igniter, "~> 0.5", optional: true}, 62 | {:dialyxir, "~> 1.1", only: :dev, runtime: false}, 63 | {:credo, "~> 1.2", only: :test, runtime: false}, 64 | {:ex_doc, "~> 0.22", only: :docs, runtime: false}, 65 | {:vintage_net, "~> 0.7", optional: true} 66 | ] 67 | end 68 | 69 | defp dialyzer() do 70 | [ 71 | flags: [:missing_return, :extra_return, :unmatched_returns, :error_handling, :underspecs], 72 | ignore_warnings: ".dialyzer_ignore.exs", 73 | plt_add_apps: [:vintage_net, :igniter, :mix, :sourceror] 74 | ] 75 | end 76 | 77 | defp docs do 78 | [ 79 | extras: ["README.md", "CHANGELOG.md"], 80 | main: "readme", 81 | source_ref: "v#{@version}", 82 | source_url: @source_url, 83 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"] 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "beam_notify": {:hex, :beam_notify, "1.1.1", "da7dd04f16120bcab71f01ff81607ea38ad78af859cd87fad2012eac8feab034", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "5a09fc8449171da423f781de16b95cdec52763e60308345de6f15358c2382a42"}, 3 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 4 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 8 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 9 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, 12 | "gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"}, 13 | "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, 14 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 15 | "igniter": {:hex, :igniter, "0.6.5", "0b16a37e1aaaefc39777c6250980a314df8ba02a8ae81063d786a7bddb40dbf0", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "21dec3066f372f49f391d00a2067769eb20f7a2213513e022593e4b51bad93e2"}, 16 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 17 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 18 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 19 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 20 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 21 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 22 | "muontrap": {:hex, :muontrap, "1.6.1", "4a81a159f64e4c7bf01162a7863559d634bc48929218690ada309a9a98a9ac22", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8ad31072402bebed3f554c9a463aa272c6dd964168c9cb81385f8711f068ed47"}, 23 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 24 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 25 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 26 | "owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"}, 27 | "property_table": {:hex, :property_table, "0.3.1", "f71381dea834f9c62eff043c626e7af0ef5697258cb961040927e086c7ecb9b6", [:mix], [], "hexpm", "3ebc06667d3a95101a7dba5e9db800395bd5de6b8debbc5a064f78bb1ec5668b"}, 28 | "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, 29 | "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, 30 | "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, 31 | "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, 32 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 33 | "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, 34 | "vintage_net": {:hex, :vintage_net, "0.13.6", "87b6426d4748dce32b530025236444932c4e18c5e68297f8a48c9a33a15891fa", [:make, :mix], [{:beam_notify, "~> 0.2.0 or ~> 1.0", [hex: :beam_notify, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0 or ~> 2.1.0 or ~> 3.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.1 or ~> 0.6.0 or ~> 1.0", [hex: :muontrap, repo: "hexpm", optional: false]}, {:property_table, "~> 0.2.0 or ~> 0.3.0", [hex: :property_table, repo: "hexpm", optional: false]}], "hexpm", "f8410507f47dab294431802c4d501c50871c45fed74d279d1a97e34e8b4fed4c"}, 35 | } 36 | -------------------------------------------------------------------------------- /src/mdns_lite_inet_dns.hrl: -------------------------------------------------------------------------------- 1 | %% SPDX-FileCopyrightText: Ericsson AB 1997-2021. All Rights Reserved. 2 | %% 3 | %% SPDX-License-Identifier: Apache-2.0 4 | %% 5 | %% Definition for Domain Name System 6 | %% 7 | 8 | %% 9 | %% Currently defined opcodes 10 | %% 11 | -define(QUERY, 16#0). %% standard query 12 | -define(IQUERY, 16#1). %% inverse query 13 | -define(STATUS, 16#2). %% nameserver status query 14 | %% -define(xxx, 16#3) %% 16#3 reserved 15 | %% non standard 16 | -define(UPDATEA, 16#9). %% add resource record 17 | -define(UPDATED, 16#a). %% delete a specific resource record 18 | -define(UPDATEDA, 16#b). %% delete all nemed resource record 19 | -define(UPDATEM, 16#c). %% modify a specific resource record 20 | -define(UPDATEMA, 16#d). %% modify all named resource record 21 | 22 | -define(ZONEINIT, 16#e). %% initial zone transfer 23 | -define(ZONEREF, 16#f). %% incremental zone referesh 24 | 25 | 26 | %% 27 | %% Currently defined response codes 28 | %% 29 | -define(NOERROR, 0). %% no error 30 | -define(FORMERR, 1). %% format error 31 | -define(SERVFAIL, 2). %% server failure 32 | -define(NXDOMAIN, 3). %% non existent domain 33 | -define(NOTIMP, 4). %% not implemented 34 | -define(REFUSED, 5). %% query refused 35 | %% non standard 36 | -define(NOCHANGE, 16#f). %% update failed to change db 37 | -define(BADVERS, 16). 38 | 39 | %% 40 | %% Type values for resources and queries 41 | %% 42 | -define(T_A, 1). %% host address 43 | -define(T_NS, 2). %% authoritative server 44 | -define(T_MD, 3). %% mail destination 45 | -define(T_MF, 4). %% mail forwarder 46 | -define(T_CNAME, 5). %% connonical name 47 | -define(T_SOA, 6). %% start of authority zone 48 | -define(T_MB, 7). %% mailbox domain name 49 | -define(T_MG, 8). %% mail group member 50 | -define(T_MR, 9). %% mail rename name 51 | -define(T_NULL, 10). %% null resource record 52 | -define(T_WKS, 11). %% well known service 53 | -define(T_PTR, 12). %% domain name pointer 54 | -define(T_HINFO, 13). %% host information 55 | -define(T_MINFO, 14). %% mailbox information 56 | -define(T_MX, 15). %% mail routing information 57 | -define(T_TXT, 16). %% text strings 58 | -define(T_AAAA, 28). %% ipv6 address 59 | %% SRV (RFC 2052) 60 | -define(T_SRV, 33). %% services 61 | %% NAPTR (RFC 2915) 62 | -define(T_NAPTR, 35). %% naming authority pointer 63 | -define(T_OPT, 41). %% EDNS pseudo-rr RFC2671(7) 64 | %% SPF (RFC 4408) 65 | -define(T_SPF, 99). %% server policy framework 66 | %% non standard 67 | -define(T_UINFO, 100). %% user (finger) information 68 | -define(T_UID, 101). %% user ID 69 | -define(T_GID, 102). %% group ID 70 | -define(T_UNSPEC, 103). %% Unspecified format (binary data) 71 | %% Query type values which do not appear in resource records 72 | -define(T_AXFR, 252). %% transfer zone of authority 73 | -define(T_MAILB, 253). %% transfer mailbox records 74 | -define(T_MAILA, 254). %% transfer mail agent records 75 | -define(T_ANY, 255). %% wildcard match 76 | %% URI (RFC 7553) 77 | -define(T_URI, 256). %% uniform resource identifier 78 | %% CAA (RFC 6844) 79 | -define(T_CAA, 257). %% certification authority authorization 80 | 81 | %% 82 | %% Symbolic Type values for resources and queries 83 | %% 84 | -define(S_A, a). %% host address 85 | -define(S_NS, ns). %% authoritative server 86 | -define(S_MD, md). %% mail destination 87 | -define(S_MF, mf). %% mail forwarder 88 | -define(S_CNAME, cname). %% connonical name 89 | -define(S_SOA, soa). %% start of authority zone 90 | -define(S_MB, mb). %% mailbox domain name 91 | -define(S_MG, mg). %% mail group member 92 | -define(S_MR, mr). %% mail rename name 93 | -define(S_NULL, null). %% null resource record 94 | -define(S_WKS, wks). %% well known service 95 | -define(S_PTR, ptr). %% domain name pointer 96 | -define(S_HINFO, hinfo). %% host information 97 | -define(S_MINFO, minfo). %% mailbox information 98 | -define(S_MX, mx). %% mail routing information 99 | -define(S_TXT, txt). %% text strings 100 | -define(S_AAAA, aaaa). %% ipv6 address 101 | %% SRV (RFC 2052) 102 | -define(S_SRV, srv). %% services 103 | %% NAPTR (RFC 2915) 104 | -define(S_NAPTR, naptr). %% naming authority pointer 105 | -define(S_OPT, opt). %% EDNS pseudo-rr RFC2671(7) 106 | %% SPF (RFC 4408) 107 | -define(S_SPF, spf). %% server policy framework 108 | %% non standard 109 | -define(S_UINFO, uinfo). %% user (finger) information 110 | -define(S_UID, uid). %% user ID 111 | -define(S_GID, gid). %% group ID 112 | -define(S_UNSPEC, unspec). %% Unspecified format (binary data) 113 | %% Query type values which do not appear in resource records 114 | -define(S_AXFR, axfr). %% transfer zone of authority 115 | -define(S_MAILB, mailb). %% transfer mailbox records 116 | -define(S_MAILA, maila). %% transfer mail agent records 117 | -define(S_ANY, any). %% wildcard match 118 | %% URI (RFC 7553) 119 | -define(S_URI, uri). %% uniform resource identifier 120 | %% CAA (RFC 6844) 121 | -define(S_CAA, caa). %% certification authority authorization 122 | 123 | %% 124 | %% Values for class field 125 | %% 126 | 127 | -define(C_IN, 1). %% the arpa internet 128 | -define(C_CHAOS, 3). %% for chaos net at MIT 129 | -define(C_HS, 4). %% for Hesiod name server at MIT 130 | %% Query class values which do not appear in resource records 131 | -define(C_ANY, 255). %% wildcard match 132 | 133 | 134 | %% indirection mask for compressed domain names 135 | -define(INDIR_MASK, 16#c0). 136 | 137 | %% 138 | %% Structure for query header, the order of the fields is machine and 139 | %% compiler dependent, in our case, the bits within a byte are assignd 140 | %% least significant first, while the order of transmition is most 141 | %% significant first. This requires a somewhat confusing rearrangement. 142 | %% 143 | -record(dns_header, 144 | { 145 | id = 0, %% ushort query identification number 146 | %% byte F0 147 | qr = 0, %% :1 response flag 148 | opcode = 0, %% :4 purpose of message 149 | aa = 0, %% :1 authoritive answer 150 | tc = 0, %% :1 truncated message 151 | rd = 0, %% :1 recursion desired 152 | %% byte F1 153 | ra = 0, %% :1 recursion available 154 | pr = 0, %% :1 primary server required (non standard) 155 | %% :2 unused bits 156 | rcode = 0 %% :4 response code 157 | }). 158 | 159 | -record(dns_rec, 160 | { 161 | header, %% dns_header record 162 | qdlist = [], %% list of question entries 163 | anlist = [], %% list of answer entries 164 | nslist = [], %% list of authority entries 165 | arlist = [] %% list of resource entries 166 | }). 167 | 168 | %% DNS resource record 169 | -record(dns_rr, 170 | { 171 | domain = "", %% resource domain 172 | type = any, %% resource type 173 | class = in, %% reource class 174 | cnt = 0, %% access count 175 | ttl = 0, %% time to live 176 | data = [], %% raw data 177 | %% 178 | tm, %% creation time 179 | bm = [], %% Bitmap storing domain character case information. 180 | func = false %% Was: Optional function calculating the data field. 181 | %% Now: cache-flush Class flag from mDNS RFC 6762 182 | }). 183 | 184 | -define(DNS_UDP_PAYLOAD_SIZE, 1280). 185 | 186 | -record(dns_rr_opt, %% EDNS RR OPT (RFC2671), dns_rr{type=opt} 187 | { 188 | domain = "", %% should be the root domain 189 | type = opt, 190 | udp_payload_size = ?DNS_UDP_PAYLOAD_SIZE, %% RFC2671(4.5 CLASS) 191 | ext_rcode = 0, %% RFC2671(4.6 EXTENDED-RCODE) 192 | version = 0, %% RFC2671(4.6 VERSION) 193 | z = 0, %% RFC2671(4.6 Z) 194 | data = [] %% RFC2671(4.4) 195 | }). 196 | 197 | -record(dns_query, 198 | { 199 | domain, %% query domain 200 | type, %% query type 201 | class, %% query class 202 | unicast_response = false %% mDNS RFC 6762 Class flag 203 | }). 204 | -------------------------------------------------------------------------------- /src/mdns_lite_inet_int.hrl: -------------------------------------------------------------------------------- 1 | %% SPDX-FileCopyrightText: Ericsson AB 1997-2021. All Rights Reserved. 2 | %% 3 | %% SPDX-License-Identifier: Apache-2.0 4 | %% 5 | 6 | %%---------------------------------------------------------------------------- 7 | %% Interface constants. 8 | %% 9 | %% This section must be "identical" to the corresponding in inet_drv.c 10 | %% 11 | 12 | %% family codes to open 13 | -define(INET_AF_UNSPEC, 0). 14 | -define(INET_AF_INET, 1). 15 | -define(INET_AF_INET6, 2). 16 | -define(INET_AF_ANY, 3). % Fake for ANY in any address family 17 | -define(INET_AF_LOOPBACK, 4). % Fake for LOOPBACK in any address family 18 | -define(INET_AF_LOCAL, 5). % For Unix Domain address family 19 | -define(INET_AF_UNDEFINED, 6). % For any unknown address family 20 | 21 | %% type codes to open and gettype - INET_REQ_GETTYPE 22 | -define(INET_TYPE_STREAM, 1). 23 | -define(INET_TYPE_DGRAM, 2). 24 | -define(INET_TYPE_SEQPACKET, 3). 25 | 26 | %% socket modes, INET_LOPT_MODE 27 | -define(INET_MODE_LIST, 0). 28 | -define(INET_MODE_BINARY, 1). 29 | 30 | %% deliver mode, INET_LOPT_DELIVER 31 | -define(INET_DELIVER_PORT, 0). 32 | -define(INET_DELIVER_TERM, 1). 33 | 34 | %% active socket, INET_LOPT_ACTIVE 35 | -define(INET_PASSIVE, 0). 36 | -define(INET_ACTIVE, 1). 37 | -define(INET_ONCE, 2). % Active once then passive 38 | -define(INET_MULTI, 3). % Active N then passive 39 | 40 | %% state codes (getstatus, INET_REQ_GETSTATUS) 41 | -define(INET_F_OPEN, 16#0001). 42 | -define(INET_F_BOUND, 16#0002). 43 | -define(INET_F_ACTIVE, 16#0004). 44 | -define(INET_F_LISTEN, 16#0008). 45 | -define(INET_F_CON, 16#0010). 46 | -define(INET_F_ACC, 16#0020). 47 | -define(INET_F_LST, 16#0040). 48 | -define(INET_F_BUSY, 16#0080). 49 | 50 | %% request codes (erlang:port_control/3) 51 | -define(INET_REQ_OPEN, 1). 52 | -define(INET_REQ_CLOSE, 2). 53 | -define(INET_REQ_CONNECT, 3). 54 | -define(INET_REQ_PEER, 4). 55 | -define(INET_REQ_NAME, 5). 56 | -define(INET_REQ_BIND, 6). 57 | -define(INET_REQ_SETOPTS, 7). 58 | -define(INET_REQ_GETOPTS, 8). 59 | -define(INET_REQ_GETIX, 9). 60 | %% -define(INET_REQ_GETIF, 10). OBSOLETE 61 | -define(INET_REQ_GETSTAT, 11). 62 | -define(INET_REQ_GETHOSTNAME, 12). 63 | -define(INET_REQ_FDOPEN, 13). 64 | -define(INET_REQ_GETFD, 14). 65 | -define(INET_REQ_GETTYPE, 15). 66 | -define(INET_REQ_GETSTATUS, 16). 67 | -define(INET_REQ_GETSERVBYNAME, 17). 68 | -define(INET_REQ_GETSERVBYPORT, 18). 69 | -define(INET_REQ_SETNAME, 19). 70 | -define(INET_REQ_SETPEER, 20). 71 | -define(INET_REQ_GETIFLIST, 21). 72 | -define(INET_REQ_IFGET, 22). 73 | -define(INET_REQ_IFSET, 23). 74 | -define(INET_REQ_SUBSCRIBE, 24). 75 | -define(INET_REQ_GETIFADDRS, 25). 76 | -define(INET_REQ_ACCEPT, 26). 77 | -define(INET_REQ_LISTEN, 27). 78 | -define(INET_REQ_IGNOREFD, 28). 79 | -define(INET_REQ_GETLADDRS, 29). 80 | -define(INET_REQ_GETPADDRS, 30). 81 | 82 | %% TCP requests 83 | %%-define(TCP_REQ_ACCEPT, 40). MOVED 84 | %%-define(TCP_REQ_LISTEN, 41). MERGED 85 | -define(TCP_REQ_RECV, 42). 86 | -define(TCP_REQ_UNRECV, 43). 87 | -define(TCP_REQ_SHUTDOWN, 44). 88 | -define(TCP_REQ_SENDFILE, 45). 89 | 90 | %% UDP and SCTP requests 91 | -define(PACKET_REQ_RECV, 60). 92 | %%-define(SCTP_REQ_LISTEN, 61). MERGED 93 | -define(SCTP_REQ_BINDX, 62). %% Multi-home SCTP bind 94 | -define(SCTP_REQ_PEELOFF, 63). 95 | 96 | %% subscribe codes, INET_REQ_SUBSCRIBE 97 | -define(INET_SUBS_EMPTY_OUT_Q, 1). 98 | 99 | %% reply codes for *_REQ_* 100 | -define(INET_REP_ERROR, 0). 101 | -define(INET_REP_OK, 1). 102 | -define(INET_REP, 2). 103 | 104 | %% INET, TCP and UDP options: 105 | -define(INET_OPT_REUSEADDR, 0). 106 | -define(INET_OPT_KEEPALIVE, 1). 107 | -define(INET_OPT_DONTROUTE, 2). 108 | -define(INET_OPT_LINGER, 3). 109 | -define(INET_OPT_BROADCAST, 4). 110 | -define(INET_OPT_OOBINLINE, 5). 111 | -define(INET_OPT_SNDBUF, 6). 112 | -define(INET_OPT_RCVBUF, 7). 113 | -define(INET_OPT_PRIORITY, 8). 114 | -define(INET_OPT_TOS, 9). 115 | -define(TCP_OPT_NODELAY, 10). 116 | -define(UDP_OPT_MULTICAST_IF, 11). 117 | -define(UDP_OPT_MULTICAST_TTL, 12). 118 | -define(UDP_OPT_MULTICAST_LOOP, 13). 119 | -define(UDP_OPT_ADD_MEMBERSHIP, 14). 120 | -define(UDP_OPT_DROP_MEMBERSHIP, 15). 121 | -define(INET_OPT_IPV6_V6ONLY, 16). 122 | % "Local" options: codes start from 20: 123 | -define(INET_LOPT_BUFFER, 20). 124 | -define(INET_LOPT_HEADER, 21). 125 | -define(INET_LOPT_ACTIVE, 22). 126 | -define(INET_LOPT_PACKET, 23). 127 | -define(INET_LOPT_MODE, 24). 128 | -define(INET_LOPT_DELIVER, 25). 129 | -define(INET_LOPT_EXITONCLOSE, 26). 130 | -define(INET_LOPT_TCP_HIWTRMRK, 27). 131 | -define(INET_LOPT_TCP_LOWTRMRK, 28). 132 | -define(INET_LOPT_TCP_SEND_TIMEOUT, 30). 133 | -define(INET_LOPT_TCP_DELAY_SEND, 31). 134 | -define(INET_LOPT_PACKET_SIZE, 32). 135 | -define(INET_LOPT_READ_PACKETS, 33). 136 | -define(INET_OPT_RAW, 34). 137 | -define(INET_LOPT_TCP_SEND_TIMEOUT_CLOSE, 35). 138 | -define(INET_LOPT_MSGQ_HIWTRMRK, 36). 139 | -define(INET_LOPT_MSGQ_LOWTRMRK, 37). 140 | -define(INET_LOPT_NETNS, 38). 141 | -define(INET_LOPT_TCP_SHOW_ECONNRESET, 39). 142 | -define(INET_LOPT_LINE_DELIM, 40). 143 | -define(INET_OPT_TCLASS, 41). 144 | -define(INET_OPT_BIND_TO_DEVICE, 42). 145 | -define(INET_OPT_RECVTOS, 43). 146 | -define(INET_OPT_RECVTCLASS, 44). 147 | -define(INET_OPT_PKTOPTIONS, 45). 148 | -define(INET_OPT_TTL, 46). 149 | -define(INET_OPT_RECVTTL, 47). 150 | -define(TCP_OPT_NOPUSH, 48). 151 | % Specific SCTP options: separate range: 152 | -define(SCTP_OPT_RTOINFO, 100). 153 | -define(SCTP_OPT_ASSOCINFO, 101). 154 | -define(SCTP_OPT_INITMSG, 102). 155 | -define(SCTP_OPT_AUTOCLOSE, 103). 156 | -define(SCTP_OPT_NODELAY, 104). 157 | -define(SCTP_OPT_DISABLE_FRAGMENTS, 105). 158 | -define(SCTP_OPT_I_WANT_MAPPED_V4_ADDR, 106). 159 | -define(SCTP_OPT_MAXSEG, 107). 160 | -define(SCTP_OPT_SET_PEER_PRIMARY_ADDR, 108). 161 | -define(SCTP_OPT_PRIMARY_ADDR, 109). 162 | -define(SCTP_OPT_ADAPTATION_LAYER, 110). 163 | -define(SCTP_OPT_PEER_ADDR_PARAMS, 111). 164 | -define(SCTP_OPT_DEFAULT_SEND_PARAM, 112). 165 | -define(SCTP_OPT_EVENTS, 113). 166 | -define(SCTP_OPT_DELAYED_ACK_TIME, 114). 167 | -define(SCTP_OPT_STATUS, 115). 168 | -define(SCTP_OPT_GET_PEER_ADDR_INFO, 116). 169 | 170 | %% interface options, INET_REQ_IFGET and INET_REQ_IFSET 171 | -define(INET_IFOPT_ADDR, 1). 172 | -define(INET_IFOPT_BROADADDR, 2). 173 | -define(INET_IFOPT_DSTADDR, 3). 174 | -define(INET_IFOPT_MTU, 4). 175 | -define(INET_IFOPT_NETMASK, 5). 176 | -define(INET_IFOPT_FLAGS, 6). 177 | -define(INET_IFOPT_HWADDR, 7). %% where support (e.g linux) 178 | 179 | %% packet byte values, INET_LOPT_PACKET 180 | -define(TCP_PB_RAW, 0). 181 | -define(TCP_PB_1, 1). 182 | -define(TCP_PB_2, 2). 183 | -define(TCP_PB_4, 3). 184 | -define(TCP_PB_ASN1, 4). 185 | -define(TCP_PB_RM, 5). 186 | -define(TCP_PB_CDR, 6). 187 | -define(TCP_PB_FCGI, 7). 188 | -define(TCP_PB_LINE_LF, 8). 189 | -define(TCP_PB_TPKT, 9). 190 | -define(TCP_PB_HTTP, 10). 191 | -define(TCP_PB_HTTPH, 11). 192 | -define(TCP_PB_SSL_TLS, 12). 193 | -define(TCP_PB_HTTP_BIN,13). 194 | -define(TCP_PB_HTTPH_BIN,14). 195 | 196 | 197 | %% getstat, INET_REQ_GETSTAT 198 | -define(INET_STAT_RECV_CNT, 1). 199 | -define(INET_STAT_RECV_MAX, 2). 200 | -define(INET_STAT_RECV_AVG, 3). 201 | -define(INET_STAT_RECV_DVI, 4). 202 | -define(INET_STAT_SEND_CNT, 5). 203 | -define(INET_STAT_SEND_MAX, 6). 204 | -define(INET_STAT_SEND_AVG, 7). 205 | -define(INET_STAT_SEND_PEND, 8). 206 | -define(INET_STAT_RECV_OCT, 9). 207 | -define(INET_STAT_SEND_OCT, 10). 208 | 209 | %% interface stuff, INET_IFOPT_FLAGS 210 | -define(INET_IFNAMSIZ, 16). 211 | -define(INET_IFF_UP, 16#0001). 212 | -define(INET_IFF_BROADCAST, 16#0002). 213 | -define(INET_IFF_LOOPBACK, 16#0004). 214 | -define(INET_IFF_POINTTOPOINT, 16#0008). 215 | -define(INET_IFF_RUNNING, 16#0010). 216 | -define(INET_IFF_MULTICAST, 16#0020). 217 | %% 218 | -define(INET_IFF_DOWN, 16#0100). 219 | -define(INET_IFF_NBROADCAST, 16#0200). 220 | -define(INET_IFF_NPOINTTOPOINT, 16#0800). 221 | 222 | %% SCTP Flags for "sctp_sndrcvinfo": 223 | %% INET_REQ_SETOPTS:SCTP_OPT_DEFAULT_SEND_PARAM 224 | -define(SCTP_FLAG_UNORDERED, 1). % sctp_unordered 225 | -define(SCTP_FLAG_ADDR_OVER, 2). % sctp_addr_over 226 | -define(SCTP_FLAG_ABORT, 4). % sctp_abort 227 | -define(SCTP_FLAG_EOF, 8). % sctp_eof 228 | -define(SCTP_FLAG_SNDALL, 16). % sctp_sndall, NOT YET IMPLEMENTED. 229 | 230 | %% SCTP Flags for "sctp_paddrparams", and the corresp Atoms: 231 | -define(SCTP_FLAG_HB_ENABLE, 1). % sctp_hb_enable 232 | -define(SCTP_FLAG_HB_DISABLE, 2). % sctp_hb_disable 233 | -define(SCTP_FLAG_HB_DEMAND, 4). % sctp_hb_demand 234 | -define(SCTP_FLAG_PMTUD_ENABLE, 8). % sctp_pmtud_enable 235 | -define(SCTP_FLAG_PMTUD_DISABLE, 16). % sctp_pmtud_disable 236 | -define(SCTP_FLAG_SACKDELAY_ENABLE, 32). % sctp_sackdelay_enable 237 | -define(SCTP_FLAG_SACKDELAY_DISABLE, 64). % sctp_sackdelay_disable 238 | 239 | %% 240 | %% End of interface constants. 241 | %%---------------------------------------------------------------------------- 242 | 243 | -define(LISTEN_BACKLOG, 5). %% default backlog 244 | 245 | %% 5 secs need more ??? 246 | -define(INET_CLOSE_TIMEOUT, 5000). 247 | 248 | %% 249 | %% Port/socket numbers: network standard functions 250 | %% 251 | -define(IPPORT_ECHO, 7). 252 | -define(IPPORT_DISCARD, 9). 253 | -define(IPPORT_SYSTAT, 11). 254 | -define(IPPORT_DAYTIME, 13). 255 | -define(IPPORT_NETSTAT, 15). 256 | -define(IPPORT_FTP, 21). 257 | -define(IPPORT_TELNET, 23). 258 | -define(IPPORT_SMTP, 25). 259 | -define(IPPORT_TIMESERVER, 37). 260 | -define(IPPORT_NAMESERVER, 42). 261 | -define(IPPORT_WHOIS, 43). 262 | -define(IPPORT_MTP, 57). 263 | 264 | %% 265 | %% Port/socket numbers: host specific functions 266 | %% 267 | -define(IPPORT_TFTP, 69). 268 | -define(IPPORT_RJE, 77). 269 | -define(IPPORT_FINGER, 79). 270 | -define(IPPORT_TTYLINK, 87). 271 | -define(IPPORT_SUPDUP, 95). 272 | 273 | %% 274 | %% UNIX TCP sockets 275 | %% 276 | -define(IPPORT_EXECSERVER, 512). 277 | -define(IPPORT_LOGINSERVER, 513). 278 | -define(IPPORT_CMDSERVER, 514). 279 | -define(IPPORT_EFSSERVER, 520). 280 | 281 | %% 282 | %% UNIX UDP sockets 283 | %% 284 | -define(IPPORT_BIFFUDP, 512). 285 | -define(IPPORT_WHOSERVER, 513). 286 | -define(IPPORT_ROUTESERVER, 520). %% 520+1 also used 287 | 288 | 289 | %% 290 | %% Ports < IPPORT_RESERVED are reserved for 291 | %% privileged processes (e.g. root). 292 | %% Ports > IPPORT_USERRESERVED are reserved 293 | %% for servers, not necessarily privileged. 294 | %% 295 | -define(IPPORT_RESERVED, 1024). 296 | -define(IPPORT_USERRESERVED, 5000). 297 | 298 | %% standard port for socks 299 | -define(IPPORT_SOCKS, 1080). 300 | 301 | %% 302 | %% Int to bytes 303 | %% 304 | -define(int8(X), [(X) band 16#ff]). 305 | 306 | -define(int16(X), [((X) bsr 8) band 16#ff, (X) band 16#ff]). 307 | 308 | -define(int24(X), [((X) bsr 16) band 16#ff, 309 | ((X) bsr 8) band 16#ff, (X) band 16#ff]). 310 | 311 | -define(int32(X), 312 | [((X) bsr 24) band 16#ff, ((X) bsr 16) band 16#ff, 313 | ((X) bsr 8) band 16#ff, (X) band 16#ff]). 314 | 315 | -define(int64(X), 316 | [((X) bsr 56) band 16#ff, ((X) bsr 48) band 16#ff, 317 | ((X) bsr 40) band 16#ff, ((X) bsr 32) band 16#ff, 318 | ((X) bsr 24) band 16#ff, ((X) bsr 16) band 16#ff, 319 | ((X) bsr 8) band 16#ff, (X) band 16#ff]). 320 | 321 | -define(intAID(X), % For SCTP AssocID 322 | ?int32(X)). 323 | 324 | %% Bytes to unsigned 325 | -define(u64(X7,X6,X5,X4,X3,X2,X1,X0), 326 | ( ((X7) bsl 56) bor ((X6) bsl 48) bor ((X5) bsl 40) bor 327 | ((X4) bsl 32) bor ((X3) bsl 24) bor ((X2) bsl 16) bor 328 | ((X1) bsl 8) bor (X0) )). 329 | 330 | -define(u32(X3,X2,X1,X0), 331 | (((X3) bsl 24) bor ((X2) bsl 16) bor ((X1) bsl 8) bor (X0))). 332 | 333 | -define(u24(X2,X1,X0), 334 | (((X2) bsl 16) bor ((X1) bsl 8) bor (X0))). 335 | 336 | -define(u16(X1,X0), 337 | (((X1) bsl 8) bor (X0))). 338 | 339 | -define(u8(X0), (X0)). 340 | 341 | %% Bytes to signed 342 | -define(i32(X3,X2,X1,X0), 343 | (?u32(X3,X2,X1,X0) - 344 | (if (X3) > 127 -> 16#100000000; true -> 0 end))). 345 | 346 | -define(i24(X2,X1,X0), 347 | (?u24(X2,X1,X0) - 348 | (if (X2) > 127 -> 16#1000000; true -> 0 end))). 349 | 350 | -define(i16(X1,X0), 351 | (?u16(X1,X0) - 352 | (if (X1) > 127 -> 16#10000; true -> 0 end))). 353 | 354 | -define(i8(X0), 355 | (?u8(X0) - 356 | (if (X0) > 127 -> 16#100; true -> 0 end))). 357 | 358 | %% macro for use in guard for checking ip address {A,B,C,D} 359 | -define(ip(A,B,C,D), 360 | (((A) bor (B) bor (C) bor (D)) band (bnot 16#ff)) =:= 0). 361 | -define(ip(Addr), 362 | ?ip(element(1, (Addr)), element(2, (Addr)), 363 | element(3, (Addr)), element(4, (Addr)))). 364 | 365 | -define(ip6(A,B,C,D,E,F,G,H), 366 | (((A) bor (B) bor (C) bor (D) bor (E) bor (F) bor (G) bor (H)) 367 | band (bnot 16#ffff)) =:= 0). 368 | -define(ip6(Addr), 369 | ?ip6(element(1, (Addr)), element(2, (Addr)), 370 | element(3, (Addr)), element(4, (Addr)), 371 | element(5, (Addr)), element(6, (Addr)), 372 | element(7, (Addr)), element(8, (Addr)))). 373 | 374 | -define(ether(A,B,C,D,E,F), 375 | (((A) bor (B) bor (C) bor (D) bor (E) bor (F)) 376 | band (bnot 16#ff)) =:= 0). 377 | 378 | -define(port(P), (((P) band bnot 16#ffff) =:= 0)). 379 | 380 | %% default options (when inet_drv port is started) 381 | %% 382 | %% bufsz = INET_MIN_BUFFER (8K) 383 | %% header = 0 384 | %% packet = 0 (raw) 385 | %% mode = list 386 | %% deliver = term 387 | %% active = false 388 | %% 389 | -record(connect_opts, 390 | { 391 | ifaddr, %% don't bind explicitly, let connect decide 392 | port = 0, %% bind to port (default is dynamic port) 393 | fd = -1, %% fd >= 0 => already bound 394 | opts = [] %% [{active,true}] added in inet:connect_options 395 | }). 396 | 397 | -record(listen_opts, 398 | { 399 | ifaddr, %% interpreted as 'any' in *_tcp.erl 400 | port = 0, %% bind to port (default is dynamic port) 401 | backlog = ?LISTEN_BACKLOG, %% backlog 402 | fd = -1, %% %% fd >= 0 => already bound 403 | opts = [] %% [{active,true}] added in 404 | %% inet:listen_options 405 | }). 406 | 407 | -record(udp_opts, 408 | { 409 | ifaddr, 410 | port = 0, 411 | fd = -1, 412 | opts = [{active, true}] 413 | }). 414 | 415 | -define(SCTP_DEF_BUFSZ, 65536). 416 | -record(sctp_opts, 417 | { 418 | ifaddr, 419 | port = 0, 420 | fd = -1, 421 | type = seqpacket, 422 | opts = [{mode, binary}, 423 | {buffer, ?SCTP_DEF_BUFSZ}, 424 | {sndbuf, ?SCTP_DEF_BUFSZ}, 425 | {recbuf, 1024}, 426 | {sctp_events, undefined}%, 427 | %%{active, true} 428 | ] 429 | }). 430 | 431 | %% The following Tags are purely internal, used for marking items in the 432 | %% send buffer: 433 | -define(SCTP_TAG_SEND_ANC_INITMSG, 0). 434 | -define(SCTP_TAG_SEND_ANC_PARAMS, 1). 435 | -define(SCTP_TAG_SEND_DATA, 2). 436 | -------------------------------------------------------------------------------- /test/mdns_lite/cache_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.CacheTest do 6 | use ExUnit.Case 7 | 8 | import MdnsLite.DNS 9 | alias MdnsLite.Cache 10 | 11 | doctest Cache 12 | 13 | @test_a_record dns_rr( 14 | class: :in, 15 | type: :a, 16 | ttl: 120, 17 | domain: ~c"nerves-1234.local", 18 | data: {192, 168, 0, 100} 19 | ) 20 | @test_aaaa_record dns_rr( 21 | class: :in, 22 | type: :aaaa, 23 | ttl: 120, 24 | domain: ~c"nerves-1234.local", 25 | data: {65152, 0, 0, 0, 3297, 21943, 7498, 1443} 26 | ) 27 | 28 | test "caches A and AAAA records" do 29 | cache = 30 | Cache.new() 31 | |> Cache.insert(0, @test_a_record) 32 | |> Cache.insert(0, @test_aaaa_record) 33 | 34 | assert Cache.query(cache, dns_query(class: :in, type: :a, domain: ~c"nerves-1234.local")) == 35 | %{answer: [@test_a_record], additional: []} 36 | end 37 | 38 | test "expires old records" do 39 | # Insert a second record right after the first one expires 40 | cache = 41 | Cache.new() 42 | |> Cache.insert(0, @test_a_record) 43 | |> Cache.insert(120, @test_aaaa_record) 44 | 45 | assert cache == %Cache{records: [@test_aaaa_record], last_gc: 120} 46 | end 47 | 48 | test "inserting bumps ttl of existing entry" do 49 | cache = 50 | Cache.new() 51 | |> Cache.insert(0, @test_a_record) 52 | |> Cache.insert(60, @test_a_record) 53 | 54 | assert cache == %Cache{records: [@test_a_record], last_gc: 60} 55 | end 56 | 57 | test "inserting forces max ttl" do 58 | cache = 59 | Cache.new() 60 | |> Cache.insert( 61 | 0, 62 | dns_rr( 63 | class: :in, 64 | type: :a, 65 | ttl: 1_200_000, 66 | domain: ~c"nerves-1234.local", 67 | data: {192, 168, 0, 100} 68 | ) 69 | ) 70 | 71 | assert cache == %Cache{ 72 | records: [ 73 | dns_rr( 74 | class: :in, 75 | type: :a, 76 | ttl: 75 * 60, 77 | domain: ~c"nerves-1234.local", 78 | data: {192, 168, 0, 100} 79 | ) 80 | ], 81 | last_gc: 0 82 | } 83 | end 84 | 85 | test "doesn't cache non-mDNS records" do 86 | cache = 87 | Cache.new() 88 | |> Cache.insert( 89 | 0, 90 | dns_rr( 91 | class: :in, 92 | type: :mx, 93 | domain: ~c"nerves-1234.local", 94 | data: {192, 168, 0, 100} 95 | ) 96 | ) 97 | 98 | assert cache == Cache.new() 99 | end 100 | 101 | test "limits the number of records" do 102 | final_cache = 103 | Enum.reduce(1..1000, Cache.new(), fn i, cache -> 104 | Cache.insert( 105 | cache, 106 | 0, 107 | dns_rr(class: :in, type: :a, domain: ~c"nerves-#{i}.local", data: {1, 2, 3, 4}) 108 | ) 109 | end) 110 | 111 | assert Enum.count(final_cache.records) == 200 112 | end 113 | 114 | test "can insert many records at a time" do 115 | cache = 116 | Cache.new() 117 | |> Cache.insert_many(0, [@test_a_record, @test_aaaa_record]) 118 | 119 | assert cache == %Cache{records: [@test_aaaa_record, @test_a_record], last_gc: 0} 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /test/mdns_lite/client_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.ClientTest do 6 | use ExUnit.Case 7 | 8 | alias MdnsLite.Client 9 | 10 | test "encoding a-record queries" do 11 | query = Client.query_a("nerves.local") 12 | 13 | assert Client.encode(query) == 14 | <<0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6E, 0x65, 0x72, 15 | 0x76, 0x65, 0x73, 0x5, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x0, 0x0, 0x1, 0x0, 0x1>> 16 | 17 | # Unicast bit should be set - second last byte 18 | assert Client.encode(query, unicast: true) == 19 | <<0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6E, 0x65, 0x72, 20 | 0x76, 0x65, 0x73, 0x5, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x0, 0x0, 0x1, 0x80, 0x1>> 21 | end 22 | 23 | test "encoding aaaa-record queries" do 24 | query = Client.query_aaaa("nerves.local") 25 | 26 | assert Client.encode(query) == 27 | <<0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6E, 0x65, 0x72, 28 | 0x76, 0x65, 0x73, 0x5, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x0, 0x0, 0x1C, 0x0, 0x1>> 29 | 30 | # Unicast bit should be set - second last byte 31 | assert Client.encode(query, unicast: true) == 32 | <<0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x6E, 0x65, 0x72, 33 | 0x76, 0x65, 0x73, 0x5, 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x0, 0x0, 0x1C, 0x80, 0x1>> 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/mdns_lite/core_monitor_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2024 Kevin Schweikert 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.CoreMonitorTest do 7 | use ExUnit.Case, async: true 8 | 9 | alias MdnsLite.CoreMonitor 10 | 11 | test "adding IPs" do 12 | state = 13 | CoreMonitor.init([]) 14 | |> CoreMonitor.set_ip_list("eth0", [{1, 2, 3, 4}, {1, 2, 3, 4, 5, 6, 7, 8}]) 15 | |> CoreMonitor.set_ip_list("wlan0", [{10, 11, 12, 13}, {14, 15, 16, 17}]) 16 | 17 | # IPv4 filtering is on by default 18 | assert state.todo == [ 19 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {1, 2, 3, 4}]}, 20 | {MdnsLite.ResponderSupervisor, :start_child, ["wlan0", {10, 11, 12, 13}]}, 21 | {MdnsLite.ResponderSupervisor, :start_child, ["wlan0", {14, 15, 16, 17}]} 22 | ] 23 | end 24 | 25 | test "removing IPs" do 26 | state = 27 | CoreMonitor.init([]) 28 | |> CoreMonitor.set_ip_list("eth0", [{1, 2, 3, 4}, {5, 6, 7, 8}]) 29 | |> CoreMonitor.set_ip_list("eth0", [{5, 6, 7, 8}]) 30 | 31 | assert state.todo == [ 32 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {1, 2, 3, 4}]}, 33 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {5, 6, 7, 8}]}, 34 | {MdnsLite.ResponderSupervisor, :stop_child, ["eth0", {1, 2, 3, 4}]} 35 | ] 36 | end 37 | 38 | test "applying the todo list works" do 39 | {:ok, agent} = Agent.start_link(fn -> 0 end) 40 | 41 | state = 42 | CoreMonitor.init([]) 43 | |> Map.put(:todo, [ 44 | {Agent, :update, [agent, fn x -> x + 1 end]}, 45 | {Agent, :update, [agent, fn x -> x + 1 end]} 46 | ]) 47 | |> CoreMonitor.flush_todo_list() 48 | 49 | assert state.todo == [] 50 | assert Agent.get(agent, fn x -> x end) == 2 51 | end 52 | 53 | test "filtering interfaces" do 54 | state = 55 | CoreMonitor.init(excluded_ifnames: ["wlan0"]) 56 | |> CoreMonitor.set_ip_list("eth0", [{1, 2, 3, 4}, {1, 2, 3, 4, 5, 6, 7, 8}]) 57 | |> CoreMonitor.set_ip_list("wlan0", [{10, 11, 12, 13}, {14, 15, 16, 17}]) 58 | 59 | # IPv4 filtering is on by default 60 | assert state.todo == [ 61 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {1, 2, 3, 4}]} 62 | ] 63 | end 64 | 65 | test "allowing IPv6" do 66 | state = 67 | CoreMonitor.init(ipv4_only: false) 68 | |> CoreMonitor.set_ip_list("eth0", [{1, 2, 3, 4}, {1, 2, 3, 4, 5, 6, 7, 8}]) 69 | 70 | # IPv4 filtering is on by default 71 | assert state.todo == [ 72 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {1, 2, 3, 4}]}, 73 | {MdnsLite.ResponderSupervisor, :start_child, ["eth0", {1, 2, 3, 4, 5, 6, 7, 8}]} 74 | ] 75 | end 76 | 77 | test "remove unset ifnames" do 78 | state = 79 | CoreMonitor.init([]) 80 | |> CoreMonitor.set_ip_list("eth0", [{1, 2, 3, 4}, {1, 2, 3, 4, 5, 6, 7, 8}]) 81 | |> CoreMonitor.set_ip_list("wlan0", [{10, 11, 12, 13}, {14, 15, 16, 17}]) 82 | |> CoreMonitor.flush_todo_list() 83 | 84 | state = 85 | state 86 | |> CoreMonitor.set_ip_list("wlan0", [{10, 11, 12, 13}]) 87 | |> CoreMonitor.unset_remaining_ifnames(["wlan0"]) 88 | 89 | # IPv4 filtering is on by default 90 | assert state.todo == [ 91 | {MdnsLite.ResponderSupervisor, :stop_child, ["wlan0", {14, 15, 16, 17}]}, 92 | {MdnsLite.ResponderSupervisor, :stop_child, ["eth0", {1, 2, 3, 4}]} 93 | ] 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/mdns_lite/dns_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.DNSTest do 6 | use ExUnit.Case 7 | 8 | import MdnsLite.DNS 9 | alias MdnsLite.DNS 10 | 11 | test "encoding and decoding the Elgato packet" do 12 | encoded = 13 | <<0, 0, 132, 0, 0, 0, 0, 1, 0, 0, 0, 6, 4, 95, 101, 108, 103, 4, 95, 116, 99, 112, 5, 108, 14 | 111, 99, 97, 108, 0, 0, 12, 0, 1, 0, 0, 17, 148, 0, 24, 21, 69, 108, 103, 97, 116, 111, 15 | 32, 75, 101, 121, 32, 76, 105, 103, 104, 116, 32, 57, 57, 51, 66, 192, 12, 21, 101, 108, 16 | 103, 97, 116, 111, 45, 107, 101, 121, 45, 108, 105, 103, 104, 116, 45, 57, 57, 51, 98, 17 | 192, 22, 0, 1, 128, 1, 0, 0, 0, 120, 0, 4, 192, 168, 3, 39, 192, 63, 0, 28, 128, 1, 0, 0, 18 | 0, 120, 0, 16, 254, 128, 0, 0, 0, 0, 0, 0, 62, 106, 157, 255, 254, 20, 213, 105, 192, 39, 19 | 0, 33, 128, 1, 0, 0, 0, 120, 0, 8, 0, 0, 0, 0, 35, 163, 192, 63, 192, 39, 0, 16, 128, 1, 20 | 0, 0, 17, 148, 0, 74, 9, 109, 102, 61, 69, 108, 103, 97, 116, 111, 5, 100, 116, 61, 53, 21 | 51, 20, 105, 100, 61, 51, 67, 58, 54, 65, 58, 57, 68, 58, 49, 52, 58, 68, 53, 58, 54, 57, 22 | 29, 109, 100, 61, 69, 108, 103, 97, 116, 111, 32, 75, 101, 121, 32, 76, 105, 103, 104, 23 | 116, 32, 50, 48, 71, 65, 75, 57, 57, 48, 49, 6, 112, 118, 61, 49, 46, 48, 192, 63, 0, 47, 24 | 128, 1, 0, 0, 0, 120, 0, 8, 192, 63, 0, 4, 64, 0, 0, 8, 192, 39, 0, 47, 128, 1, 0, 0, 0, 25 | 120, 0, 9, 192, 39, 0, 5, 0, 0, 128, 0, 64>> 26 | 27 | decoded = 28 | dns_rec( 29 | header: 30 | dns_header( 31 | id: 0, 32 | qr: true, 33 | opcode: :query, 34 | aa: true, 35 | tc: false, 36 | rd: false, 37 | ra: false, 38 | pr: false, 39 | rcode: 0 40 | ), 41 | anlist: [ 42 | dns_rr( 43 | domain: ~c"_elg._tcp.local", 44 | type: :ptr, 45 | class: :in, 46 | ttl: 4500, 47 | data: ~c"Elgato Key Light 993B._elg._tcp.local" 48 | ) 49 | ], 50 | arlist: [ 51 | dns_rr( 52 | domain: ~c"elgato-key-light-993b.local", 53 | type: :a, 54 | class: :in, 55 | ttl: 120, 56 | data: {192, 168, 3, 39}, 57 | func: true 58 | ), 59 | dns_rr( 60 | domain: ~c"elgato-key-light-993b.local", 61 | type: :aaaa, 62 | class: :in, 63 | ttl: 120, 64 | data: {65152, 0, 0, 0, 15978, 40447, 65044, 54633}, 65 | func: true 66 | ), 67 | dns_rr( 68 | domain: ~c"Elgato Key Light 993B._elg._tcp.local", 69 | type: :srv, 70 | class: :in, 71 | ttl: 120, 72 | data: {0, 0, 9123, ~c"elgato-key-light-993b.local"}, 73 | func: true 74 | ), 75 | dns_rr( 76 | domain: ~c"Elgato Key Light 993B._elg._tcp.local", 77 | type: :txt, 78 | class: :in, 79 | ttl: 4500, 80 | data: [ 81 | ~c"mf=Elgato", 82 | ~c"dt=53", 83 | ~c"id=3C:6A:9D:14:D5:69", 84 | ~c"md=Elgato Key Light 20GAK9901", 85 | ~c"pv=1.0" 86 | ], 87 | func: true 88 | ), 89 | dns_rr( 90 | domain: ~c"elgato-key-light-993b.local", 91 | type: 47, 92 | class: :in, 93 | ttl: 120, 94 | data: <<192, 63, 0, 4, 64, 0, 0, 8>>, 95 | func: true 96 | ), 97 | dns_rr( 98 | domain: ~c"Elgato Key Light 993B._elg._tcp.local", 99 | type: 47, 100 | class: :in, 101 | ttl: 120, 102 | data: <<192, 39, 0, 5, 0, 0, 128, 0, 64>>, 103 | func: true 104 | ) 105 | ] 106 | ) 107 | 108 | assert {:ok, decoded} == DNS.decode(encoded) 109 | assert encoded == DNS.encode(decoded) 110 | end 111 | 112 | test "encoding and decoding a query" do 113 | encoded = 114 | <<0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 11, 110, 101, 114, 118, 101, 115, 45, 49, 50, 51, 52, 115 | 5, 108, 111, 99, 97, 108, 0, 0, 1, 0, 1>> 116 | 117 | decoded = 118 | dns_rec( 119 | header: 120 | dns_header( 121 | id: 0, 122 | qr: false, 123 | opcode: :query, 124 | aa: false, 125 | tc: false, 126 | rd: false, 127 | ra: false, 128 | pr: false, 129 | rcode: 0 130 | ), 131 | qdlist: [ 132 | dns_query(class: :in, type: :a, domain: ~c"nerves-1234.local", unicast_response: false) 133 | ] 134 | ) 135 | 136 | assert {:ok, decoded} == DNS.decode(encoded) 137 | assert encoded == DNS.encode(decoded) 138 | end 139 | 140 | test "encoding and decoding the unicast_response flag" do 141 | encoded = 142 | <<0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 11, 110, 101, 114, 118, 101, 115, 45, 49, 50, 51, 52, 143 | 5, 108, 111, 99, 97, 108, 0, 0, 1, 128, 1>> 144 | 145 | decoded = 146 | dns_rec( 147 | header: 148 | dns_header( 149 | id: 0, 150 | qr: false, 151 | opcode: :query, 152 | aa: false, 153 | tc: false, 154 | rd: false, 155 | ra: false, 156 | pr: false, 157 | rcode: 0 158 | ), 159 | qdlist: [ 160 | dns_query(class: :in, type: :a, domain: ~c"nerves-1234.local", unicast_response: true) 161 | ] 162 | ) 163 | 164 | assert {:ok, decoded} == DNS.decode(encoded) 165 | assert encoded == DNS.encode(decoded) 166 | end 167 | 168 | describe "pretty/1 for rr" do 169 | test "a" do 170 | assert pretty( 171 | dns_rr( 172 | class: :in, 173 | type: :a, 174 | ttl: 120, 175 | domain: ~c"nerves-1234.local", 176 | data: :ipv4_address 177 | ) 178 | ) == "nerves-1234.local: type A, class IN, ttl 120, addr " 179 | 180 | assert pretty( 181 | dns_rr( 182 | class: :in, 183 | type: :a, 184 | ttl: 120, 185 | domain: ~c"nerves-1234.local", 186 | data: {1, 2, 3, 4} 187 | ) 188 | ) == "nerves-1234.local: type A, class IN, ttl 120, addr 1.2.3.4" 189 | end 190 | 191 | test "aaaa" do 192 | assert pretty( 193 | dns_rr( 194 | class: :in, 195 | type: :aaaa, 196 | ttl: 120, 197 | domain: ~c"nerves-1234.local", 198 | data: :ipv6_address 199 | ) 200 | ) == "nerves-1234.local: type AAAA, class IN, ttl 120, addr " 201 | 202 | assert pretty( 203 | dns_rr( 204 | class: :in, 205 | type: :aaaa, 206 | ttl: 120, 207 | domain: ~c"nerves-1234.local", 208 | data: {65152, 0, 0, 0, 3297, 21943, 7498, 1443} 209 | ) 210 | ) == "nerves-1234.local: type AAAA, class IN, ttl 120, addr fe80::ce1:55b7:1d4a:5a3" 211 | end 212 | 213 | test "ptr" do 214 | assert pretty( 215 | assert dns_rr( 216 | class: :in, 217 | type: :ptr, 218 | ttl: 120, 219 | domain: :ipv4_arpa_address, 220 | data: ~c"nerves-1234.local" 221 | ) 222 | ) == ".in-addr.arpa: type PTR, class IN, ttl 120, nerves-1234.local" 223 | end 224 | 225 | test "txt" do 226 | assert pretty( 227 | dns_rr( 228 | domain: ~c"nerves-21a5._http._tcp.local", 229 | type: :txt, 230 | class: :in, 231 | ttl: 120, 232 | data: ["key1=1", "key2=2"] 233 | ) 234 | ) == "nerves-21a5._http._tcp.local: type TXT, class IN, ttl 120, key1=1, key2=2" 235 | end 236 | 237 | test "srv" do 238 | assert pretty( 239 | dns_rr( 240 | domain: ~c"nerves-21a5._http._tcp.local", 241 | type: :srv, 242 | class: :in, 243 | ttl: 120, 244 | data: {0, 0, 80, ~c"nerves-21a5.local."} 245 | ) 246 | ) == 247 | "nerves-21a5._http._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 80, nerves-21a5.local." 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /test/mdns_lite/info_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.InfoTest do 6 | use ExUnit.Case 7 | import ExUnit.CaptureIO 8 | 9 | alias MdnsLite.Info 10 | 11 | test "tables match config" do 12 | out = capture_io(&Info.dump_records/0) 13 | 14 | {:ok, name} = :inet.gethostname() 15 | 16 | # The lines aren't in guaranteed to be in this order, so check one by one 17 | assert out =~ ".in-addr.arpa: type PTR, class IN, ttl 120, #{name}.local" 18 | assert out =~ ".ip6.arpa: type PTR, class IN, ttl 120, #{name}.local" 19 | 20 | assert out =~ 21 | "#{name}._http._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 80, #{name}.local." 22 | 23 | assert out =~ "#{name}._http._tcp.local: type TXT, class IN, ttl 120, key=value" 24 | 25 | assert out =~ 26 | "#{name}._ssh._tcp.local: type SRV, class IN, ttl 120, priority 0, weight 0, port 22, #{name}.local." 27 | 28 | assert out =~ "#{name}._ssh._tcp.local: type TXT, class IN, ttl 120" 29 | assert out =~ "#{name}.local: type A, class IN, ttl 120, addr " 30 | assert out =~ "#{name}.local: type AAAA, class IN, ttl 120, addr " 31 | assert out =~ "_http._tcp.local: type PTR, class IN, ttl 120, #{name}._http._tcp.local" 32 | assert out =~ "_services._dns-sd._udp.local: type PTR, class IN, ttl 120, _http._tcp.local" 33 | assert out =~ "_services._dns-sd._udp.local: type PTR, class IN, ttl 120, _ssh._tcp.local" 34 | assert out =~ "_ssh._tcp.local: type PTR, class IN, ttl 120, #{name}._ssh._tcp.local" 35 | assert out =~ "nerves.local: type A, class IN, ttl 120, addr " 36 | assert out =~ "nerves.local: type AAAA, class IN, ttl 120, addr " 37 | 38 | line_count = out |> String.split("\n") |> length() 39 | assert line_count == 16 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/mdns_lite/mix/tasks/install_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Lee Nussbaum 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | 6 | defmodule MdnsLite.Mix.Tasks.InstallTest do 7 | use ExUnit.Case, async: true 8 | import Igniter.Test 9 | 10 | test "installer modifies config.exs but warns if no target.exs is present" do 11 | test_project( 12 | files: %{ 13 | "config/config.exs" => """ 14 | import Config 15 | config :logger, level: :info 16 | config :other_thing, foo: :bar 17 | """ 18 | } 19 | ) 20 | |> Igniter.compose_task("mdns_lite.install", []) 21 | |> assert_has_patch("config/config.exs", """ 22 | + |config :mdns_lite, 23 | + | host: [hostname: "nerves"], 24 | + | ttl: 120, 25 | + | services: [ 26 | + | %{protocol: "ssh", port: 22, transport: "tcp"}, 27 | + | %{protocol: "sftp-ssh", port: 22, transport: "tcp"}, 28 | + | %{protocol: "epmd", port: 4369, transport: "tcp"} 29 | + | ] 30 | + | 31 | """) 32 | |> assert_has_notice(""" 33 | The defaults for `mix mdns_lite.install` are intended for Nerves projects. Please visit 34 | its README at https://hexdocs.pm/mdns_lite/readme.html for an overview of usage. 35 | """) 36 | end 37 | 38 | test "installer adds default mdns_lite values for target.exs" do 39 | test_project( 40 | files: %{ 41 | "config/target.exs" => """ 42 | import Config 43 | 44 | config :other_thing, foo: :bar 45 | """ 46 | } 47 | ) 48 | |> Igniter.compose_task("mdns_lite.install", []) 49 | |> assert_has_patch("config/target.exs", """ 50 | + |config :mdns_lite, 51 | + | host: [hostname: "nerves"], 52 | + | ttl: 120, 53 | + | services: [ 54 | + | %{protocol: "ssh", port: 22, transport: "tcp"}, 55 | + | %{protocol: "sftp-ssh", port: 22, transport: "tcp"}, 56 | + | %{protocol: "epmd", port: 4369, transport: "tcp"} 57 | + | ] 58 | """) 59 | end 60 | 61 | test "installer leaves mdns values in place if already present" do 62 | test_project( 63 | files: %{ 64 | "config/target.exs" => """ 65 | import Config 66 | 67 | config :mdns_lite, 68 | host: [hostname: "my-nerves-device"], 69 | services: [ 70 | %{port: 2222, protocol: "ssh", transport: "tcp"}, 71 | %{port: 2222, protocol: "sftp-ssh", transport: "tcp"}, 72 | %{port: 4369, protocol: "epmd", transport: "tcp"} 73 | ] 74 | """ 75 | } 76 | ) 77 | |> Igniter.compose_task("mdns_lite.install", []) 78 | |> assert_has_patch("config/target.exs", ~S""" 79 | - | ] 80 | + | ], 81 | + | ttl: 120 82 | """) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/mdns_lite/options_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2021 Mat Trudel 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.OptionsTest do 7 | use ExUnit.Case, async: false 8 | 9 | import ExUnit.CaptureLog 10 | 11 | alias MdnsLite.Options 12 | 13 | test "default options" do 14 | {:ok, hostname} = :inet.gethostname() 15 | 16 | expected_if_monitor = 17 | if Version.match?(System.version(), "~> 1.11"), 18 | do: MdnsLite.VintageNetMonitor, 19 | else: MdnsLite.InetMonitor 20 | 21 | assert Options.new() == %Options{ 22 | dot_local_names: ["#{hostname}.local"], 23 | hosts: ["#{hostname}"], 24 | services: MapSet.new(), 25 | ttl: 120, 26 | instance_name: :unspecified, 27 | if_monitor: expected_if_monitor 28 | } 29 | end 30 | 31 | test "warns on host key" do 32 | log = 33 | capture_log(fn -> 34 | opts = Options.new(host: "old_way") 35 | assert opts.hosts == ["old_way"] 36 | end) 37 | 38 | assert log =~ "deprecated" 39 | end 40 | 41 | test "wraps non-list hosts" do 42 | opts = Options.new(hosts: "not_list") 43 | assert opts.hosts == ["not_list"] 44 | end 45 | 46 | test "hosts lists work" do 47 | opts = Options.new(hosts: [:hostname, "alias"]) 48 | {:ok, hostname} = :inet.gethostname() 49 | assert opts.hosts == [to_string(hostname), "alias"] 50 | end 51 | 52 | test "add and remove a single mdns service" do 53 | options = Options.new() 54 | 55 | assert Options.get_services(options) == [] 56 | 57 | options = 58 | Options.add_service(options, %{ 59 | id: :ssh_service, 60 | instance_name: "banana", 61 | protocol: "ssh", 62 | transport: "tcp", 63 | port: 22 64 | }) 65 | 66 | assert Options.get_services(options) == [ 67 | %{ 68 | id: :ssh_service, 69 | instance_name: "banana", 70 | port: 22, 71 | priority: 0, 72 | txt_payload: [], 73 | type: "_ssh._tcp", 74 | weight: 0 75 | } 76 | ] 77 | 78 | options = Options.remove_service_by_id(options, :ssh_service) 79 | assert Options.get_services(options) == [] 80 | end 81 | 82 | test "can set hostname string" do 83 | host = "howdy" 84 | 85 | options = 86 | Options.new() 87 | |> Options.set_hosts([host]) 88 | 89 | assert options.hosts == [host] 90 | end 91 | 92 | test "can set instance name" do 93 | instance_name = "My Device" 94 | 95 | options = 96 | Options.new() 97 | |> Options.set_instance_name(instance_name) 98 | 99 | assert options.instance_name == instance_name 100 | end 101 | 102 | test "can add hosts" do 103 | host = "howdy" 104 | host_alias = "partner" 105 | 106 | options = 107 | Options.new() 108 | |> Options.set_hosts([host]) 109 | |> Options.add_host(host_alias) 110 | 111 | assert options.hosts == [host, host_alias] 112 | end 113 | 114 | test "fails with invalid host" do 115 | options = Options.new() 116 | 117 | assert_raise RuntimeError, fn -> Options.set_hosts(options, [:wat]) end 118 | end 119 | 120 | describe "service normalization" do 121 | test "converts names to ids with warning" do 122 | log = 123 | capture_log(fn -> 124 | {:ok, normalized} = 125 | Options.normalize_service(%{ 126 | name: "name", 127 | port: 22, 128 | type: "_ssh._tcp" 129 | }) 130 | 131 | assert normalized.id == "name" 132 | end) 133 | 134 | assert log =~ "deprecated" 135 | end 136 | 137 | test "unspecified id is filled in" do 138 | {:ok, normalized} = 139 | Options.normalize_service(%{ 140 | port: 22, 141 | type: "_ssh._tcp" 142 | }) 143 | 144 | assert normalized.id == :unspecified 145 | end 146 | 147 | test "port required" do 148 | assert Options.normalize_service(%{id: :id, type: "_ssh._tcp"}) == 149 | {:error, "Specify a port"} 150 | end 151 | 152 | test "converts protocol and transport to a type" do 153 | {:ok, normalized} = 154 | Options.normalize_service(%{id: :id, port: 22, protocol: "ssh", transport: "tcp"}) 155 | 156 | assert normalized.type == "_ssh._tcp" 157 | end 158 | 159 | test "type or protocol/transport required" do 160 | assert Options.normalize_service(%{id: :id, port: 22}) == 161 | {:error, "Specify either 1. :protocol and :transport or 2. :type"} 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/mdns_lite/responder_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Jon Carstens 2 | # SPDX-FileCopyrightText: 2024 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.ResponderTest do 7 | use ExUnit.Case, async: false 8 | 9 | alias MdnsLite.Responder 10 | 11 | test "stopping a nonexistent responder" do 12 | Responder.stop_server("no_exist", {1, 2, 3, 4}) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/mdns_lite/table/builder_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.Table.BuilderTest do 6 | use ExUnit.Case 7 | 8 | import MdnsLite.DNS 9 | alias MdnsLite.Options 10 | alias MdnsLite.Table.Builder 11 | 12 | doctest MdnsLite.Table.Builder 13 | 14 | test "Adds A and AAAA records" do 15 | config = %Options{} |> Options.add_hosts(["nerves-1234"]) 16 | table = Builder.from_options(config) 17 | 18 | assert dns_rr( 19 | class: :in, 20 | type: :a, 21 | ttl: 120, 22 | domain: ~c"nerves-1234.local", 23 | data: :ipv4_address 24 | ) in table 25 | 26 | assert dns_rr( 27 | class: :in, 28 | type: :aaaa, 29 | ttl: 120, 30 | domain: ~c"nerves-1234.local", 31 | data: :ipv6_address 32 | ) in table 33 | end 34 | 35 | test "Adds PTR records" do 36 | config = %Options{} |> Options.add_hosts(["nerves-1234"]) 37 | table = Builder.from_options(config) 38 | 39 | assert dns_rr( 40 | class: :in, 41 | type: :ptr, 42 | ttl: 120, 43 | domain: :ipv4_arpa_address, 44 | data: ~c"nerves-1234.local" 45 | ) in table 46 | 47 | assert dns_rr( 48 | class: :in, 49 | type: :ptr, 50 | ttl: 120, 51 | domain: :ipv6_arpa_address, 52 | data: ~c"nerves-1234.local" 53 | ) in table 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/mdns_lite/table_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # SPDX-FileCopyrightText: 2021 Mat Trudel 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule MdnsLite.TableTest do 7 | use ExUnit.Case 8 | 9 | import MdnsLite.DNS 10 | 11 | alias MdnsLite.Options 12 | alias MdnsLite.Table 13 | 14 | doctest MdnsLite.Table 15 | 16 | defp test_config() do 17 | %Options{} 18 | |> Options.add_hosts(["nerves-21a5", "nerves"]) 19 | |> Options.add_services([ 20 | %{ 21 | id: :http_service, 22 | txt_payload: ["key=value"], 23 | port: 80, 24 | priority: 0, 25 | protocol: "http", 26 | transport: "tcp", 27 | type: "_http._tcp", 28 | weight: 0 29 | }, 30 | %{ 31 | id: :ssh_service, 32 | txt_payload: [""], 33 | port: 22, 34 | priority: 0, 35 | protocol: "ssh", 36 | transport: "tcp", 37 | type: "_ssh._tcp", 38 | weight: 0 39 | } 40 | ]) 41 | end 42 | 43 | defp do_query(query, config \\ test_config()) do 44 | table = Table.Builder.from_options(config) 45 | if_info = %MdnsLite.IfInfo{ipv4_address: {192, 168, 9, 57}} 46 | answer_rr = Table.query(table, query, if_info) 47 | additional_rr = Table.additional_records(table, answer_rr, if_info) 48 | %{answer: answer_rr, additional: additional_rr} 49 | end 50 | 51 | test "responds to an A request" do 52 | query = dns_query(domain: ~c"nerves-21a5.local", type: :a, class: :in) 53 | 54 | result = %{ 55 | answer: [ 56 | dns_rr( 57 | domain: ~c"nerves-21a5.local", 58 | type: :a, 59 | class: :in, 60 | ttl: 120, 61 | data: {192, 168, 9, 57} 62 | ) 63 | ], 64 | additional: [] 65 | } 66 | 67 | assert do_query(query) == result 68 | end 69 | 70 | test "responds to an A request for the alias" do 71 | query = dns_query(domain: ~c"nerves.local", type: :a, class: :in) 72 | 73 | result = %{ 74 | answer: [ 75 | dns_rr(domain: ~c"nerves.local", type: :a, class: :in, ttl: 120, data: {192, 168, 9, 57}) 76 | ], 77 | additional: [] 78 | } 79 | 80 | assert do_query(query) == result 81 | end 82 | 83 | test "responds to a unicast A request" do 84 | query = dns_query(domain: ~c"nerves-21a5.local", type: :a, class: :in, unicast_response: true) 85 | 86 | result = %{ 87 | answer: [ 88 | dns_rr( 89 | domain: ~c"nerves-21a5.local", 90 | type: :a, 91 | class: :in, 92 | ttl: 120, 93 | data: {192, 168, 9, 57} 94 | ) 95 | ], 96 | additional: [] 97 | } 98 | 99 | assert do_query(query) == result 100 | end 101 | 102 | test "ignores A request for someone else" do 103 | query = 104 | dns_query(domain: ~c"someone-else.local", type: :a, class: :in, unicast_response: true) 105 | 106 | assert do_query(query) == %{answer: [], additional: []} 107 | end 108 | 109 | test "responds to a PTR request with a reverse lookup domain" do 110 | query = dns_query(domain: ~c"57.9.168.192.in-addr.arpa.", type: :ptr, class: :in) 111 | 112 | result = %{ 113 | answer: [ 114 | dns_rr( 115 | domain: ~c"57.9.168.192.in-addr.arpa.", 116 | type: :ptr, 117 | class: :in, 118 | ttl: 120, 119 | data: ~c"nerves-21a5.local" 120 | ) 121 | ], 122 | additional: [] 123 | } 124 | 125 | assert do_query(query) == result 126 | end 127 | 128 | test "responds to a PTR request with a specific domain" do 129 | test_domain = ~c"_http._tcp.local" 130 | query = dns_query(domain: test_domain, type: :ptr, class: :in) 131 | 132 | result = %{ 133 | answer: [ 134 | dns_rr( 135 | domain: test_domain, 136 | type: :ptr, 137 | class: :in, 138 | ttl: 120, 139 | data: ~c"nerves-21a5._http._tcp.local" 140 | ) 141 | ], 142 | additional: [ 143 | dns_rr( 144 | domain: ~c"nerves-21a5._http._tcp.local", 145 | type: :srv, 146 | class: :in, 147 | ttl: 120, 148 | data: {0, 0, 80, ~c"nerves-21a5.local."} 149 | ), 150 | dns_rr( 151 | domain: ~c"nerves-21a5._http._tcp.local", 152 | type: :txt, 153 | class: :in, 154 | ttl: 120, 155 | data: ["key=value"] 156 | ), 157 | dns_rr( 158 | domain: ~c"nerves-21a5.local", 159 | type: :a, 160 | class: :in, 161 | ttl: 120, 162 | data: {192, 168, 9, 57} 163 | ) 164 | ] 165 | } 166 | 167 | assert do_query(query) == result 168 | end 169 | 170 | test "responds to a PTR request with a specific domain using host-level instance name" do 171 | test_domain = ~c"_http._tcp.local" 172 | query = dns_query(domain: test_domain, type: :ptr, class: :in) 173 | 174 | result = %{ 175 | answer: [ 176 | dns_rr( 177 | domain: test_domain, 178 | type: :ptr, 179 | class: :in, 180 | ttl: 120, 181 | data: ~c"myidentifier._http._tcp.local" 182 | ) 183 | ], 184 | additional: [ 185 | dns_rr( 186 | domain: ~c"myidentifier._http._tcp.local", 187 | type: :srv, 188 | class: :in, 189 | ttl: 120, 190 | data: {0, 0, 80, ~c"nerves-21a5.local."} 191 | ), 192 | dns_rr( 193 | domain: ~c"myidentifier._http._tcp.local", 194 | type: :txt, 195 | class: :in, 196 | ttl: 120, 197 | data: ["key=value"] 198 | ), 199 | dns_rr( 200 | domain: ~c"nerves-21a5.local", 201 | type: :a, 202 | class: :in, 203 | ttl: 120, 204 | data: {192, 168, 9, 57} 205 | ) 206 | ] 207 | } 208 | 209 | config = 210 | %Options{} 211 | |> Options.add_hosts(["nerves-21a5", "nerves"]) 212 | |> Options.set_instance_name("myidentifier") 213 | |> Options.add_service(%{ 214 | id: :http_service, 215 | txt_payload: ["key=value"], 216 | port: 80, 217 | priority: 0, 218 | protocol: "http", 219 | transport: "tcp", 220 | type: "_http._tcp", 221 | weight: 0 222 | }) 223 | 224 | assert do_query(query, config) == result 225 | end 226 | 227 | test "responds to a PTR request with a specific domain using service-level instance name" do 228 | test_domain = ~c"_http._tcp.local" 229 | query = dns_query(domain: test_domain, type: :ptr, class: :in) 230 | 231 | result = %{ 232 | answer: [ 233 | dns_rr( 234 | domain: test_domain, 235 | type: :ptr, 236 | class: :in, 237 | ttl: 120, 238 | data: ~c"myidentifier._http._tcp.local" 239 | ) 240 | ], 241 | additional: [ 242 | dns_rr( 243 | domain: ~c"myidentifier._http._tcp.local", 244 | type: :srv, 245 | class: :in, 246 | ttl: 120, 247 | data: {0, 0, 80, ~c"nerves-21a5.local."} 248 | ), 249 | dns_rr( 250 | domain: ~c"myidentifier._http._tcp.local", 251 | type: :txt, 252 | class: :in, 253 | ttl: 120, 254 | data: ["key=value"] 255 | ), 256 | dns_rr( 257 | domain: ~c"nerves-21a5.local", 258 | type: :a, 259 | class: :in, 260 | ttl: 120, 261 | data: {192, 168, 9, 57} 262 | ) 263 | ] 264 | } 265 | 266 | config = 267 | %Options{} 268 | |> Options.add_hosts(["nerves-21a5", "nerves"]) 269 | |> Options.add_service(%{ 270 | id: :http_service, 271 | instance_name: "myidentifier", 272 | txt_payload: ["key=value"], 273 | port: 80, 274 | priority: 0, 275 | protocol: "http", 276 | transport: "tcp", 277 | type: "_http._tcp", 278 | weight: 0 279 | }) 280 | 281 | assert do_query(query, config) == result 282 | end 283 | 284 | test "responds to a PTR request with domain \'_services._dns-sd._udp.local\'" do 285 | test_domain = ~c"_services._dns-sd._udp.local" 286 | query = dns_query(domain: test_domain, type: :ptr, class: :in) 287 | 288 | result = %{ 289 | answer: [ 290 | dns_rr( 291 | domain: ~c"_services._dns-sd._udp.local", 292 | type: :ptr, 293 | class: :in, 294 | ttl: 120, 295 | data: ~c"_http._tcp.local" 296 | ), 297 | dns_rr( 298 | domain: ~c"_services._dns-sd._udp.local", 299 | type: :ptr, 300 | class: :in, 301 | ttl: 120, 302 | data: ~c"_ssh._tcp.local" 303 | ) 304 | ], 305 | additional: [] 306 | } 307 | 308 | assert do_query(query) == result 309 | end 310 | 311 | test "responds to an SRV request for a known service" do 312 | known_service = "nerves-21a5._http._tcp.local" 313 | query = dns_query(domain: to_charlist(known_service), type: :srv, class: :in) 314 | 315 | result = %{ 316 | answer: [ 317 | dns_rr( 318 | domain: ~c"nerves-21a5._http._tcp.local", 319 | type: :srv, 320 | class: :in, 321 | ttl: 120, 322 | data: {0, 0, 80, ~c"nerves-21a5.local."} 323 | ) 324 | ], 325 | additional: [ 326 | {:dns_rr, ~c"nerves-21a5.local", :a, :in, 0, 120, {192, 168, 9, 57}, :undefined, [], 327 | false} 328 | ] 329 | } 330 | 331 | assert do_query(query) == result 332 | end 333 | 334 | test "responds to an SRV request for a known service with host-level instance name" do 335 | known_service = "myidentifier._http._tcp.local" 336 | query = dns_query(domain: to_charlist(known_service), type: :srv, class: :in) 337 | 338 | result = %{ 339 | answer: [ 340 | dns_rr( 341 | domain: ~c"myidentifier._http._tcp.local", 342 | type: :srv, 343 | class: :in, 344 | ttl: 120, 345 | data: {0, 0, 80, ~c"nerves-21a5.local."} 346 | ) 347 | ], 348 | additional: [ 349 | {:dns_rr, ~c"nerves-21a5.local", :a, :in, 0, 120, {192, 168, 9, 57}, :undefined, [], 350 | false} 351 | ] 352 | } 353 | 354 | config = 355 | %Options{} 356 | |> Options.add_hosts(["nerves-21a5", "nerves"]) 357 | |> Options.set_instance_name("myidentifier") 358 | |> Options.add_service(%{ 359 | id: :http_service, 360 | txt_payload: ["key=value"], 361 | port: 80, 362 | priority: 0, 363 | protocol: "http", 364 | transport: "tcp", 365 | type: "_http._tcp", 366 | weight: 0 367 | }) 368 | 369 | assert do_query(query, config) == result 370 | end 371 | 372 | test "responds to an SRV request for a known service with service-level instance name" do 373 | known_service = "myidentifier._http._tcp.local" 374 | query = dns_query(domain: to_charlist(known_service), type: :srv, class: :in) 375 | 376 | result = %{ 377 | answer: [ 378 | dns_rr( 379 | domain: ~c"myidentifier._http._tcp.local", 380 | type: :srv, 381 | class: :in, 382 | ttl: 120, 383 | data: {0, 0, 80, ~c"nerves-21a5.local."} 384 | ) 385 | ], 386 | additional: [ 387 | {:dns_rr, ~c"nerves-21a5.local", :a, :in, 0, 120, {192, 168, 9, 57}, :undefined, [], 388 | false} 389 | ] 390 | } 391 | 392 | config = 393 | %Options{} 394 | |> Options.add_hosts(["nerves-21a5", "nerves"]) 395 | |> Options.add_service(%{ 396 | id: :http_service, 397 | instance_name: "myidentifier", 398 | txt_payload: ["key=value"], 399 | port: 80, 400 | priority: 0, 401 | protocol: "http", 402 | transport: "tcp", 403 | type: "_http._tcp", 404 | weight: 0 405 | }) 406 | 407 | assert do_query(query, config) == result 408 | end 409 | 410 | test "ignore SRV request without the instance name" do 411 | service_only = "_http._tcp.local" 412 | query = dns_query(domain: to_charlist(service_only), type: :srv, class: :in) 413 | 414 | assert do_query(query) == %{answer: [], additional: []} 415 | end 416 | end 417 | -------------------------------------------------------------------------------- /test/mdns_lite/utilities_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLite.UtilitiesTest do 6 | use ExUnit.Case 7 | 8 | alias MdnsLite.Utilities 9 | 10 | doctest MdnsLite.Utilities 11 | 12 | defp test_ifaddrs() do 13 | [ 14 | {~c"lo0", 15 | [ 16 | flags: [:up, :loopback, :running, :multicast], 17 | addr: {127, 0, 0, 1}, 18 | netmask: {255, 0, 0, 0}, 19 | addr: {0, 0, 0, 0, 0, 0, 0, 1}, 20 | netmask: {65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535}, 21 | addr: {65152, 0, 0, 0, 0, 0, 0, 1}, 22 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0} 23 | ]}, 24 | {~c"gif0", [flags: [:pointtopoint, :multicast]]}, 25 | {~c"stf0", [flags: []]}, 26 | {~c"XHC0", [flags: []]}, 27 | {~c"XHC1", [flags: []]}, 28 | {~c"XHC20", [flags: []]}, 29 | {~c"en0", 30 | [ 31 | flags: [:up, :broadcast, :running, :multicast], 32 | addr: {65152, 0, 0, 0, 3177, 34598, 19643, 57597}, 33 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0}, 34 | addr: {192, 168, 9, 213}, 35 | netmask: {255, 255, 255, 0}, 36 | broadaddr: {192, 168, 9, 255}, 37 | hwaddr: [140, 133, 144, 54, 173, 41] 38 | ]}, 39 | {~c"p2p0", 40 | [ 41 | flags: [:up, :broadcast, :running, :multicast], 42 | hwaddr: [14, 133, 144, 54, 173, 41] 43 | ]}, 44 | {~c"awdl0", 45 | [ 46 | flags: [:up, :broadcast, :running, :multicast], 47 | addr: {65152, 0, 0, 0, 50389, 24319, 65082, 34455}, 48 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0}, 49 | hwaddr: [198, 213, 94, 58, 134, 151] 50 | ]}, 51 | {~c"en1", 52 | [ 53 | flags: [:up, :broadcast, :running, :multicast], 54 | hwaddr: [106, 0, 181, 2, 88, 1] 55 | ]}, 56 | {~c"en2", 57 | [ 58 | flags: [:up, :broadcast, :running, :multicast], 59 | hwaddr: [106, 0, 181, 2, 88, 0] 60 | ]}, 61 | {~c"en3", 62 | [ 63 | flags: [:up, :broadcast, :running, :multicast], 64 | hwaddr: [106, 0, 181, 2, 88, 5] 65 | ]}, 66 | {~c"en4", 67 | [ 68 | flags: [:up, :broadcast, :running, :multicast], 69 | hwaddr: [106, 0, 181, 2, 88, 4] 70 | ]}, 71 | {~c"bridge0", [flags: [:broadcast, :multicast], hwaddr: [106, 0, 181, 2, 88, 1]]}, 72 | {~c"utun0", 73 | [ 74 | flags: [:up, :pointtopoint, :running, :multicast], 75 | addr: {65152, 0, 0, 0, 5736, 498, 36548, 10713}, 76 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0}, 77 | dstaddr: {20, 18, 22, 0} 78 | ]}, 79 | {~c"utun1", 80 | [ 81 | flags: [:up, :pointtopoint, :running, :multicast], 82 | addr: {65152, 0, 0, 0, 27257, 3319, 61087, 19401}, 83 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0}, 84 | dstaddr: {20, 18, 7, 0} 85 | ]}, 86 | {~c"en5", 87 | [ 88 | flags: [:up, :broadcast, :running, :multicast], 89 | addr: {65152, 0, 0, 0, 44766, 18687, 65024, 4386}, 90 | netmask: {65535, 65535, 65535, 65535, 0, 0, 0, 0}, 91 | hwaddr: [172, 222, 72, 0, 17, 34] 92 | ]} 93 | ] 94 | end 95 | 96 | test "ifaddrs_to_ip_list finds IP addresses" do 97 | assert Utilities.ifaddrs_to_ip_list(test_ifaddrs(), "en5") == [ 98 | {65152, 0, 0, 0, 44766, 18687, 65024, 4386} 99 | ] 100 | 101 | assert Utilities.ifaddrs_to_ip_list(test_ifaddrs(), "en0") == [ 102 | {65152, 0, 0, 0, 3177, 34598, 19643, 57597}, 103 | {192, 168, 9, 213} 104 | ] 105 | 106 | assert Utilities.ifaddrs_to_ip_list(test_ifaddrs(), "bridge0") == [] 107 | assert Utilities.ifaddrs_to_ip_list(test_ifaddrs(), "doesnt_exist0") == [] 108 | end 109 | 110 | test "can tell IPv4 and IPv6 apart" do 111 | assert Utilities.ip_family({192, 168, 9, 213}) == :inet 112 | assert Utilities.ip_family({65152, 0, 0, 0, 3177, 34598, 19643, 57597}) == :inet6 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/mdns_lite_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Frank Hunleth 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule MdnsLiteTest do 6 | use ExUnit.Case, async: false 7 | 8 | describe "set_hosts/1" do 9 | test "set hosts to something else" do 10 | :ok = MdnsLite.set_hosts(["pineapple"]) 11 | assert {:ok, {127, 0, 0, 1}} = MdnsLite.gethostbyname("pineapple.local", 0) 12 | assert {:error, :nxdomain} = MdnsLite.gethostbyname("nerves.local", 0) 13 | 14 | # Restore the default 15 | :ok = MdnsLite.set_hosts([:hostname, "nerves"]) 16 | end 17 | end 18 | 19 | describe "gethostbyname/1" do 20 | test "query from our configuration" do 21 | assert {:ok, {127, 0, 0, 1}} = MdnsLite.gethostbyname("nerves.local") 22 | 23 | {:ok, hostname} = :inet.gethostname() 24 | assert {:ok, {127, 0, 0, 1}} = MdnsLite.gethostbyname(to_string(hostname) <> ".local") 25 | end 26 | 27 | test "missing host" do 28 | assert {:error, :nxdomain} = MdnsLite.gethostbyname("definitely-not-on-network.local", 0) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Peter C. Marks 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | ExUnit.start() 6 | --------------------------------------------------------------------------------