├── .github ├── .gitignore ├── fetch-scripts.sh ├── install-hooks.sh └── workflows │ ├── api.yaml │ ├── codeql-analysis.yml │ ├── lint.yaml │ ├── release.yml │ ├── renovate-go-sum-fix.yaml │ ├── reuse.yml │ ├── test.yaml │ └── tidy-check.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .reuse └── dep5 ├── LICENSE ├── LICENSES └── MIT.txt ├── README.md ├── codecov.yml ├── config.go ├── conn.go ├── conn_test.go ├── errors.go ├── examples ├── query │ └── main.go └── server │ ├── main.go │ └── publish_ip │ └── main.go ├── go.mod ├── go.sum ├── mdns.go └── renovate.json /.github/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | .goassets 5 | -------------------------------------------------------------------------------- /.github/fetch-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | set -eu 15 | 16 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 17 | GOASSETS_PATH="${SCRIPT_PATH}/.goassets" 18 | 19 | GOASSETS_REF=${GOASSETS_REF:-master} 20 | 21 | if [ -d "${GOASSETS_PATH}" ]; then 22 | if ! git -C "${GOASSETS_PATH}" diff --exit-code; then 23 | echo "${GOASSETS_PATH} has uncommitted changes" >&2 24 | exit 1 25 | fi 26 | git -C "${GOASSETS_PATH}" fetch origin 27 | git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} 28 | git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} 29 | else 30 | git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" 31 | fi 32 | -------------------------------------------------------------------------------- /.github/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 15 | 16 | . ${SCRIPT_PATH}/fetch-scripts.sh 17 | 18 | cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" 19 | cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" 20 | cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" 21 | -------------------------------------------------------------------------------- /.github/workflows/api.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: API 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | check: 20 | uses: pion/.goassets/.github/workflows/api.reusable.yml@master 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: CodeQL 15 | 16 | on: 17 | workflow_dispatch: 18 | schedule: 19 | - cron: '23 5 * * 0' 20 | pull_request: 21 | branches: 22 | - master 23 | paths: 24 | - '**.go' 25 | 26 | jobs: 27 | analyze: 28 | uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Lint 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | lint: 20 | uses: pion/.goassets/.github/workflows/lint.reusable.yml@master 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Release 15 | on: 16 | push: 17 | tags: 18 | - 'v*' 19 | 20 | jobs: 21 | release: 22 | uses: pion/.goassets/.github/workflows/release.reusable.yml@master 23 | with: 24 | go-version: "1.24" # auto-update/latest-go-version 25 | -------------------------------------------------------------------------------- /.github/workflows/renovate-go-sum-fix.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Fix go.sum 15 | on: 16 | push: 17 | branches: 18 | - renovate/* 19 | 20 | jobs: 21 | fix: 22 | uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master 23 | secrets: 24 | token: ${{ secrets.PIONBOT_PRIVATE_KEY }} 25 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: REUSE Compliance Check 15 | 16 | on: 17 | push: 18 | pull_request: 19 | 20 | jobs: 21 | lint: 22 | uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Test 15 | on: 16 | push: 17 | branches: 18 | - master 19 | pull_request: 20 | 21 | jobs: 22 | test: 23 | uses: pion/.goassets/.github/workflows/test.reusable.yml@master 24 | strategy: 25 | matrix: 26 | go: ["1.24", "1.23"] # auto-update/supported-go-version-list 27 | fail-fast: false 28 | with: 29 | go-version: ${{ matrix.go }} 30 | secrets: inherit 31 | 32 | test-i386: 33 | uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master 34 | strategy: 35 | matrix: 36 | go: ["1.24", "1.23"] # auto-update/supported-go-version-list 37 | fail-fast: false 38 | with: 39 | go-version: ${{ matrix.go }} 40 | 41 | test-wasm: 42 | uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master 43 | with: 44 | go-version: "1.24" # auto-update/latest-go-version 45 | secrets: inherit 46 | -------------------------------------------------------------------------------- /.github/workflows/tidy-check.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Go mod tidy 15 | on: 16 | pull_request: 17 | push: 18 | branches: 19 | - master 20 | 21 | jobs: 22 | tidy: 23 | uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master 24 | with: 25 | go-version: "1.24" # auto-update/latest-go-version 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | ### JetBrains IDE ### 5 | ##################### 6 | .idea/ 7 | 8 | ### Emacs Temporary Files ### 9 | ############################# 10 | *~ 11 | 12 | ### Folders ### 13 | ############### 14 | bin/ 15 | vendor/ 16 | node_modules/ 17 | 18 | ### Files ### 19 | ############# 20 | *.ivf 21 | *.ogg 22 | tags 23 | cover.out 24 | *.sw[poe] 25 | *.wasm 26 | examples/sfu-ws/cert.pem 27 | examples/sfu-ws/key.pem 28 | wasm_exec.js 29 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | run: 5 | timeout: 5m 6 | 7 | linters-settings: 8 | govet: 9 | enable: 10 | - shadow 11 | misspell: 12 | locale: US 13 | exhaustive: 14 | default-signifies-exhaustive: true 15 | gomodguard: 16 | blocked: 17 | modules: 18 | - github.com/pkg/errors: 19 | recommendations: 20 | - errors 21 | forbidigo: 22 | analyze-types: true 23 | forbid: 24 | - ^fmt.Print(f|ln)?$ 25 | - ^log.(Panic|Fatal|Print)(f|ln)?$ 26 | - ^os.Exit$ 27 | - ^panic$ 28 | - ^print(ln)?$ 29 | - p: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ 30 | pkg: ^testing$ 31 | msg: "use testify/assert instead" 32 | varnamelen: 33 | max-distance: 12 34 | min-name-length: 2 35 | ignore-type-assert-ok: true 36 | ignore-map-index-ok: true 37 | ignore-chan-recv-ok: true 38 | ignore-decls: 39 | - i int 40 | - n int 41 | - w io.Writer 42 | - r io.Reader 43 | - b []byte 44 | 45 | linters: 46 | enable: 47 | - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers 48 | - bidichk # Checks for dangerous unicode character sequences 49 | - bodyclose # checks whether HTTP response body is closed successfully 50 | - containedctx # containedctx is a linter that detects struct contained context.Context field 51 | - contextcheck # check the function whether use a non-inherited context 52 | - cyclop # checks function and package cyclomatic complexity 53 | - decorder # check declaration order and count of types, constants, variables and functions 54 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 55 | - dupl # Tool for code clone detection 56 | - durationcheck # check for two durations multiplied together 57 | - err113 # Golang linter to check the errors handling expressions 58 | - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases 59 | - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. 60 | - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. 61 | - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 62 | - exhaustive # check exhaustiveness of enum switch statements 63 | - exportloopref # checks for pointers to enclosing loop variables 64 | - forbidigo # Forbids identifiers 65 | - forcetypeassert # finds forced type assertions 66 | - gci # Gci control golang package import order and make it always deterministic. 67 | - gochecknoglobals # Checks that no globals are present in Go code 68 | - gocognit # Computes and checks the cognitive complexity of functions 69 | - goconst # Finds repeated strings that could be replaced by a constant 70 | - gocritic # The most opinionated Go source code linter 71 | - gocyclo # Computes and checks the cyclomatic complexity of functions 72 | - godot # Check if comments end in a period 73 | - godox # Tool for detection of FIXME, TODO and other comment keywords 74 | - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification 75 | - gofumpt # Gofumpt checks whether code was gofumpt-ed. 76 | - goheader # Checks is file header matches to pattern 77 | - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports 78 | - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. 79 | - goprintffuncname # Checks that printf-like functions are named with `f` at the end 80 | - gosec # Inspects source code for security problems 81 | - gosimple # Linter for Go source code that specializes in simplifying a code 82 | - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 83 | - grouper # An analyzer to analyze expression groups. 84 | - importas # Enforces consistent import aliases 85 | - ineffassign # Detects when assignments to existing variables are not used 86 | - lll # Reports long lines 87 | - maintidx # maintidx measures the maintainability index of each function. 88 | - makezero # Finds slice declarations with non-zero initial length 89 | - misspell # Finds commonly misspelled English words in comments 90 | - nakedret # Finds naked returns in functions greater than a specified function length 91 | - nestif # Reports deeply nested if statements 92 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil. 93 | - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. 94 | - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity 95 | - noctx # noctx finds sending http request without context.Context 96 | - predeclared # find code that shadows one of Go's predeclared identifiers 97 | - revive # golint replacement, finds style mistakes 98 | - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks 99 | - stylecheck # Stylecheck is a replacement for golint 100 | - tagliatelle # Checks the struct tags. 101 | - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 102 | - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers 103 | - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code 104 | - unconvert # Remove unnecessary type conversions 105 | - unparam # Reports unused function parameters 106 | - unused # Checks Go code for unused constants, variables, functions and types 107 | - varnamelen # checks that the length of a variable's name matches its scope 108 | - wastedassign # wastedassign finds wasted assignment statements 109 | - whitespace # Tool for detection of leading and trailing whitespace 110 | disable: 111 | - depguard # Go linter that checks if package imports are in a list of acceptable packages 112 | - funlen # Tool for detection of long functions 113 | - gochecknoinits # Checks that no init functions are present in Go code 114 | - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. 115 | - interfacebloat # A linter that checks length of interface. 116 | - ireturn # Accept Interfaces, Return Concrete Types 117 | - mnd # An analyzer to detect magic numbers 118 | - nolintlint # Reports ill-formed or insufficient nolint directives 119 | - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test 120 | - prealloc # Finds slice declarations that could potentially be preallocated 121 | - promlinter # Check Prometheus metrics naming via promlint 122 | - rowserrcheck # checks whether Err of rows is checked successfully 123 | - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. 124 | - testpackage # linter that makes you use a separate _test package 125 | - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes 126 | - wrapcheck # Checks that errors returned from external packages are wrapped 127 | - wsl # Whitespace Linter - Forces you to use empty lines! 128 | 129 | issues: 130 | exclude-use-default: false 131 | exclude-dirs-use-default: false 132 | exclude-rules: 133 | # Allow complex tests and examples, better to be self contained 134 | - path: (examples|main\.go) 135 | linters: 136 | - gocognit 137 | - forbidigo 138 | - path: _test\.go 139 | linters: 140 | - gocognit 141 | 142 | # Allow forbidden identifiers in CLI commands 143 | - path: cmd 144 | linters: 145 | - forbidigo 146 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | builds: 5 | - skip: true 6 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Pion 3 | Source: https://github.com/pion/ 4 | 5 | Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json 6 | Copyright: 2023 The Pion community 7 | License: MIT 8 | 9 | Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt 10 | Copyright: 2023 The Pion community 11 | License: CC0-1.0 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Pion mDNS 4 |
5 |

6 |

A Go implementation of mDNS

7 |

8 | Pion mDNS 9 | join us on Discord Follow us on Bluesky 10 |
11 | GitHub Workflow Status 12 | Go Reference 13 | Coverage Status 14 | Go Report Card 15 | License: MIT 16 |

17 |
18 | 19 | Go mDNS implementation. The original user is Pion WebRTC, but we would love to see it work for everyone. 20 | 21 | ### Running Server 22 | For a mDNS server that responds to queries for `pion-test.local` 23 | ```sh 24 | go run examples/server/main.go 25 | ``` 26 | 27 | For a mDNS server that responds to queries for `pion-test.local` with a given address 28 | ```sh 29 | go run examples/server/publish_ip/main.go -ip=[IP] 30 | ``` 31 | If you don't set the `ip` parameter, "1.2.3.4" will be used instead. 32 | 33 | 34 | ### Running Client 35 | To query using Pion you can run the `query` example 36 | ```sh 37 | go run examples/query/main.go 38 | ``` 39 | 40 | You can use the macOS client 41 | ``` 42 | dns-sd -q pion-test.local 43 | ``` 44 | 45 | Or the avahi client 46 | ``` 47 | avahi-resolve -a pion-test.local 48 | ``` 49 | 50 | ### RFCs 51 | #### Implemented 52 | - **RFC 6762** [Multicast DNS][rfc6762] 53 | - **draft-ietf-rtcweb-mdns-ice-candidates-02** [Using Multicast DNS to protect privacy when exposing ICE candidates](https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-mdns-ice-candidates-02.html) 54 | 55 | [rfc6762]: https://tools.ietf.org/html/rfc6762 56 | 57 | ### Roadmap 58 | The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. 59 | 60 | ### Community 61 | Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). 62 | 63 | Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. 64 | 65 | We are always looking to support **your projects**. Please reach out if you have something to build! 66 | If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) 67 | 68 | ### Contributing 69 | Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible 70 | 71 | ### License 72 | MIT License - see [LICENSE](LICENSE) for full text -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # 6 | # SPDX-FileCopyrightText: 2023 The Pion community 7 | # SPDX-License-Identifier: MIT 8 | 9 | coverage: 10 | status: 11 | project: 12 | default: 13 | # Allow decreasing 2% of total coverage to avoid noise. 14 | threshold: 2% 15 | patch: 16 | default: 17 | target: 70% 18 | only_pulls: true 19 | 20 | ignore: 21 | - "examples/*" 22 | - "examples/**/*" 23 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package mdns 5 | 6 | import ( 7 | "net" 8 | "time" 9 | 10 | "github.com/pion/logging" 11 | ) 12 | 13 | const ( 14 | // DefaultAddressIPv4 is the default used by mDNS 15 | // and in most cases should be the address that the 16 | // ipv4.PacketConn passed to Server is bound to. 17 | DefaultAddressIPv4 = "224.0.0.0:5353" 18 | 19 | // DefaultAddressIPv6 is the default IPv6 address used 20 | // by mDNS and in most cases should be the address that 21 | // the ipv6.PacketConn passed to Server is bound to. 22 | DefaultAddressIPv6 = "[FF02::]:5353" 23 | ) 24 | 25 | // Config is used to configure a mDNS client or server. 26 | type Config struct { 27 | // Name is the name of the client/server used for logging purposes. 28 | Name string 29 | 30 | // QueryInterval controls how often we sends Queries until we 31 | // get a response for the requested name 32 | QueryInterval time.Duration 33 | 34 | // LocalNames are the names that we will generate answers for 35 | // when we get questions 36 | LocalNames []string 37 | 38 | // LocalAddress will override the published address with the given IP 39 | // when set. Otherwise, the automatically determined address will be used. 40 | LocalAddress net.IP 41 | 42 | LoggerFactory logging.LoggerFactory 43 | 44 | // IncludeLoopback will include loopback interfaces to be eligble for queries and answers. 45 | IncludeLoopback bool 46 | 47 | // Interfaces will override the interfaces used for queries and answers. 48 | Interfaces []net.Interface 49 | } 50 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package mdns 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/netip" 12 | "sync" 13 | "time" 14 | 15 | "github.com/pion/logging" 16 | "golang.org/x/net/dns/dnsmessage" 17 | "golang.org/x/net/ipv4" 18 | "golang.org/x/net/ipv6" 19 | ) 20 | 21 | // Conn represents a mDNS Server. 22 | type Conn struct { 23 | mu sync.RWMutex 24 | name string 25 | log logging.LeveledLogger 26 | 27 | multicastPktConnV4 ipPacketConn 28 | multicastPktConnV6 ipPacketConn 29 | dstAddr4 *net.UDPAddr 30 | dstAddr6 *net.UDPAddr 31 | 32 | unicastPktConnV4 ipPacketConn 33 | unicastPktConnV6 ipPacketConn 34 | 35 | queryInterval time.Duration 36 | localNames []string 37 | queries []*query 38 | ifaces map[int]netInterface 39 | 40 | closed chan interface{} 41 | } 42 | 43 | type query struct { 44 | nameWithSuffix string 45 | queryResultChan chan queryResult 46 | } 47 | 48 | type queryResult struct { 49 | answer dnsmessage.ResourceHeader 50 | addr netip.Addr 51 | } 52 | 53 | const ( 54 | defaultQueryInterval = time.Second 55 | destinationAddress4 = "224.0.0.251:5353" 56 | destinationAddress6 = "[FF02::FB]:5353" 57 | maxMessageRecords = 3 58 | responseTTL = 120 59 | // maxPacketSize is the maximum size of a mdns packet. 60 | // From RFC 6762: 61 | // Even when fragmentation is used, a Multicast DNS packet, including IP 62 | // and UDP headers, MUST NOT exceed 9000 bytes. 63 | // https://datatracker.ietf.org/doc/html/rfc6762#section-17 64 | maxPacketSize = 9000 65 | ) 66 | 67 | var ( 68 | errNoPositiveMTUFound = errors.New("no positive MTU found") 69 | errNoPacketConn = errors.New("must supply at least a multicast IPv4 or IPv6 PacketConn") 70 | errNoUsableInterfaces = errors.New("no usable interfaces found for mDNS") 71 | errFailedToClose = errors.New("failed to close mDNS Conn") 72 | ) 73 | 74 | type netInterface struct { 75 | net.Interface 76 | ipAddrs []netip.Addr 77 | supportsV4 bool 78 | supportsV6 bool 79 | } 80 | 81 | // Server establishes a mDNS connection over an existing conn. 82 | // Either one or both of the multicast packet conns should be provided. 83 | // The presence of each IP type of PacketConn will dictate what kinds 84 | // of questions are sent for queries. That is, if an ipv6.PacketConn is 85 | // provided, then AAAA questions will be sent. A questions will only be 86 | // sent if an ipv4.PacketConn is also provided. In the future, we may 87 | // add a QueryAddr method that allows specifying this more clearly. 88 | // 89 | //nolint:gocognit,gocyclo,cyclop,maintidx 90 | func Server( 91 | multicastPktConnV4 *ipv4.PacketConn, 92 | multicastPktConnV6 *ipv6.PacketConn, 93 | config *Config, 94 | ) (*Conn, error) { 95 | if config == nil { 96 | return nil, errNilConfig 97 | } 98 | loggerFactory := config.LoggerFactory 99 | if loggerFactory == nil { 100 | loggerFactory = logging.NewDefaultLoggerFactory() 101 | } 102 | log := loggerFactory.NewLogger("mdns") 103 | 104 | conn := &Conn{ 105 | queryInterval: defaultQueryInterval, 106 | log: log, 107 | closed: make(chan interface{}), 108 | } 109 | conn.name = config.Name 110 | if conn.name == "" { 111 | conn.name = fmt.Sprintf("%p", &conn) 112 | } 113 | 114 | if multicastPktConnV4 == nil && multicastPktConnV6 == nil { 115 | return nil, errNoPacketConn 116 | } 117 | 118 | ifaces := config.Interfaces 119 | if ifaces == nil { 120 | var err error 121 | ifaces, err = net.Interfaces() 122 | if err != nil { 123 | return nil, err 124 | } 125 | } 126 | 127 | var unicastPktConnV4 *ipv4.PacketConn 128 | { 129 | addr4, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0") 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | unicastConnV4, err := net.ListenUDP("udp4", addr4) 135 | if err != nil { 136 | log.Warnf( 137 | "[%s] failed to listen on unicast IPv4 %s: %s; will not be able to receive unicast responses on IPv4", 138 | conn.name, addr4, err, 139 | ) 140 | } else { 141 | unicastPktConnV4 = ipv4.NewPacketConn(unicastConnV4) 142 | } 143 | } 144 | 145 | var unicastPktConnV6 *ipv6.PacketConn 146 | { 147 | addr6, err := net.ResolveUDPAddr("udp6", "[::]:") 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | unicastConnV6, err := net.ListenUDP("udp6", addr6) 153 | if err != nil { 154 | log.Warnf( 155 | "[%s] failed to listen on unicast IPv6 %s: %s; will not be able to receive unicast responses on IPv6", 156 | conn.name, addr6, err, 157 | ) 158 | } else { 159 | unicastPktConnV6 = ipv6.NewPacketConn(unicastConnV6) 160 | } 161 | } 162 | 163 | multicastGroup4 := net.IPv4(224, 0, 0, 251) 164 | multicastGroupAddr4 := &net.UDPAddr{IP: multicastGroup4} 165 | 166 | // FF02::FB 167 | multicastGroup6 := net.IP{0xff, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfb} 168 | multicastGroupAddr6 := &net.UDPAddr{IP: multicastGroup6} 169 | 170 | inboundBufferSize := 0 171 | joinErrCount := 0 172 | ifacesToUse := make(map[int]netInterface, len(ifaces)) 173 | for i := range ifaces { 174 | ifc := ifaces[i] 175 | if !config.IncludeLoopback && ifc.Flags&net.FlagLoopback == net.FlagLoopback { 176 | continue 177 | } 178 | if ifc.Flags&net.FlagUp == 0 { 179 | continue 180 | } 181 | 182 | addrs, err := ifc.Addrs() 183 | if err != nil { 184 | continue 185 | } 186 | var supportsV4, supportsV6 bool 187 | ifcIPAddrs := make([]netip.Addr, 0, len(addrs)) 188 | for _, addr := range addrs { 189 | var ipToConv net.IP 190 | switch addr := addr.(type) { 191 | case *net.IPNet: 192 | ipToConv = addr.IP 193 | case *net.IPAddr: 194 | ipToConv = addr.IP 195 | default: 196 | continue 197 | } 198 | 199 | ipAddr, ok := netip.AddrFromSlice(ipToConv) 200 | if !ok { 201 | continue 202 | } 203 | if multicastPktConnV4 != nil { 204 | // don't want mapping since we also support IPv4/A 205 | ipAddr = ipAddr.Unmap() 206 | } 207 | ipAddr = addrWithOptionalZone(ipAddr, ifc.Name) 208 | 209 | if ipAddr.Is6() && !ipAddr.Is4In6() { 210 | supportsV6 = true 211 | } else { 212 | // we'll claim we support v4 but defer if we send it or not 213 | // based on IPv4-to-IPv6 mapping rules later (search for Is4In6 below) 214 | supportsV4 = true 215 | } 216 | ifcIPAddrs = append(ifcIPAddrs, ipAddr) 217 | } 218 | if !(supportsV4 || supportsV6) { 219 | continue 220 | } 221 | 222 | var atLeastOneJoin bool 223 | if supportsV4 && multicastPktConnV4 != nil { 224 | if err := multicastPktConnV4.JoinGroup(&ifc, multicastGroupAddr4); err == nil { 225 | atLeastOneJoin = true 226 | } 227 | } 228 | if supportsV6 && multicastPktConnV6 != nil { 229 | if err := multicastPktConnV6.JoinGroup(&ifc, multicastGroupAddr6); err == nil { 230 | atLeastOneJoin = true 231 | } 232 | } 233 | if !atLeastOneJoin { 234 | joinErrCount++ 235 | 236 | continue 237 | } 238 | 239 | ifacesToUse[ifc.Index] = netInterface{ 240 | Interface: ifc, 241 | ipAddrs: ifcIPAddrs, 242 | supportsV4: supportsV4, 243 | supportsV6: supportsV6, 244 | } 245 | if ifc.MTU > inboundBufferSize { 246 | inboundBufferSize = ifc.MTU 247 | } 248 | } 249 | 250 | if len(ifacesToUse) == 0 { 251 | return nil, errNoUsableInterfaces 252 | } 253 | if inboundBufferSize == 0 { 254 | return nil, errNoPositiveMTUFound 255 | } 256 | if inboundBufferSize > maxPacketSize { 257 | inboundBufferSize = maxPacketSize 258 | } 259 | if joinErrCount >= len(ifaces) { 260 | return nil, errJoiningMulticastGroup 261 | } 262 | 263 | dstAddr4, err := net.ResolveUDPAddr("udp4", destinationAddress4) 264 | if err != nil { 265 | return nil, err 266 | } 267 | 268 | dstAddr6, err := net.ResolveUDPAddr("udp6", destinationAddress6) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | var localNames []string 274 | for _, l := range config.LocalNames { 275 | localNames = append(localNames, l+".") 276 | } 277 | 278 | conn.dstAddr4 = dstAddr4 279 | conn.dstAddr6 = dstAddr6 280 | conn.localNames = localNames 281 | conn.ifaces = ifacesToUse 282 | 283 | if config.QueryInterval != 0 { 284 | conn.queryInterval = config.QueryInterval 285 | } 286 | 287 | if multicastPktConnV4 != nil { 288 | if err := multicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { 289 | conn.log.Warnf( 290 | "[%s] failed to SetControlMessage(ipv4.FlagInterface) on multicast IPv4 PacketConn %v", 291 | conn.name, err, 292 | ) 293 | } 294 | if err := multicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { 295 | conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagDst) on multicast IPv4 PacketConn %v", conn.name, err) 296 | } 297 | conn.multicastPktConnV4 = ipPacketConn4{conn.name, multicastPktConnV4, log} 298 | } 299 | if multicastPktConnV6 != nil { 300 | if err := multicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { 301 | conn.log.Warnf( 302 | "[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", 303 | conn.name, err, 304 | ) 305 | } 306 | if err := multicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { 307 | conn.log.Warnf( 308 | "[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", 309 | conn.name, err, 310 | ) 311 | } 312 | conn.multicastPktConnV6 = ipPacketConn6{conn.name, multicastPktConnV6, log} 313 | } 314 | if unicastPktConnV4 != nil { 315 | if err := unicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { 316 | conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", conn.name, err) 317 | } 318 | if err := unicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { 319 | conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", conn.name, err) 320 | } 321 | conn.unicastPktConnV4 = ipPacketConn4{conn.name, unicastPktConnV4, log} 322 | } 323 | if unicastPktConnV6 != nil { 324 | if err := unicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { 325 | conn.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", conn.name, err) 326 | } 327 | if err := unicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { 328 | conn.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", conn.name, err) 329 | } 330 | conn.unicastPktConnV6 = ipPacketConn6{conn.name, unicastPktConnV6, log} 331 | } 332 | 333 | if config.IncludeLoopback { //nolint:nestif 334 | // this is an efficient way for us to send ourselves a message faster instead of it going 335 | // further out into the network stack. 336 | if multicastPktConnV4 != nil { 337 | if err := multicastPktConnV4.SetMulticastLoopback(true); err != nil { 338 | conn.log.Warnf( 339 | //nolint:lll 340 | "[%s] failed to SetMulticastLoopback(true) on multicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", 341 | conn.name, err, 342 | ) 343 | } 344 | } 345 | if multicastPktConnV6 != nil { 346 | if err := multicastPktConnV6.SetMulticastLoopback(true); err != nil { 347 | conn.log.Warnf( 348 | //nolint:lll 349 | "[%s] failed to SetMulticastLoopback(true) on multicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", 350 | conn.name, err, 351 | ) 352 | } 353 | } 354 | if unicastPktConnV4 != nil { 355 | if err := unicastPktConnV4.SetMulticastLoopback(true); err != nil { 356 | conn.log.Warnf( 357 | //nolint:lll 358 | "[%s] failed to SetMulticastLoopback(true) on unicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", 359 | conn.name, err, 360 | ) 361 | } 362 | } 363 | if unicastPktConnV6 != nil { 364 | if err := unicastPktConnV6.SetMulticastLoopback(true); err != nil { 365 | conn.log.Warnf( 366 | //nolint:lll 367 | "[%s] failed to SetMulticastLoopback(true) on unicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", 368 | conn.name, err, 369 | ) 370 | } 371 | } 372 | } 373 | 374 | // https://www.rfc-editor.org/rfc/rfc6762.html#section-17 375 | // Multicast DNS messages carried by UDP may be up to the IP MTU of the 376 | // physical interface, less the space required for the IP header (20 377 | // bytes for IPv4; 40 bytes for IPv6) and the UDP header (8 bytes). 378 | started := make(chan struct{}) 379 | go conn.start(started, inboundBufferSize-20-8, config) 380 | <-started 381 | 382 | return conn, nil 383 | } 384 | 385 | // Close closes the mDNS Conn. 386 | func (c *Conn) Close() error { //nolint:cyclop 387 | select { 388 | case <-c.closed: 389 | return nil 390 | default: 391 | } 392 | 393 | // Once on go1.20, can use errors.Join 394 | var errs []error 395 | if c.multicastPktConnV4 != nil { 396 | if err := c.multicastPktConnV4.Close(); err != nil { 397 | errs = append(errs, err) 398 | } 399 | } 400 | 401 | if c.multicastPktConnV6 != nil { 402 | if err := c.multicastPktConnV6.Close(); err != nil { 403 | errs = append(errs, err) 404 | } 405 | } 406 | 407 | if c.unicastPktConnV4 != nil { 408 | if err := c.unicastPktConnV4.Close(); err != nil { 409 | errs = append(errs, err) 410 | } 411 | } 412 | 413 | if c.unicastPktConnV6 != nil { 414 | if err := c.unicastPktConnV6.Close(); err != nil { 415 | errs = append(errs, err) 416 | } 417 | } 418 | 419 | if len(errs) == 0 { 420 | <-c.closed 421 | 422 | return nil 423 | } 424 | 425 | rtrn := errFailedToClose 426 | for _, err := range errs { 427 | rtrn = fmt.Errorf("%w\n%w", err, rtrn) 428 | } 429 | 430 | return rtrn 431 | } 432 | 433 | // Query sends mDNS Queries for the following name until 434 | // either the Context is canceled/expires or we get a result 435 | // 436 | // Deprecated: Use QueryAddr instead as it supports the easier to use netip.Addr. 437 | func (c *Conn) Query(ctx context.Context, name string) (dnsmessage.ResourceHeader, net.Addr, error) { 438 | header, addr, err := c.QueryAddr(ctx, name) 439 | if err != nil { 440 | return header, nil, err 441 | } 442 | 443 | return header, &net.IPAddr{ 444 | IP: addr.AsSlice(), 445 | Zone: addr.Zone(), 446 | }, nil 447 | } 448 | 449 | // QueryAddr sends mDNS Queries for the following name until 450 | // either the Context is canceled/expires or we get a result. 451 | func (c *Conn) QueryAddr(ctx context.Context, name string) (dnsmessage.ResourceHeader, netip.Addr, error) { 452 | select { 453 | case <-c.closed: 454 | return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed 455 | default: 456 | } 457 | 458 | nameWithSuffix := name + "." 459 | 460 | queryChan := make(chan queryResult, 1) 461 | query := &query{nameWithSuffix, queryChan} 462 | c.mu.Lock() 463 | c.queries = append(c.queries, query) 464 | c.mu.Unlock() 465 | 466 | defer func() { 467 | c.mu.Lock() 468 | defer c.mu.Unlock() 469 | for i := len(c.queries) - 1; i >= 0; i-- { 470 | if c.queries[i] == query { 471 | c.queries = append(c.queries[:i], c.queries[i+1:]...) 472 | } 473 | } 474 | }() 475 | 476 | ticker := time.NewTicker(c.queryInterval) 477 | defer ticker.Stop() 478 | 479 | c.sendQuestion(nameWithSuffix) 480 | for { 481 | select { 482 | case <-ticker.C: 483 | c.sendQuestion(nameWithSuffix) 484 | case <-c.closed: 485 | return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed 486 | case res := <-queryChan: 487 | // Given https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2-2 488 | // An ICE agent SHOULD ignore candidates where the hostname resolution returns more than one IP address. 489 | // 490 | // We will take the first we receive which could result in a race between two suitable addresses where 491 | // one is better than the other (e.g. localhost vs LAN). 492 | return res.answer, res.addr, nil 493 | case <-ctx.Done(): 494 | return dnsmessage.ResourceHeader{}, netip.Addr{}, errContextElapsed 495 | } 496 | } 497 | } 498 | 499 | type ipToBytesError struct { 500 | addr netip.Addr 501 | expectedType string 502 | } 503 | 504 | func (err ipToBytesError) Error() string { 505 | return fmt.Sprintf("ip (%s) is not %s", err.addr, err.expectedType) 506 | } 507 | 508 | // assumes ipv4-to-ipv6 mapping has been checked. 509 | func ipv4ToBytes(ipAddr netip.Addr) ([4]byte, error) { 510 | if !ipAddr.Is4() { 511 | return [4]byte{}, ipToBytesError{ipAddr, "IPv4"} 512 | } 513 | 514 | md, err := ipAddr.MarshalBinary() 515 | if err != nil { 516 | return [4]byte{}, err 517 | } 518 | 519 | // net.IPs are stored in big endian / network byte order 520 | var out [4]byte 521 | copy(out[:], md) 522 | 523 | return out, nil 524 | } 525 | 526 | // assumes ipv4-to-ipv6 mapping has been checked. 527 | func ipv6ToBytes(ipAddr netip.Addr) ([16]byte, error) { 528 | if !ipAddr.Is6() { 529 | return [16]byte{}, ipToBytesError{ipAddr, "IPv6"} 530 | } 531 | md, err := ipAddr.MarshalBinary() 532 | if err != nil { 533 | return [16]byte{}, err 534 | } 535 | 536 | // net.IPs are stored in big endian / network byte order 537 | var out [16]byte 538 | copy(out[:], md) 539 | 540 | return out, nil 541 | } 542 | 543 | type ipToAddrError struct { 544 | ip []byte 545 | } 546 | 547 | func (err ipToAddrError) Error() string { 548 | return fmt.Sprintf("failed to convert ip address '%s' to netip.Addr", err.ip) 549 | } 550 | 551 | func interfaceForRemote(remote string) (*netip.Addr, error) { 552 | conn, err := net.Dial("udp", remote) 553 | if err != nil { 554 | return nil, err 555 | } 556 | 557 | localAddr, ok := conn.LocalAddr().(*net.UDPAddr) 558 | if !ok { 559 | return nil, errFailedCast 560 | } 561 | 562 | if err := conn.Close(); err != nil { 563 | return nil, err 564 | } 565 | 566 | ipAddr, ok := netip.AddrFromSlice(localAddr.IP) 567 | if !ok { 568 | return nil, ipToAddrError{localAddr.IP} 569 | } 570 | ipAddr = addrWithOptionalZone(ipAddr, localAddr.Zone) 571 | 572 | return &ipAddr, nil 573 | } 574 | 575 | type writeType byte 576 | 577 | const ( 578 | writeTypeQuestion writeType = iota 579 | writeTypeAnswer 580 | ) 581 | 582 | func (c *Conn) sendQuestion(name string) { 583 | packedName, err := dnsmessage.NewName(name) 584 | if err != nil { 585 | c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) 586 | 587 | return 588 | } 589 | 590 | // https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-mdns-ice-candidates-04#section-3.2.1 591 | // 592 | // 2. Otherwise, resolve the candidate using mDNS. The ICE agent 593 | // SHOULD set the unicast-response bit of the corresponding mDNS 594 | // query message; this minimizes multicast traffic, as the response 595 | // is probably only useful to the querying node. 596 | // 597 | // 18.12. Repurposing of Top Bit of qclass in Question Section 598 | // 599 | // In the Question Section of a Multicast DNS query, the top bit of the 600 | // qclass field is used to indicate that unicast responses are preferred 601 | // for this particular question. (See Section 5.4.) 602 | // 603 | // We'll follow this up sending on our unicast based packet connections so that we can 604 | // get a unicast response back. 605 | msg := dnsmessage.Message{ 606 | Header: dnsmessage.Header{}, 607 | } 608 | 609 | // limit what we ask for based on what IPv is available. In the future, 610 | // this could be an option since there's no reason you cannot get an 611 | // A record on an IPv6 sourced question and vice versa. 612 | if c.multicastPktConnV4 != nil { 613 | msg.Questions = append(msg.Questions, dnsmessage.Question{ 614 | Type: dnsmessage.TypeA, 615 | Class: dnsmessage.ClassINET | (1 << 15), 616 | Name: packedName, 617 | }) 618 | } 619 | if c.multicastPktConnV6 != nil { 620 | msg.Questions = append(msg.Questions, dnsmessage.Question{ 621 | Type: dnsmessage.TypeAAAA, 622 | Class: dnsmessage.ClassINET | (1 << 15), 623 | Name: packedName, 624 | }) 625 | } 626 | 627 | rawQuery, err := msg.Pack() 628 | if err != nil { 629 | c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) 630 | 631 | return 632 | } 633 | 634 | c.writeToSocket(-1, rawQuery, false, false, writeTypeQuestion, nil) 635 | } 636 | 637 | //nolint:gocognit,gocyclo,cyclop 638 | func (c *Conn) writeToSocket( 639 | ifIndex int, 640 | b []byte, 641 | hasLoopbackData bool, 642 | hasIPv6Zone bool, 643 | wType writeType, 644 | unicastDst *net.UDPAddr, 645 | ) { 646 | var dst4, dst6 net.Addr 647 | if wType == writeTypeAnswer { //nolint:nestif 648 | if unicastDst == nil { 649 | dst4 = c.dstAddr4 650 | dst6 = c.dstAddr6 651 | } else { 652 | if unicastDst.IP.To4() == nil { 653 | dst6 = unicastDst 654 | } else { 655 | dst4 = unicastDst 656 | } 657 | } 658 | } 659 | 660 | if ifIndex != -1 { //nolint:nestif 661 | if wType == writeTypeQuestion { 662 | c.log.Errorf("[%s] Unexpected question using specific interface index %d; dropping question", c.name, ifIndex) 663 | 664 | return 665 | } 666 | 667 | ifc, ok := c.ifaces[ifIndex] 668 | if !ok { 669 | c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) 670 | 671 | return 672 | } 673 | if hasLoopbackData && ifc.Flags&net.FlagLoopback == 0 { 674 | // avoid accidentally tricking the destination that itself is the same as us 675 | c.log.Debugf("[%s] interface is not loopback %d", c.name, ifIndex) 676 | 677 | return 678 | } 679 | 680 | c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) 681 | 682 | if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { 683 | if !hasIPv6Zone { 684 | if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { 685 | c.log.Warnf("[%s] failed to send mDNS packet on IPv4 interface %d: %v", c.name, ifIndex, err) 686 | } 687 | } else { 688 | c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) 689 | } 690 | } 691 | if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { 692 | if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { 693 | c.log.Warnf("[%s] failed to send mDNS packet on IPv6 interface %d: %v", c.name, ifIndex, err) 694 | } 695 | } 696 | 697 | return 698 | } 699 | for ifcIdx := range c.ifaces { 700 | ifc := c.ifaces[ifcIdx] 701 | if hasLoopbackData { 702 | c.log.Debugf("[%s] Refusing to send loopback data with non-specific interface", c.name) 703 | 704 | continue 705 | } 706 | 707 | if wType == writeTypeQuestion { //nolint:nestif 708 | // we'll write via unicast if we can in case the responder chooses to respond to the address the request 709 | // came from (i.e. not respecting unicast-response bit). If we were to use the multicast packet 710 | // conn here, we'd be writing from a specific multicast address which won't be able to receive unicast 711 | // traffic (it only works when listening on 0.0.0.0/[::]). 712 | if c.unicastPktConnV4 == nil && c.unicastPktConnV6 == nil { 713 | c.log.Debugf("[%s] writing question to multicast IPv4/6 %s", c.name, c.dstAddr4) 714 | if ifc.supportsV4 && c.multicastPktConnV4 != nil { 715 | if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { 716 | c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifc.Index, err) 717 | } 718 | } 719 | if ifc.supportsV6 && c.multicastPktConnV6 != nil { 720 | if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { 721 | c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifc.Index, err) 722 | } 723 | } 724 | } 725 | if ifc.supportsV4 && c.unicastPktConnV4 != nil { 726 | c.log.Debugf("[%s] writing question to unicast IPv4 %s", c.name, c.dstAddr4) 727 | if _, err := c.unicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { 728 | c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) 729 | } 730 | } 731 | if ifc.supportsV6 && c.unicastPktConnV6 != nil { 732 | c.log.Debugf("[%s] writing question to unicast IPv6 %s", c.name, c.dstAddr6) 733 | if _, err := c.unicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { 734 | c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) 735 | } 736 | } 737 | } else { 738 | c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) 739 | 740 | if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { 741 | if !hasIPv6Zone { 742 | if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { 743 | c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifIndex, err) 744 | } 745 | } else { 746 | c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) 747 | } 748 | } 749 | if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { 750 | if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { 751 | c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifIndex, err) 752 | } 753 | } 754 | } 755 | } 756 | } 757 | 758 | func createAnswer(id uint16, name string, addr netip.Addr) (dnsmessage.Message, error) { 759 | packedName, err := dnsmessage.NewName(name) 760 | if err != nil { 761 | return dnsmessage.Message{}, err 762 | } 763 | 764 | msg := dnsmessage.Message{ 765 | Header: dnsmessage.Header{ 766 | ID: id, 767 | Response: true, 768 | Authoritative: true, 769 | }, 770 | Answers: []dnsmessage.Resource{ 771 | { 772 | Header: dnsmessage.ResourceHeader{ 773 | Class: dnsmessage.ClassINET, 774 | Name: packedName, 775 | TTL: responseTTL, 776 | }, 777 | }, 778 | }, 779 | } 780 | 781 | if addr.Is4() { 782 | ipBuf, err := ipv4ToBytes(addr) 783 | if err != nil { 784 | return dnsmessage.Message{}, err 785 | } 786 | msg.Answers[0].Header.Type = dnsmessage.TypeA 787 | msg.Answers[0].Body = &dnsmessage.AResource{ 788 | A: ipBuf, 789 | } 790 | } else if addr.Is6() { 791 | // we will lose the zone here, but the receiver can reconstruct it 792 | ipBuf, err := ipv6ToBytes(addr) 793 | if err != nil { 794 | return dnsmessage.Message{}, err 795 | } 796 | msg.Answers[0].Header.Type = dnsmessage.TypeAAAA 797 | msg.Answers[0].Body = &dnsmessage.AAAAResource{ 798 | AAAA: ipBuf, 799 | } 800 | } 801 | 802 | return msg, nil 803 | } 804 | 805 | func (c *Conn) sendAnswer(queryID uint16, name string, ifIndex int, result netip.Addr, dst *net.UDPAddr) { 806 | answer, err := createAnswer(queryID, name, result) 807 | if err != nil { 808 | c.log.Warnf("[%s] failed to create mDNS answer %v", c.name, err) 809 | 810 | return 811 | } 812 | 813 | rawAnswer, err := answer.Pack() 814 | if err != nil { 815 | c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) 816 | 817 | return 818 | } 819 | 820 | c.writeToSocket( 821 | ifIndex, 822 | rawAnswer, 823 | result.IsLoopback(), 824 | result.Is6() && result.Zone() != "", 825 | writeTypeAnswer, 826 | dst, 827 | ) 828 | } 829 | 830 | type ipControlMessage struct { 831 | IfIndex int 832 | Dst net.IP 833 | } 834 | 835 | type ipPacketConn interface { 836 | ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) 837 | WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) 838 | Close() error 839 | } 840 | 841 | type ipPacketConn4 struct { 842 | name string 843 | conn *ipv4.PacketConn 844 | log logging.LeveledLogger 845 | } 846 | 847 | func (c ipPacketConn4) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { 848 | n, cm4, src, err := c.conn.ReadFrom(b) 849 | if err != nil || cm4 == nil { 850 | return n, nil, src, err 851 | } 852 | 853 | return n, &ipControlMessage{IfIndex: cm4.IfIndex, Dst: cm4.Dst}, src, err 854 | } 855 | 856 | func (c ipPacketConn4) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { 857 | var cm4 *ipv4.ControlMessage 858 | if cm != nil { 859 | cm4 = &ipv4.ControlMessage{ 860 | IfIndex: cm.IfIndex, 861 | } 862 | } 863 | if err := c.conn.SetMulticastInterface(via); err != nil { 864 | c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) 865 | 866 | return 0, err 867 | } 868 | 869 | return c.conn.WriteTo(b, cm4, dst) 870 | } 871 | 872 | func (c ipPacketConn4) Close() error { 873 | return c.conn.Close() 874 | } 875 | 876 | type ipPacketConn6 struct { 877 | name string 878 | conn *ipv6.PacketConn 879 | log logging.LeveledLogger 880 | } 881 | 882 | func (c ipPacketConn6) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { 883 | n, cm6, src, err := c.conn.ReadFrom(b) 884 | if err != nil || cm6 == nil { 885 | return n, nil, src, err 886 | } 887 | 888 | return n, &ipControlMessage{IfIndex: cm6.IfIndex, Dst: cm6.Dst}, src, err 889 | } 890 | 891 | func (c ipPacketConn6) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { 892 | var cm6 *ipv6.ControlMessage 893 | if cm != nil { 894 | cm6 = &ipv6.ControlMessage{ 895 | IfIndex: cm.IfIndex, 896 | } 897 | } 898 | if err := c.conn.SetMulticastInterface(via); err != nil { 899 | c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) 900 | 901 | return 0, err 902 | } 903 | 904 | return c.conn.WriteTo(b, cm6, dst) 905 | } 906 | 907 | func (c ipPacketConn6) Close() error { 908 | return c.conn.Close() 909 | } 910 | 911 | //nolint:gocognit,gocyclo,cyclop,maintidx 912 | func (c *Conn) readLoop(name string, pktConn ipPacketConn, inboundBufferSize int, config *Config) { 913 | b := make([]byte, inboundBufferSize) 914 | parser := dnsmessage.Parser{} 915 | 916 | for { 917 | n, cm, src, err := pktConn.ReadFrom(b) 918 | if err != nil { 919 | if errors.Is(err, net.ErrClosed) { 920 | return 921 | } 922 | c.log.Warnf("[%s] failed to ReadFrom %q %v", c.name, src, err) 923 | 924 | continue 925 | } 926 | c.log.Debugf("[%s] got read on %s from %s", c.name, name, src) 927 | 928 | var ifIndex int 929 | var pktDst net.IP 930 | if cm != nil { 931 | ifIndex = cm.IfIndex 932 | pktDst = cm.Dst 933 | } else { 934 | ifIndex = -1 935 | } 936 | srcAddr, ok := src.(*net.UDPAddr) 937 | if !ok { 938 | c.log.Warnf("[%s] expected source address %s to be UDP but got %", c.name, src, src) 939 | 940 | continue 941 | } 942 | 943 | func() { 944 | header, err := parser.Start(b[:n]) 945 | if err != nil { 946 | c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) 947 | 948 | return 949 | } 950 | 951 | for i := 0; i <= maxMessageRecords; i++ { 952 | question, err := parser.Question() 953 | if errors.Is(err, dnsmessage.ErrSectionDone) { 954 | break 955 | } else if err != nil { 956 | c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) 957 | 958 | return 959 | } 960 | 961 | if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA { 962 | continue 963 | } 964 | 965 | // https://datatracker.ietf.org/doc/html/rfc6762#section-6 966 | // The destination UDP port in all Multicast DNS responses MUST be 5353, 967 | // and the destination address MUST be the mDNS IPv4 link-local 968 | // multicast address 224.0.0.251 or its IPv6 equivalent FF02::FB, except 969 | // when generating a reply to a query that explicitly requested a 970 | // unicast response 971 | shouldUnicastResponse := (question.Class&(1<<15)) != 0 || // via the unicast-response bit 972 | srcAddr.Port != 5353 || // by virtue of being a legacy query (Section 6.7), or 973 | (len(pktDst) != 0 && !(pktDst.Equal(c.dstAddr4.IP) || // by virtue of being a direct unicast query 974 | pktDst.Equal(c.dstAddr6.IP))) 975 | var dst *net.UDPAddr 976 | if shouldUnicastResponse { 977 | dst = srcAddr 978 | } 979 | 980 | queryWantsV4 := question.Type == dnsmessage.TypeA 981 | 982 | for _, localName := range c.localNames { 983 | if localName == question.Name.String() { //nolint:nestif 984 | var localAddress *netip.Addr 985 | if config.LocalAddress != nil { 986 | // this means the LocalAddress does not support link-local since 987 | // we have no zone to set here. 988 | ipAddr, ok := netip.AddrFromSlice(config.LocalAddress) 989 | if !ok { 990 | c.log.Warnf("[%s] failed to convert config.LocalAddress '%s' to netip.Addr", c.name, config.LocalAddress) 991 | 992 | continue 993 | } 994 | if c.multicastPktConnV4 != nil { 995 | // don't want mapping since we also support IPv4/A 996 | ipAddr = ipAddr.Unmap() 997 | } 998 | localAddress = &ipAddr 999 | } else { 1000 | // prefer the address of the interface if we know its index, but otherwise 1001 | // derive it from the address we read from. We do this because even if 1002 | // multicast loopback is in use or we send from a loopback interface, 1003 | // there are still cases where the IP packet will contain the wrong 1004 | // source IP (e.g. a LAN interface). 1005 | // For example, we can have a packet that has: 1006 | // Source: 192.168.65.3 1007 | // Destination: 224.0.0.251 1008 | // Interface Index: 1 1009 | // Interface Addresses @ 1: [127.0.0.1/8 ::1/128] 1010 | if ifIndex != -1 { 1011 | ifc, ok := c.ifaces[ifIndex] 1012 | if !ok { 1013 | c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) 1014 | 1015 | return 1016 | } 1017 | var selectedAddrs []netip.Addr 1018 | for _, addr := range ifc.ipAddrs { 1019 | addrCopy := addr 1020 | 1021 | // match up respective IP types based on question 1022 | if queryWantsV4 { 1023 | if addrCopy.Is4In6() { 1024 | // we may allow 4-in-6, but the question wants an A record 1025 | addrCopy = addrCopy.Unmap() 1026 | } 1027 | if !addrCopy.Is4() { 1028 | continue 1029 | } 1030 | } else { // queryWantsV6 1031 | if !addrCopy.Is6() { 1032 | continue 1033 | } 1034 | if !isSupportedIPv6(addrCopy, c.multicastPktConnV4 == nil) { 1035 | c.log.Debugf("[%s] interface %d address not a supported IPv6 address %s", c.name, ifIndex, &addrCopy) 1036 | 1037 | continue 1038 | } 1039 | } 1040 | 1041 | selectedAddrs = append(selectedAddrs, addrCopy) 1042 | } 1043 | if len(selectedAddrs) == 0 { 1044 | c.log.Debugf( 1045 | "[%s] failed to find suitable IP for interface %d; deriving address from source address c.name,instead", 1046 | c.name, ifIndex, 1047 | ) 1048 | } else { 1049 | // choose the best match 1050 | var choice *netip.Addr 1051 | for _, option := range selectedAddrs { 1052 | optCopy := option 1053 | if option.Is4() { 1054 | // select first 1055 | choice = &optCopy 1056 | 1057 | break 1058 | } 1059 | // we're okay with 4In6 for now but ideally we get a an actual IPv6. 1060 | // Maybe in the future we never want this but it does look like Docker 1061 | // can route IPv4 over IPv6. 1062 | if choice == nil { 1063 | choice = &optCopy 1064 | } else if !optCopy.Is4In6() { 1065 | choice = &optCopy 1066 | } 1067 | if !optCopy.Is4In6() { 1068 | break 1069 | } 1070 | // otherwise keep searching for an actual IPv6 1071 | } 1072 | localAddress = choice 1073 | } 1074 | } 1075 | if ifIndex == -1 || localAddress == nil { 1076 | localAddress, err = interfaceForRemote(src.String()) 1077 | if err != nil { 1078 | c.log.Warnf("[%s] failed to get local interface to communicate with %s: %v", c.name, src.String(), err) 1079 | 1080 | continue 1081 | } 1082 | } 1083 | } 1084 | if queryWantsV4 { 1085 | if !localAddress.Is4() { 1086 | c.log.Debugf( 1087 | "[%s] have IPv6 address %s to respond with but question is for A not c.name,AAAA", 1088 | c.name, localAddress, 1089 | ) 1090 | 1091 | continue 1092 | } 1093 | } else { 1094 | if !localAddress.Is6() { 1095 | c.log.Debugf( 1096 | "[%s] have IPv4 address %s to respond with but question is for AAAA not c.name,A", 1097 | c.name, localAddress, 1098 | ) 1099 | 1100 | continue 1101 | } 1102 | if !isSupportedIPv6(*localAddress, c.multicastPktConnV4 == nil) { 1103 | c.log.Debugf("[%s] got local interface address but not a supported IPv6 address %v", c.name, localAddress) 1104 | 1105 | continue 1106 | } 1107 | } 1108 | 1109 | if dst != nil && len(dst.IP) == net.IPv4len && 1110 | localAddress.Is6() && 1111 | localAddress.Zone() != "" && 1112 | (localAddress.IsLinkLocalUnicast() || localAddress.IsLinkLocalMulticast()) { 1113 | // This case happens when multicast v4 picks up an AAAA question that has a zone 1114 | // in the address. Since we cannot send this zone over DNS (it's meaningless), 1115 | // the other side can only infer this via the response interface on the other 1116 | // side (some IPv6 interface). 1117 | c.log.Debugf("[%s] refusing to send link-local address %s to an IPv4 destination %s", c.name, localAddress, dst) 1118 | 1119 | continue 1120 | } 1121 | c.log.Debugf( 1122 | "[%s] sending response for %s on ifc %d of %s to %s", 1123 | c.name, question.Name, ifIndex, *localAddress, dst, 1124 | ) 1125 | c.sendAnswer(header.ID, question.Name.String(), ifIndex, *localAddress, dst) 1126 | } 1127 | } 1128 | } 1129 | 1130 | for i := 0; i <= maxMessageRecords; i++ { 1131 | answer, err := parser.AnswerHeader() 1132 | if errors.Is(err, dnsmessage.ErrSectionDone) { 1133 | return 1134 | } 1135 | if err != nil { 1136 | c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) 1137 | 1138 | return 1139 | } 1140 | 1141 | if answer.Type != dnsmessage.TypeA && answer.Type != dnsmessage.TypeAAAA { 1142 | continue 1143 | } 1144 | 1145 | c.mu.Lock() 1146 | queries := make([]*query, len(c.queries)) 1147 | copy(queries, c.queries) 1148 | c.mu.Unlock() 1149 | 1150 | var answered []*query 1151 | for _, query := range queries { 1152 | queryCopy := query 1153 | if queryCopy.nameWithSuffix == answer.Name.String() { 1154 | addr, err := addrFromAnswerHeader(answer, parser) 1155 | if err != nil { 1156 | c.log.Warnf("[%s] failed to parse mDNS answer %v", c.name, err) 1157 | 1158 | return 1159 | } 1160 | 1161 | resultAddr := *addr 1162 | // DNS records don't contain IPv6 zones. 1163 | // We're trusting that since we're on the same link, that we will only 1164 | // be sent link-local addresses from that source's interface's address. 1165 | // If it's not present, we're out of luck since we cannot rely on the 1166 | // interface zone to be the same as the source's. 1167 | resultAddr = addrWithOptionalZone(resultAddr, srcAddr.Zone) 1168 | 1169 | select { 1170 | case queryCopy.queryResultChan <- queryResult{answer, resultAddr}: 1171 | answered = append(answered, queryCopy) 1172 | default: 1173 | } 1174 | } 1175 | } 1176 | 1177 | c.mu.Lock() 1178 | for queryIdx := len(c.queries) - 1; queryIdx >= 0; queryIdx-- { 1179 | for answerIdx := len(answered) - 1; answerIdx >= 0; answerIdx-- { 1180 | if c.queries[queryIdx] == answered[answerIdx] { 1181 | c.queries = append(c.queries[:queryIdx], c.queries[queryIdx+1:]...) 1182 | answered = append(answered[:answerIdx], answered[answerIdx+1:]...) 1183 | queryIdx-- 1184 | 1185 | break 1186 | } 1187 | } 1188 | } 1189 | c.mu.Unlock() 1190 | } 1191 | }() 1192 | } 1193 | } 1194 | 1195 | func (c *Conn) start(started chan<- struct{}, inboundBufferSize int, config *Config) { 1196 | defer func() { 1197 | c.mu.Lock() 1198 | defer c.mu.Unlock() 1199 | close(c.closed) 1200 | }() 1201 | 1202 | var numReaders int 1203 | readerStarted := make(chan struct{}) 1204 | readerEnded := make(chan struct{}) 1205 | 1206 | if c.multicastPktConnV4 != nil { 1207 | numReaders++ 1208 | go func() { 1209 | defer func() { 1210 | readerEnded <- struct{}{} 1211 | }() 1212 | readerStarted <- struct{}{} 1213 | c.readLoop("multi4", c.multicastPktConnV4, inboundBufferSize, config) 1214 | }() 1215 | } 1216 | if c.multicastPktConnV6 != nil { 1217 | numReaders++ 1218 | go func() { 1219 | defer func() { 1220 | readerEnded <- struct{}{} 1221 | }() 1222 | readerStarted <- struct{}{} 1223 | c.readLoop("multi6", c.multicastPktConnV6, inboundBufferSize, config) 1224 | }() 1225 | } 1226 | if c.unicastPktConnV4 != nil { 1227 | numReaders++ 1228 | go func() { 1229 | defer func() { 1230 | readerEnded <- struct{}{} 1231 | }() 1232 | readerStarted <- struct{}{} 1233 | c.readLoop("uni4", c.unicastPktConnV4, inboundBufferSize, config) 1234 | }() 1235 | } 1236 | if c.unicastPktConnV6 != nil { 1237 | numReaders++ 1238 | go func() { 1239 | defer func() { 1240 | readerEnded <- struct{}{} 1241 | }() 1242 | readerStarted <- struct{}{} 1243 | c.readLoop("uni6", c.unicastPktConnV6, inboundBufferSize, config) 1244 | }() 1245 | } 1246 | for i := 0; i < numReaders; i++ { 1247 | <-readerStarted 1248 | } 1249 | close(started) 1250 | for i := 0; i < numReaders; i++ { 1251 | <-readerEnded 1252 | } 1253 | } 1254 | 1255 | func addrFromAnswerHeader(header dnsmessage.ResourceHeader, parser dnsmessage.Parser) (addr *netip.Addr, err error) { 1256 | switch header.Type { 1257 | case dnsmessage.TypeA: 1258 | resource, err := parser.AResource() 1259 | if err != nil { 1260 | return nil, err 1261 | } 1262 | ipAddr, ok := netip.AddrFromSlice(resource.A[:]) 1263 | if !ok { 1264 | return nil, fmt.Errorf("failed to convert A record: %w", ipToAddrError{resource.A[:]}) 1265 | } 1266 | ipAddr = ipAddr.Unmap() // do not want 4-in-6 1267 | 1268 | return &ipAddr, nil 1269 | case dnsmessage.TypeAAAA: 1270 | resource, err := parser.AAAAResource() 1271 | if err != nil { 1272 | return nil, err 1273 | } 1274 | ipAddr, ok := netip.AddrFromSlice(resource.AAAA[:]) 1275 | if !ok { 1276 | return nil, fmt.Errorf("failed to convert AAAA record: %w", ipToAddrError{resource.AAAA[:]}) 1277 | } 1278 | 1279 | return &ipAddr, nil 1280 | default: 1281 | return nil, fmt.Errorf("unsupported record type %d", header.Type) //nolint:err113 // Never happens 1282 | } 1283 | } 1284 | 1285 | func isSupportedIPv6(addr netip.Addr, ipv6Only bool) bool { 1286 | if !addr.Is6() { 1287 | return false 1288 | } 1289 | // IPv4-mapped-IPv6 addresses cannot be connected to unless 1290 | // unmapped. 1291 | if !ipv6Only && addr.Is4In6() { 1292 | return false 1293 | } 1294 | 1295 | return true 1296 | } 1297 | 1298 | func addrWithOptionalZone(addr netip.Addr, zone string) netip.Addr { 1299 | if zone == "" { 1300 | return addr 1301 | } 1302 | if addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) { 1303 | return addr.WithZone(zone) 1304 | } 1305 | 1306 | return addr 1307 | } 1308 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package mdns 8 | 9 | import ( 10 | "context" 11 | "net" 12 | "net/netip" 13 | "runtime" 14 | "testing" 15 | "time" 16 | 17 | "github.com/pion/transport/v3/test" 18 | "github.com/stretchr/testify/assert" 19 | "golang.org/x/net/dns/dnsmessage" 20 | "golang.org/x/net/ipv4" 21 | "golang.org/x/net/ipv6" 22 | ) 23 | 24 | const localAddress = "1.2.3.4" 25 | 26 | func check(t *testing.T, err error) { 27 | t.Helper() 28 | assert.NoError(t, err) 29 | } 30 | 31 | func checkIPv4(t *testing.T, addr netip.Addr) { 32 | t.Helper() 33 | assert.Truef(t, addr.Is4(), "expected IPv4 for answer but got %s", addr) 34 | } 35 | 36 | func checkIPv6(t *testing.T, addr netip.Addr) { 37 | t.Helper() 38 | assert.Truef(t, addr.Is6(), "expected IPv6 for answer but got %s", addr) 39 | } 40 | 41 | func createListener4(t *testing.T) *net.UDPConn { 42 | t.Helper() 43 | addr, err := net.ResolveUDPAddr("udp", DefaultAddressIPv4) 44 | check(t, err) 45 | 46 | sock, err := net.ListenUDP("udp4", addr) 47 | check(t, err) 48 | 49 | return sock 50 | } 51 | 52 | func createListener6(t *testing.T) *net.UDPConn { 53 | t.Helper() 54 | addr, err := net.ResolveUDPAddr("udp", DefaultAddressIPv6) 55 | check(t, err) 56 | 57 | sock, err := net.ListenUDP("udp6", addr) 58 | check(t, err) 59 | 60 | return sock 61 | } 62 | 63 | func TestValidCommunication(t *testing.T) { 64 | lim := test.TimeOut(time.Second * 10) 65 | defer lim.Stop() 66 | 67 | report := test.CheckRoutines(t) 68 | defer report() 69 | 70 | aSock := createListener4(t) 71 | bSock := createListener4(t) 72 | 73 | aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ 74 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 75 | }) 76 | check(t, err) 77 | 78 | bServer, err := Server(ipv4.NewPacketConn(bSock), nil, &Config{}) 79 | check(t, err) 80 | 81 | _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 82 | check(t, err) 83 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 84 | checkIPv4(t, addr) 85 | 86 | _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") 87 | check(t, err) 88 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 89 | checkIPv4(t, addr) 90 | 91 | // test against regression from https://github.com/pion/mdns/commit/608f20b 92 | // where by properly sending mDNS responses to all interfaces, we significantly 93 | // increased the chance that we send a loopback response to a Query that is 94 | // unwillingly to use loopback addresses (the default in pion/ice). 95 | for i := 0; i < 100; i++ { 96 | _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") 97 | check(t, err) 98 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 99 | assert.NotEqual(t, "127.0.0.1", addr.String(), "unexpected loopback") 100 | checkIPv4(t, addr) 101 | } 102 | 103 | check(t, aServer.Close()) 104 | check(t, bServer.Close()) 105 | 106 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 107 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 108 | } 109 | 110 | func TestValidCommunicationWithAddressConfig(t *testing.T) { 111 | lim := test.TimeOut(time.Second * 10) 112 | defer lim.Stop() 113 | 114 | report := test.CheckRoutines(t) 115 | defer report() 116 | 117 | aSock := createListener4(t) 118 | 119 | aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ 120 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 121 | LocalAddress: net.ParseIP(localAddress), 122 | }) 123 | check(t, err) 124 | 125 | _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 126 | check(t, err) 127 | assert.Equalf(t, localAddress, addr.String(), "address mismatch: expected %s, but got %v\n", localAddress, addr) 128 | 129 | check(t, aServer.Close()) 130 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 131 | } 132 | 133 | func TestValidCommunicationWithLoopbackAddressConfig(t *testing.T) { 134 | lim := test.TimeOut(time.Second * 10) 135 | defer lim.Stop() 136 | 137 | report := test.CheckRoutines(t) 138 | defer report() 139 | 140 | aSock := createListener4(t) 141 | 142 | loopbackIP := net.ParseIP("127.0.0.1") 143 | 144 | aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ 145 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 146 | LocalAddress: loopbackIP, 147 | IncludeLoopback: true, // the test would fail if this was false 148 | }) 149 | check(t, err) 150 | 151 | _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 152 | check(t, err) 153 | assert.Equalf(t, loopbackIP.String(), addr.String(), "address mismatch: expected %s, but got %v\n", loopbackIP, addr) 154 | 155 | check(t, aServer.Close()) 156 | } 157 | 158 | func TestValidCommunicationWithLoopbackInterface(t *testing.T) { 159 | lim := test.TimeOut(time.Second * 10) 160 | defer lim.Stop() 161 | 162 | report := test.CheckRoutines(t) 163 | defer report() 164 | 165 | aSock := createListener4(t) 166 | 167 | ifaces, err := net.Interfaces() 168 | check(t, err) 169 | ifacesToUse := make([]net.Interface, 0, len(ifaces)) 170 | for _, ifc := range ifaces { 171 | if ifc.Flags&net.FlagLoopback != net.FlagLoopback { 172 | continue 173 | } 174 | ifcCopy := ifc 175 | ifacesToUse = append(ifacesToUse, ifcCopy) 176 | } 177 | 178 | // the following checks are unlikely to fail since most places where this code runs 179 | // will have a loopback 180 | if len(ifacesToUse) == 0 { 181 | t.Skip("expected at least one loopback interface, but got none") 182 | } 183 | 184 | aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ 185 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 186 | IncludeLoopback: true, // the test would fail if this was false 187 | Interfaces: ifacesToUse, 188 | }) 189 | check(t, err) 190 | 191 | _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 192 | check(t, err) 193 | var found bool 194 | for _, iface := range ifacesToUse { 195 | addrs, err := iface.Addrs() 196 | check(t, err) 197 | for _, ifaceAddr := range addrs { 198 | ipAddr, ok := ifaceAddr.(*net.IPNet) 199 | assert.Truef(t, ok, "expected *net.IPNet address for loopback but got %T", addr) 200 | if addr.String() == ipAddr.IP.String() { 201 | found = true 202 | 203 | break 204 | } 205 | } 206 | if found { 207 | break 208 | } 209 | } 210 | assert.Truef(t, found, "address mismatch: expected loopback address, but got %v\n", addr) 211 | 212 | check(t, aServer.Close()) 213 | } 214 | 215 | func TestValidCommunicationIPv6(t *testing.T) { //nolint:cyclop 216 | if runtime.GOARCH == "386" { 217 | t.Skip("IPv6 not supported on 386 for some reason") 218 | } 219 | lim := test.TimeOut(time.Second * 10) 220 | defer lim.Stop() 221 | 222 | report := test.CheckRoutines(t) 223 | defer report() 224 | 225 | _, err := Server(nil, nil, &Config{ 226 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 227 | }) 228 | assert.ErrorIs(t, err, errNoPacketConn, "expected error if no PacketConn supplied to Server") 229 | 230 | aSock := createListener6(t) 231 | bSock := createListener6(t) 232 | 233 | aServer, err := Server(nil, ipv6.NewPacketConn(aSock), &Config{ 234 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 235 | }) 236 | check(t, err) 237 | 238 | bServer, err := Server(nil, ipv6.NewPacketConn(bSock), &Config{}) 239 | check(t, err) 240 | 241 | header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 242 | check(t, err) 243 | assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) 244 | 245 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 246 | checkIPv6(t, addr) 247 | if addr.Is4In6() { 248 | // probably within docker 249 | t.Logf("address %s is an IPv4-to-IPv6 mapped address even though the stack is IPv6", addr) 250 | } else { 251 | assert.NotEqualf(t, "", addr.Zone(), "expected IPv6 to have zone but got %s", addr) 252 | } 253 | 254 | header, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") 255 | check(t, err) 256 | assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) 257 | 258 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 259 | checkIPv6(t, addr) 260 | if !addr.Is4In6() { 261 | assert.NotEqualf(t, "", addr.Zone(), "expected IPv6 to have zone but got %s", addr) 262 | } 263 | 264 | check(t, aServer.Close()) 265 | check(t, bServer.Close()) 266 | 267 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 268 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 269 | } 270 | 271 | func TestValidCommunicationIPv46(t *testing.T) { 272 | lim := test.TimeOut(time.Second * 10) 273 | defer lim.Stop() 274 | 275 | report := test.CheckRoutines(t) 276 | defer report() 277 | 278 | aSock4 := createListener4(t) 279 | bSock4 := createListener4(t) 280 | aSock6 := createListener6(t) 281 | bSock6 := createListener6(t) 282 | 283 | aServer, err := Server(ipv4.NewPacketConn(aSock4), ipv6.NewPacketConn(aSock6), &Config{ 284 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 285 | }) 286 | check(t, err) 287 | 288 | bServer, err := Server(ipv4.NewPacketConn(bSock4), ipv6.NewPacketConn(bSock6), &Config{}) 289 | check(t, err) 290 | 291 | _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 292 | check(t, err) 293 | 294 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 295 | 296 | _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") 297 | check(t, err) 298 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 299 | 300 | check(t, aServer.Close()) 301 | check(t, bServer.Close()) 302 | 303 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 304 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 305 | } 306 | 307 | func TestValidCommunicationIPv46Mixed(t *testing.T) { 308 | lim := test.TimeOut(time.Second * 10) 309 | defer lim.Stop() 310 | 311 | report := test.CheckRoutines(t) 312 | defer report() 313 | 314 | aSock4 := createListener4(t) 315 | bSock6 := createListener6(t) 316 | 317 | // we can always send from a 6-only server to a 4-only server but not always 318 | // the other way around because the IPv4-only server will only listen 319 | // on multicast for IPv4 questions, so it will never see an IPv6 originated 320 | // question that contains required information to respond (the zone, if link-local). 321 | // Therefore, the IPv4 server will refuse answering AAAA responses over 322 | // unicast/multicast IPv4 if the answer is an IPv6 link-local address. This is basically 323 | // the majority of cases unless a LocalAddress is set on the Config. 324 | aServer, err := Server(ipv4.NewPacketConn(aSock4), nil, &Config{ 325 | Name: "aServer", 326 | }) 327 | check(t, err) 328 | 329 | bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{ 330 | Name: "bServer", 331 | LocalNames: []string{"pion-mdns-1.local"}, 332 | }) 333 | check(t, err) 334 | 335 | header, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 336 | 337 | check(t, err) 338 | assert.Equalf(t, dnsmessage.TypeA, header.Type, "expected A but got %s", header.Type) 339 | 340 | checkIPv4(t, addr) 341 | 342 | check(t, aServer.Close()) 343 | check(t, bServer.Close()) 344 | 345 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 346 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 347 | } 348 | 349 | func TestValidCommunicationIPv46MixedLocalAddress(t *testing.T) { 350 | lim := test.TimeOut(time.Second * 10) 351 | defer lim.Stop() 352 | 353 | report := test.CheckRoutines(t) 354 | defer report() 355 | 356 | aSock4 := createListener4(t) 357 | bSock6 := createListener6(t) 358 | 359 | aServer, err := Server(ipv4.NewPacketConn(aSock4), nil, &Config{ 360 | LocalAddress: net.IPv4(1, 2, 3, 4), 361 | LocalNames: []string{"pion-mdns-1.local"}, 362 | }) 363 | check(t, err) 364 | 365 | bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) 366 | check(t, err) 367 | 368 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 369 | defer cancel() 370 | 371 | // we want ipv6 but all we can offer is an ipv4 mapped address, so it should fail until we support 372 | // allowing this explicitly via configuration on the aServer side 373 | _, _, err = bServer.QueryAddr(ctx, "pion-mdns-1.local") 374 | assert.ErrorIsf(t, err, errContextElapsed, "Query expired but returned unexpected error %v", err) 375 | 376 | check(t, aServer.Close()) 377 | check(t, bServer.Close()) 378 | 379 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 380 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 381 | } 382 | 383 | func TestValidCommunicationIPv66Mixed(t *testing.T) { 384 | lim := test.TimeOut(time.Second * 10) 385 | defer lim.Stop() 386 | 387 | report := test.CheckRoutines(t) 388 | defer report() 389 | 390 | aSock6 := createListener6(t) 391 | bSock6 := createListener6(t) 392 | 393 | aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), &Config{ 394 | LocalNames: []string{"pion-mdns-1.local"}, 395 | }) 396 | check(t, err) 397 | 398 | bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) 399 | check(t, err) 400 | 401 | header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 402 | check(t, err) 403 | assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) 404 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 405 | assert.Falsef(t, addr.Is4In6(), "expected address to not be ipv4-to-ipv6 mapped: %v", addr) 406 | checkIPv6(t, addr) 407 | 408 | check(t, aServer.Close()) 409 | check(t, bServer.Close()) 410 | 411 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 412 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 413 | } 414 | 415 | func TestValidCommunicationIPv66MixedLocalAddress(t *testing.T) { 416 | lim := test.TimeOut(time.Second * 10) 417 | defer lim.Stop() 418 | 419 | report := test.CheckRoutines(t) 420 | defer report() 421 | 422 | aSock6 := createListener6(t) 423 | bSock6 := createListener6(t) 424 | 425 | aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), &Config{ 426 | LocalAddress: net.IPv4(1, 2, 3, 4), 427 | LocalNames: []string{"pion-mdns-1.local"}, 428 | }) 429 | check(t, err) 430 | 431 | bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) 432 | check(t, err) 433 | 434 | header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 435 | check(t, err) 436 | assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) 437 | assert.Truef(t, addr.Is4In6(), "expected address to be ipv4-to-ipv6 mapped: %v", addr) 438 | // now unmap just for this check 439 | assert.Equalf(t, localAddress, addr.Unmap().String(), "unexpected local address: %v", addr) 440 | checkIPv6(t, addr) 441 | 442 | check(t, aServer.Close()) 443 | check(t, bServer.Close()) 444 | 445 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 446 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 447 | } 448 | 449 | func TestValidCommunicationIPv64Mixed(t *testing.T) { 450 | lim := test.TimeOut(time.Second * 10) 451 | defer lim.Stop() 452 | 453 | report := test.CheckRoutines(t) 454 | defer report() 455 | 456 | aSock6 := createListener6(t) 457 | bSock4 := createListener4(t) 458 | 459 | aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), &Config{ 460 | LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, 461 | }) 462 | check(t, err) 463 | 464 | bServer, err := Server(ipv4.NewPacketConn(bSock4), nil, &Config{}) 465 | check(t, err) 466 | 467 | _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") 468 | check(t, err) 469 | 470 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 471 | 472 | header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") 473 | check(t, err) 474 | assert.Equalf(t, dnsmessage.TypeA, header.Type, "expected A but got %s", header.Type) 475 | assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) 476 | 477 | check(t, aServer.Close()) 478 | check(t, bServer.Close()) 479 | 480 | assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") 481 | assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") 482 | } 483 | 484 | func TestMultipleClose(t *testing.T) { 485 | lim := test.TimeOut(time.Second * 10) 486 | defer lim.Stop() 487 | 488 | report := test.CheckRoutines(t) 489 | defer report() 490 | 491 | aSock := createListener4(t) 492 | 493 | server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) 494 | check(t, err) 495 | 496 | check(t, server.Close()) 497 | check(t, server.Close()) 498 | 499 | assert.Empty(t, server.queries, "Queries not cleaned up after server close") 500 | } 501 | 502 | func TestQueryRespectTimeout(t *testing.T) { 503 | lim := test.TimeOut(time.Second * 10) 504 | defer lim.Stop() 505 | 506 | report := test.CheckRoutines(t) 507 | defer report() 508 | 509 | aSock := createListener4(t) 510 | 511 | server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) 512 | check(t, err) 513 | 514 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) 515 | defer cancel() 516 | 517 | _, _, err = server.QueryAddr(ctx, "invalid-host") 518 | assert.ErrorIsf(t, err, errContextElapsed, "Query expired but returned unexpected error %v", err) 519 | 520 | assert.NoError(t, server.Close()) 521 | 522 | assert.Empty(t, server.queries, "Queries not cleaned up after server close") 523 | } 524 | 525 | func TestQueryRespectClose(t *testing.T) { 526 | lim := test.TimeOut(time.Second * 10) 527 | defer lim.Stop() 528 | 529 | report := test.CheckRoutines(t) 530 | defer report() 531 | 532 | aSock := createListener4(t) 533 | 534 | server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) 535 | check(t, err) 536 | 537 | go func() { 538 | time.Sleep(3 * time.Second) 539 | check(t, server.Close()) 540 | }() 541 | 542 | _, _, err = server.QueryAddr(context.TODO(), "invalid-host") 543 | assert.ErrorIsf(t, err, errConnectionClosed, "Query on closed server but returned unexpected error %v", err) 544 | 545 | _, _, err = server.QueryAddr(context.TODO(), "invalid-host") 546 | assert.ErrorIsf(t, err, errConnectionClosed, "Query on closed server but returned unexpected error %v", err) 547 | 548 | assert.Empty(t, server.queries, "Queries not cleaned up after server close") 549 | } 550 | 551 | func TestResourceParsing(t *testing.T) { 552 | lookForIP := func(t *testing.T, msg dnsmessage.Message, expectedIP []byte) { 553 | t.Helper() 554 | 555 | buf, err := msg.Pack() 556 | assert.NoError(t, err) 557 | 558 | var parser dnsmessage.Parser 559 | _, err = parser.Start(buf) 560 | assert.NoError(t, err) 561 | 562 | assert.NoError(t, parser.SkipAllQuestions()) 563 | 564 | h, err := parser.AnswerHeader() 565 | assert.NoError(t, err) 566 | 567 | actualAddr, err := addrFromAnswerHeader(h, parser) 568 | assert.NoError(t, err) 569 | 570 | assert.Equalf( 571 | t, 572 | expectedIP, 573 | actualAddr.AsSlice(), 574 | "Expected(%v) and Actual(%v) IP don't match", 575 | expectedIP, 576 | actualAddr.AsSlice(), 577 | ) 578 | } 579 | 580 | name := "test-server." 581 | 582 | t.Run("A Record", func(t *testing.T) { 583 | answer, err := createAnswer(1, name, mustAddr(t, net.IP{127, 0, 0, 1})) 584 | assert.NoError(t, err) 585 | lookForIP(t, answer, []byte{127, 0, 0, 1}) 586 | }) 587 | 588 | t.Run("AAAA Record", func(t *testing.T) { 589 | answer, err := createAnswer(1, name, netip.MustParseAddr("::1")) 590 | assert.NoError(t, err) 591 | lookForIP(t, answer, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) 592 | }) 593 | } 594 | 595 | func mustAddr(t *testing.T, ip net.IP) netip.Addr { 596 | t.Helper() 597 | addr, ok := netip.AddrFromSlice(ip) 598 | assert.True(t, ok) 599 | 600 | return addr 601 | } 602 | 603 | func TestIPToBytes(t *testing.T) { //nolint:cyclop 604 | expectedIP := []byte{127, 0, 0, 1} 605 | actualAddr4, err := ipv4ToBytes(netip.MustParseAddr("127.0.0.1")) 606 | assert.NoError(t, err) 607 | assert.Equalf(t, expectedIP, actualAddr4[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr4) 608 | 609 | expectedIP = []byte{0, 0, 0, 1} 610 | actualAddr4, err = ipv4ToBytes(netip.MustParseAddr("0.0.0.1")) 611 | assert.NoError(t, err) 612 | assert.Equalf(t, expectedIP, actualAddr4[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr4) 613 | 614 | expectedIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} 615 | actualAddr6, err := ipv6ToBytes(netip.MustParseAddr("::1")) 616 | assert.NoError(t, err) 617 | assert.Equalf(t, expectedIP, actualAddr6[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr6) 618 | 619 | _, err = ipv4ToBytes(netip.MustParseAddr("::1")) 620 | assert.Error(t, err, "::1 should not be output to IPv4 bytes") 621 | 622 | expectedIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1} 623 | addr, ok := netip.AddrFromSlice(net.ParseIP("127.0.0.1")) 624 | assert.True(t, ok, "expected to be able to convert IP to netip.Addr") 625 | actualAddr6, err = ipv6ToBytes(addr) 626 | assert.NoError(t, err) 627 | assert.Equalf(t, expectedIP, actualAddr6[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr6) 628 | } 629 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package mdns 5 | 6 | import "errors" 7 | 8 | var ( 9 | errJoiningMulticastGroup = errors.New("mDNS: failed to join multicast group") 10 | errConnectionClosed = errors.New("mDNS: connection is closed") 11 | errContextElapsed = errors.New("mDNS: context has elapsed") 12 | errNilConfig = errors.New("mDNS: config must not be nil") 13 | errFailedCast = errors.New("mDNS: failed to cast listener to UDPAddr") 14 | ) 15 | -------------------------------------------------------------------------------- /examples/query/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // This example program showcases the use of the mDNS client by querying a previously published address 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "net" 11 | "os" 12 | 13 | "github.com/pion/mdns/v2" 14 | "golang.org/x/net/ipv4" 15 | "golang.org/x/net/ipv6" 16 | ) 17 | 18 | func main() { //nolint:cyclop 19 | var useV4, useV6 bool 20 | if len(os.Args) > 1 { 21 | switch os.Args[1] { 22 | case "-v4only": 23 | useV4 = true 24 | useV6 = false 25 | case "-v6only": 26 | useV4 = false 27 | useV6 = true 28 | default: 29 | useV4 = true 30 | useV6 = true 31 | } 32 | } else { 33 | useV4 = true 34 | useV6 = true 35 | } 36 | 37 | var packetConnV4 *ipv4.PacketConn 38 | if useV4 { 39 | addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | l4, err := net.ListenUDP("udp4", addr4) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | packetConnV4 = ipv4.NewPacketConn(l4) 50 | } 51 | 52 | var packetConnV6 *ipv6.PacketConn 53 | if useV6 { 54 | addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | l6, err := net.ListenUDP("udp6", addr6) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | packetConnV6 = ipv6.NewPacketConn(l6) 65 | } 66 | 67 | server, err := mdns.Server(packetConnV4, packetConnV6, &mdns.Config{}) 68 | if err != nil { 69 | panic(err) 70 | } 71 | answer, src, err := server.QueryAddr(context.TODO(), "pion-test.local") 72 | fmt.Println(answer) 73 | fmt.Println(src) 74 | fmt.Println(err) 75 | } 76 | -------------------------------------------------------------------------------- /examples/server/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // This example program showcases the use of the mDNS server by publishing "pion-test.local" 5 | package main 6 | 7 | import ( 8 | "net" 9 | 10 | "github.com/pion/mdns/v2" 11 | "golang.org/x/net/ipv4" 12 | "golang.org/x/net/ipv6" 13 | ) 14 | 15 | func main() { 16 | addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | l4, err := net.ListenUDP("udp4", addr4) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | l6, err := net.ListenUDP("udp6", addr6) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ 37 | LocalNames: []string{"pion-test.local"}, 38 | }) 39 | if err != nil { 40 | panic(err) 41 | } 42 | select {} 43 | } 44 | -------------------------------------------------------------------------------- /examples/server/publish_ip/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // This example program allows to set an IP that deviates from the automatically determined interface address. 5 | // Use the "-ip" parameter to set an IP. If not set, the example server defaults to "1.2.3.4". 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "net" 11 | 12 | "github.com/pion/mdns/v2" 13 | "golang.org/x/net/ipv4" 14 | "golang.org/x/net/ipv6" 15 | ) 16 | 17 | func main() { 18 | ip := flag.String("ip", "1.2.3.4", "IP address to be published") 19 | flag.Parse() 20 | 21 | addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | l4, err := net.ListenUDP("udp4", addr4) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | l6, err := net.ListenUDP("udp6", addr6) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ 42 | LocalNames: []string{"pion-test.local"}, 43 | LocalAddress: net.ParseIP(*ip), 44 | }) 45 | if err != nil { 46 | panic(err) 47 | } 48 | select {} 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/mdns/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/pion/logging v0.2.3 7 | github.com/pion/transport/v3 v3.0.7 8 | github.com/stretchr/testify v1.10.0 9 | golang.org/x/net v0.35.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | golang.org/x/sys v0.30.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= 4 | github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= 5 | github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 6 | github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 10 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 12 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 13 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 14 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /mdns.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package mdns implements mDNS (multicast DNS) 5 | package mdns 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>pion/renovate-config" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------