pax_global_header00006660000000000000000000000064151064520740014516gustar00rootroot0000000000000052 comment=339008c88b3e25493223ad5289fd2efffd2c6422 mdns-2.1.0/000077500000000000000000000000001510645207400124575ustar00rootroot00000000000000mdns-2.1.0/.github/000077500000000000000000000000001510645207400140175ustar00rootroot00000000000000mdns-2.1.0/.github/.gitignore000066400000000000000000000001561510645207400160110ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT .goassets mdns-2.1.0/.github/fetch-scripts.sh000077500000000000000000000016001510645207400171310ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-master} if [ -d "${GOASSETS_PATH}" ]; then if ! git -C "${GOASSETS_PATH}" diff --exit-code; then echo "${GOASSETS_PATH} has uncommitted changes" >&2 exit 1 fi git -C "${GOASSETS_PATH}" fetch origin git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} else git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" fi mdns-2.1.0/.github/install-hooks.sh000077500000000000000000000012421510645207400171440ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT SCRIPT_PATH="$(realpath "$(dirname "$0")")" . ${SCRIPT_PATH}/fetch-scripts.sh cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" mdns-2.1.0/.github/workflows/000077500000000000000000000000001510645207400160545ustar00rootroot00000000000000mdns-2.1.0/.github/workflows/api.yaml000066400000000000000000000011141510645207400175060ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: API on: pull_request: jobs: check: uses: pion/.goassets/.github/workflows/api.reusable.yml@master mdns-2.1.0/.github/workflows/codeql-analysis.yml000066400000000000000000000013201510645207400216630ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: CodeQL on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - master paths: - '**.go' jobs: analyze: uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master mdns-2.1.0/.github/workflows/fuzz.yaml000066400000000000000000000013421510645207400177360ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fuzz on: push: branches: - master schedule: - cron: "0 */8 * * *" jobs: fuzz: uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version fuzz-time: "60s" mdns-2.1.0/.github/workflows/lint.yaml000066400000000000000000000011151510645207400177040ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Lint on: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/lint.reusable.yml@master mdns-2.1.0/.github/workflows/release.yml000066400000000000000000000012501510645207400202150ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Release on: push: tags: - 'v*' jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version mdns-2.1.0/.github/workflows/renovate-go-sum-fix.yaml000066400000000000000000000012671510645207400225620ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fix go.sum on: push: branches: - renovate/* jobs: fix: uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} mdns-2.1.0/.github/workflows/reuse.yml000066400000000000000000000011511510645207400177200ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: REUSE Compliance Check on: push: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master mdns-2.1.0/.github/workflows/test.yaml000066400000000000000000000033271510645207400177240ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Test on: push: branches: - master pull_request: jobs: test: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} secrets: inherit test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-windows: uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-macos: uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@master strategy: matrix: go: ["1.25", "1.24"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version secrets: inherit mdns-2.1.0/.github/workflows/tidy-check.yaml000066400000000000000000000013021510645207400207600ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: push: branches: - master jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: go-version: "1.25" # auto-update/latest-go-version mdns-2.1.0/.gitignore000066400000000000000000000006321510645207400144500ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT ### JetBrains IDE ### ##################### .idea/ ### Emacs Temporary Files ### ############################# *~ ### Folders ### ############### bin/ vendor/ node_modules/ ### Files ### ############# *.ivf *.ogg tags cover.out *.sw[poe] *.wasm examples/sfu-ws/cert.pem examples/sfu-ws/key.pem wasm_exec.js mdns-2.1.0/.golangci.yml000066400000000000000000000202661510645207400150510ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT version: "2" linters: enable: - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - bidichk # Checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - containedctx # containedctx is a linter that detects struct contained context.Context field - contextcheck # check the function whether use a non-inherited context - cyclop # checks function and package cyclomatic complexity - decorder # check declaration order and count of types, constants, variables and functions - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together - err113 # Golang linter to check the errors handling expressions - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - 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. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - 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. - exhaustive # check exhaustiveness of enum switch statements - forbidigo # Forbids identifiers - forcetypeassert # finds forced type assertions - gochecknoglobals # Checks that no globals are present in Go code - gocognit # Computes and checks the cognitive complexity of functions - goconst # Finds repeated strings that could be replaced by a constant - gocritic # The most opinionated Go source code linter - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - godox # Tool for detection of FIXME, TODO and other comment keywords - goheader # Checks is file header matches to pattern - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - goprintffuncname # Checks that printf-like functions are named with `f` at the end - gosec # Inspects source code for security problems - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - grouper # An analyzer to analyze expression groups. - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - misspell # Finds commonly misspelled English words in comments - nakedret # Finds naked returns in functions greater than a specified function length - nestif # Reports deeply nested if statements - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - noctx # noctx finds sending http request without context.Context - predeclared # find code that shadows one of Go's predeclared identifiers - revive # golint replacement, finds style mistakes - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - tagliatelle # Checks the struct tags. - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - varnamelen # checks that the length of a variable's name matches its scope - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - depguard # Go linter that checks if package imports are in a list of acceptable packages - funlen # Tool for detection of long functions - gochecknoinits # Checks that no init functions are present in Go code - 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. - interfacebloat # A linter that checks length of interface. - ireturn # Accept Interfaces, Return Concrete Types - mnd # An analyzer to detect magic numbers - nolintlint # Reports ill-formed or insufficient nolint directives - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test - prealloc # Finds slice declarations that could potentially be preallocated - promlinter # Check Prometheus metrics naming via promlint - rowserrcheck # checks whether Err of rows is checked successfully - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. - testpackage # linter that makes you use a separate _test package - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - wrapcheck # Checks that errors returned from external packages are wrapped - wsl # Whitespace Linter - Forces you to use empty lines! settings: staticcheck: checks: - all - -QF1008 # "could remove embedded field", to keep it explicit! - -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive! exhaustive: default-signifies-exhaustive: true forbidigo: forbid: - pattern: ^fmt.Print(f|ln)?$ - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ - pattern: ^os.Exit$ - pattern: ^panic$ - pattern: ^print(ln)?$ - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ pkg: ^testing$ msg: use testify/assert instead analyze-types: true gomodguard: blocked: modules: - github.com/pkg/errors: recommendations: - errors govet: enable: - shadow revive: rules: # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility - name: use-any severity: warning disabled: false misspell: locale: US varnamelen: max-distance: 12 min-name-length: 2 ignore-type-assert-ok: true ignore-map-index-ok: true ignore-chan-recv-ok: true ignore-decls: - i int - n int - w io.Writer - r io.Reader - b []byte exclusions: generated: lax rules: - linters: - forbidigo - gocognit path: (examples|main\.go) - linters: - gocognit path: _test\.go - linters: - forbidigo path: cmd formatters: enable: - gci # Gci control golang package import order and make it always deterministic. - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofumpt # Gofumpt checks whether code was gofumpt-ed. - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports exclusions: generated: lax mdns-2.1.0/.goreleaser.yml000066400000000000000000000001711510645207400154070ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT builds: - skip: true mdns-2.1.0/.reuse/000077500000000000000000000000001510645207400136605ustar00rootroot00000000000000mdns-2.1.0/.reuse/dep5000066400000000000000000000011141510645207400144350ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ 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 yarn.lock Copyright: 2023 The Pion community License: MIT Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 mdns-2.1.0/LICENSE000066400000000000000000000020411510645207400134610ustar00rootroot00000000000000MIT License Copyright (c) 2018 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mdns-2.1.0/LICENSES/000077500000000000000000000000001510645207400136645ustar00rootroot00000000000000mdns-2.1.0/LICENSES/MIT.txt000066400000000000000000000020661510645207400150620ustar00rootroot00000000000000MIT License Copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mdns-2.1.0/README.md000066400000000000000000000062121510645207400137370ustar00rootroot00000000000000


Pion mDNS

A Go implementation of mDNS

Pion mDNS join us on Discord Follow us on Bluesky
GitHub Workflow Status Go Reference Coverage Status Go Report Card License: MIT


Go mDNS implementation. The original user is Pion WebRTC, but we would love to see it work for everyone. ### Running Server For a mDNS server that responds to queries for `pion-test.local` ```sh go run examples/server/main.go ``` For a mDNS server that responds to queries for `pion-test.local` with a given address ```sh go run examples/server/publish_ip/main.go -ip=[IP] ``` If you don't set the `ip` parameter, "1.2.3.4" will be used instead. ### Running Client To query using Pion you can run the `query` example ```sh go run examples/query/main.go ``` You can use the macOS client ``` dns-sd -q pion-test.local ``` Or the avahi client ``` avahi-resolve -a pion-test.local ``` ### RFCs #### Implemented - **RFC 6762** [Multicast DNS][rfc6762] - **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) [rfc6762]: https://tools.ietf.org/html/rfc6762 ### Roadmap 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. ### Community Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! 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) ### Contributing Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible ### License MIT License - see [LICENSE](LICENSE) for full textmdns-2.1.0/codecov.yml000066400000000000000000000007151510645207400146270ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" mdns-2.1.0/config.go000066400000000000000000000026051510645207400142560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package mdns import ( "net" "time" "github.com/pion/logging" ) const ( // DefaultAddressIPv4 is the default used by mDNS // and in most cases should be the address that the // ipv4.PacketConn passed to Server is bound to. DefaultAddressIPv4 = "224.0.0.0:5353" // DefaultAddressIPv6 is the default IPv6 address used // by mDNS and in most cases should be the address that // the ipv6.PacketConn passed to Server is bound to. DefaultAddressIPv6 = "[FF02::]:5353" ) // Config is used to configure a mDNS client or server. type Config struct { // Name is the name of the client/server used for logging purposes. Name string // QueryInterval controls how often we sends Queries until we // get a response for the requested name QueryInterval time.Duration // LocalNames are the names that we will generate answers for // when we get questions LocalNames []string // LocalAddress will override the published address with the given IP // when set. Otherwise, the automatically determined address will be used. LocalAddress net.IP LoggerFactory logging.LoggerFactory // IncludeLoopback will include loopback interfaces to be eligble for queries and answers. IncludeLoopback bool // Interfaces will override the interfaces used for queries and answers. Interfaces []net.Interface } mdns-2.1.0/conn.go000066400000000000000000001113711510645207400137470ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package mdns import ( "context" "errors" "fmt" "net" "net/netip" "strings" "sync" "time" "github.com/pion/logging" "golang.org/x/net/dns/dnsmessage" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // Conn represents a mDNS Server. type Conn struct { mu sync.RWMutex name string log logging.LeveledLogger multicastPktConnV4 ipPacketConn multicastPktConnV6 ipPacketConn dstAddr4 *net.UDPAddr dstAddr6 *net.UDPAddr unicastPktConnV4 ipPacketConn unicastPktConnV6 ipPacketConn queryInterval time.Duration localNames []string queries []*query ifaces map[int]netInterface closed chan any } type query struct { nameWithSuffix string queryResultChan chan queryResult } type queryResult struct { answer dnsmessage.ResourceHeader addr netip.Addr } const ( defaultQueryInterval = time.Second destinationAddress4 = "224.0.0.251:5353" destinationAddress6 = "[FF02::FB]:5353" maxMessageRecords = 3 responseTTL = 120 // maxPacketSize is the maximum size of a mdns packet. // From RFC 6762: // Even when fragmentation is used, a Multicast DNS packet, including IP // and UDP headers, MUST NOT exceed 9000 bytes. // https://datatracker.ietf.org/doc/html/rfc6762#section-17 maxPacketSize = 9000 ) var ( errNoPositiveMTUFound = errors.New("no positive MTU found") errNoPacketConn = errors.New("must supply at least a multicast IPv4 or IPv6 PacketConn") errNoUsableInterfaces = errors.New("no usable interfaces found for mDNS") errFailedToClose = errors.New("failed to close mDNS Conn") errFailedToDecodeAddrFromAResource = errors.New("failed to decode netip.Addr from A type Resource") errFailedToDecodeAddrFromAAAAResource = errors.New("failed to decode netip.Addr from AAAA type Resource") errUnhandledAnswerHeaderType = errors.New("header for Answer had unhandled type") ) type netInterface struct { net.Interface ipAddrs []netip.Addr supportsV4 bool supportsV6 bool } // Server establishes a mDNS connection over an existing conn. // Either one or both of the multicast packet conns should be provided. // The presence of each IP type of PacketConn will dictate what kinds // of questions are sent for queries. That is, if an ipv6.PacketConn is // provided, then AAAA questions will be sent. A questions will only be // sent if an ipv4.PacketConn is also provided. In the future, we may // add a QueryAddr method that allows specifying this more clearly. // //nolint:gocognit,gocyclo,cyclop,maintidx func Server( multicastPktConnV4 *ipv4.PacketConn, multicastPktConnV6 *ipv6.PacketConn, config *Config, ) (*Conn, error) { if config == nil { return nil, errNilConfig } loggerFactory := config.LoggerFactory if loggerFactory == nil { loggerFactory = logging.NewDefaultLoggerFactory() } log := loggerFactory.NewLogger("mdns") conn := &Conn{ queryInterval: defaultQueryInterval, log: log, closed: make(chan any), } conn.name = config.Name if conn.name == "" { conn.name = fmt.Sprintf("%p", &conn) } if multicastPktConnV4 == nil && multicastPktConnV6 == nil { return nil, errNoPacketConn } ifaces := config.Interfaces if ifaces == nil { var err error ifaces, err = net.Interfaces() if err != nil { return nil, err } } var unicastPktConnV4 *ipv4.PacketConn { addr4, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0") if err != nil { return nil, err } unicastConnV4, err := net.ListenUDP("udp4", addr4) if err != nil { log.Warnf( "[%s] failed to listen on unicast IPv4 %s: %s; will not be able to receive unicast responses on IPv4", conn.name, addr4, err, ) } else { unicastPktConnV4 = ipv4.NewPacketConn(unicastConnV4) } } var unicastPktConnV6 *ipv6.PacketConn { addr6, err := net.ResolveUDPAddr("udp6", "[::]:") if err != nil { return nil, err } unicastConnV6, err := net.ListenUDP("udp6", addr6) if err != nil { log.Warnf( "[%s] failed to listen on unicast IPv6 %s: %s; will not be able to receive unicast responses on IPv6", conn.name, addr6, err, ) } else { unicastPktConnV6 = ipv6.NewPacketConn(unicastConnV6) } } multicastGroup4 := net.IPv4(224, 0, 0, 251) multicastGroupAddr4 := &net.UDPAddr{IP: multicastGroup4} // FF02::FB multicastGroup6 := net.IP{0xff, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfb} multicastGroupAddr6 := &net.UDPAddr{IP: multicastGroup6} inboundBufferSize := 0 joinErrCount := 0 ifacesToUse := make(map[int]netInterface, len(ifaces)) for i := range ifaces { ifc := ifaces[i] if !config.IncludeLoopback && ifc.Flags&net.FlagLoopback == net.FlagLoopback { continue } if ifc.Flags&net.FlagUp == 0 { continue } addrs, err := ifc.Addrs() if err != nil { continue } var supportsV4, supportsV6 bool ifcIPAddrs := make([]netip.Addr, 0, len(addrs)) for _, addr := range addrs { var ipToConv net.IP switch addr := addr.(type) { case *net.IPNet: ipToConv = addr.IP case *net.IPAddr: ipToConv = addr.IP default: continue } ipAddr, ok := netip.AddrFromSlice(ipToConv) if !ok { continue } if multicastPktConnV4 != nil { // don't want mapping since we also support IPv4/A ipAddr = ipAddr.Unmap() } ipAddr = addrWithOptionalZone(ipAddr, ifc.Name) if ipAddr.Is6() && !ipAddr.Is4In6() { supportsV6 = true } else { // we'll claim we support v4 but defer if we send it or not // based on IPv4-to-IPv6 mapping rules later (search for Is4In6 below) supportsV4 = true } ifcIPAddrs = append(ifcIPAddrs, ipAddr) } if !supportsV4 && !supportsV6 { continue } var atLeastOneJoin bool if supportsV4 && multicastPktConnV4 != nil { if err := multicastPktConnV4.JoinGroup(&ifc, multicastGroupAddr4); err == nil { atLeastOneJoin = true } } if supportsV6 && multicastPktConnV6 != nil { if err := multicastPktConnV6.JoinGroup(&ifc, multicastGroupAddr6); err == nil { atLeastOneJoin = true } } if !atLeastOneJoin { joinErrCount++ continue } ifacesToUse[ifc.Index] = netInterface{ Interface: ifc, ipAddrs: ifcIPAddrs, supportsV4: supportsV4, supportsV6: supportsV6, } if ifc.MTU > inboundBufferSize { inboundBufferSize = ifc.MTU } } if len(ifacesToUse) == 0 { return nil, errNoUsableInterfaces } if inboundBufferSize == 0 { return nil, errNoPositiveMTUFound } if inboundBufferSize > maxPacketSize { inboundBufferSize = maxPacketSize } if joinErrCount >= len(ifaces) { return nil, errJoiningMulticastGroup } dstAddr4, err := net.ResolveUDPAddr("udp4", destinationAddress4) if err != nil { return nil, err } dstAddr6, err := net.ResolveUDPAddr("udp6", destinationAddress6) if err != nil { return nil, err } var localNames []string for _, l := range config.LocalNames { localNames = append(localNames, l+".") } conn.dstAddr4 = dstAddr4 conn.dstAddr6 = dstAddr6 conn.localNames = localNames conn.ifaces = ifacesToUse if config.QueryInterval != 0 { conn.queryInterval = config.QueryInterval } if multicastPktConnV4 != nil { if err := multicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { conn.log.Warnf( "[%s] failed to SetControlMessage(ipv4.FlagInterface) on multicast IPv4 PacketConn %v", conn.name, err, ) } if err := multicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagDst) on multicast IPv4 PacketConn %v", conn.name, err) } conn.multicastPktConnV4 = ipPacketConn4{conn.name, multicastPktConnV4, log} } if multicastPktConnV6 != nil { if err := multicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { conn.log.Warnf( "[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", conn.name, err, ) } if err := multicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { conn.log.Warnf( "[%s] failed to SetControlMessage(ipv6.FlagInterface) on multicast IPv6 PacketConn %v", conn.name, err, ) } conn.multicastPktConnV6 = ipPacketConn6{conn.name, multicastPktConnV6, log} } if unicastPktConnV4 != nil { if err := unicastPktConnV4.SetControlMessage(ipv4.FlagInterface, true); err != nil { conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", conn.name, err) } if err := unicastPktConnV4.SetControlMessage(ipv4.FlagDst, true); err != nil { conn.log.Warnf("[%s] failed to SetControlMessage(ipv4.FlagInterface) on unicast IPv4 PacketConn %v", conn.name, err) } conn.unicastPktConnV4 = ipPacketConn4{conn.name, unicastPktConnV4, log} } if unicastPktConnV6 != nil { if err := unicastPktConnV6.SetControlMessage(ipv6.FlagInterface, true); err != nil { conn.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", conn.name, err) } if err := unicastPktConnV6.SetControlMessage(ipv6.FlagDst, true); err != nil { conn.log.Warnf("[%s] failed to SetControlMessage(ipv6.FlagInterface) on unicast IPv6 PacketConn %v", conn.name, err) } conn.unicastPktConnV6 = ipPacketConn6{conn.name, unicastPktConnV6, log} } if config.IncludeLoopback { //nolint:nestif // this is an efficient way for us to send ourselves a message faster instead of it going // further out into the network stack. if multicastPktConnV4 != nil { if err := multicastPktConnV4.SetMulticastLoopback(true); err != nil { conn.log.Warnf( //nolint:lll "[%s] failed to SetMulticastLoopback(true) on multicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", conn.name, err, ) } } if multicastPktConnV6 != nil { if err := multicastPktConnV6.SetMulticastLoopback(true); err != nil { conn.log.Warnf( //nolint:lll "[%s] failed to SetMulticastLoopback(true) on multicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", conn.name, err, ) } } if unicastPktConnV4 != nil { if err := unicastPktConnV4.SetMulticastLoopback(true); err != nil { conn.log.Warnf( //nolint:lll "[%s] failed to SetMulticastLoopback(true) on unicast IPv4 PacketConn %v; this may cause inefficient network path c.name,communications", conn.name, err, ) } } if unicastPktConnV6 != nil { if err := unicastPktConnV6.SetMulticastLoopback(true); err != nil { conn.log.Warnf( //nolint:lll "[%s] failed to SetMulticastLoopback(true) on unicast IPv6 PacketConn %v; this may cause inefficient network path c.name,communications", conn.name, err, ) } } } // https://www.rfc-editor.org/rfc/rfc6762.html#section-17 // Multicast DNS messages carried by UDP may be up to the IP MTU of the // physical interface, less the space required for the IP header (20 // bytes for IPv4; 40 bytes for IPv6) and the UDP header (8 bytes). started := make(chan struct{}) go conn.start(started, inboundBufferSize-20-8, config) <-started return conn, nil } // Close closes the mDNS Conn. func (c *Conn) Close() error { //nolint:cyclop select { case <-c.closed: return nil default: } // Once on go1.20, can use errors.Join var errs []error if c.multicastPktConnV4 != nil { if err := c.multicastPktConnV4.Close(); err != nil { errs = append(errs, err) } } if c.multicastPktConnV6 != nil { if err := c.multicastPktConnV6.Close(); err != nil { errs = append(errs, err) } } if c.unicastPktConnV4 != nil { if err := c.unicastPktConnV4.Close(); err != nil { errs = append(errs, err) } } if c.unicastPktConnV6 != nil { if err := c.unicastPktConnV6.Close(); err != nil { errs = append(errs, err) } } if len(errs) == 0 { <-c.closed return nil } rtrn := errFailedToClose for _, err := range errs { rtrn = fmt.Errorf("%w\n%w", err, rtrn) } return rtrn } // Query sends mDNS Queries for the following name until // either the Context is canceled/expires or we get a result // // Deprecated: Use QueryAddr instead as it supports the easier to use netip.Addr. func (c *Conn) Query(ctx context.Context, name string) (dnsmessage.ResourceHeader, net.Addr, error) { header, addr, err := c.QueryAddr(ctx, name) if err != nil { return header, nil, err } return header, &net.IPAddr{ IP: addr.AsSlice(), Zone: addr.Zone(), }, nil } // QueryAddr sends mDNS Queries for the following name until // either the Context is canceled/expires or we get a result. func (c *Conn) QueryAddr(ctx context.Context, name string) (dnsmessage.ResourceHeader, netip.Addr, error) { select { case <-c.closed: return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed default: } nameWithSuffix := name + "." queryChan := make(chan queryResult, 1) query := &query{nameWithSuffix, queryChan} c.mu.Lock() c.queries = append(c.queries, query) c.mu.Unlock() defer func() { c.mu.Lock() defer c.mu.Unlock() for i := len(c.queries) - 1; i >= 0; i-- { if c.queries[i] == query { c.queries = append(c.queries[:i], c.queries[i+1:]...) } } }() ticker := time.NewTicker(c.queryInterval) defer ticker.Stop() c.sendQuestion(nameWithSuffix) for { select { case <-ticker.C: c.sendQuestion(nameWithSuffix) case <-c.closed: return dnsmessage.ResourceHeader{}, netip.Addr{}, errConnectionClosed case res := <-queryChan: // Given https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-mdns-ice-candidates#section-3.2.2-2 // An ICE agent SHOULD ignore candidates where the hostname resolution returns more than one IP address. // // We will take the first we receive which could result in a race between two suitable addresses where // one is better than the other (e.g. localhost vs LAN). return res.answer, res.addr, nil case <-ctx.Done(): return dnsmessage.ResourceHeader{}, netip.Addr{}, errContextElapsed } } } type ipToBytesError struct { addr netip.Addr expectedType string } func (err ipToBytesError) Error() string { return fmt.Sprintf("ip (%s) is not %s", err.addr, err.expectedType) } // assumes ipv4-to-ipv6 mapping has been checked. func ipv4ToBytes(ipAddr netip.Addr) ([4]byte, error) { if !ipAddr.Is4() { return [4]byte{}, ipToBytesError{ipAddr, "IPv4"} } md, err := ipAddr.MarshalBinary() if err != nil { return [4]byte{}, err } // net.IPs are stored in big endian / network byte order var out [4]byte copy(out[:], md) return out, nil } // assumes ipv4-to-ipv6 mapping has been checked. func ipv6ToBytes(ipAddr netip.Addr) ([16]byte, error) { if !ipAddr.Is6() { return [16]byte{}, ipToBytesError{ipAddr, "IPv6"} } md, err := ipAddr.MarshalBinary() if err != nil { return [16]byte{}, err } // net.IPs are stored in big endian / network byte order var out [16]byte copy(out[:], md) return out, nil } type ipToAddrError struct { ip []byte } func (err ipToAddrError) Error() string { return fmt.Sprintf("failed to convert ip address '%s' to netip.Addr", err.ip) } func interfaceForRemote(remote string) (*netip.Addr, error) { conn, err := net.Dial("udp", remote) //nolint: noctx if err != nil { return nil, err } localAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { return nil, errFailedCast } if err := conn.Close(); err != nil { return nil, err } ipAddr, ok := netip.AddrFromSlice(localAddr.IP) if !ok { return nil, ipToAddrError{localAddr.IP} } ipAddr = addrWithOptionalZone(ipAddr, localAddr.Zone) return &ipAddr, nil } type writeType byte const ( writeTypeQuestion writeType = iota writeTypeAnswer ) func (c *Conn) sendQuestion(name string) { packedName, err := dnsmessage.NewName(name) if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } // https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-mdns-ice-candidates-04#section-3.2.1 // // 2. Otherwise, resolve the candidate using mDNS. The ICE agent // SHOULD set the unicast-response bit of the corresponding mDNS // query message; this minimizes multicast traffic, as the response // is probably only useful to the querying node. // // 18.12. Repurposing of Top Bit of qclass in Question Section // // In the Question Section of a Multicast DNS query, the top bit of the // qclass field is used to indicate that unicast responses are preferred // for this particular question. (See Section 5.4.) // // We'll follow this up sending on our unicast based packet connections so that we can // get a unicast response back. msg := dnsmessage.Message{ Header: dnsmessage.Header{}, } // limit what we ask for based on what IPv is available. In the future, // this could be an option since there's no reason you cannot get an // A record on an IPv6 sourced question and vice versa. if c.multicastPktConnV4 != nil { msg.Questions = append(msg.Questions, dnsmessage.Question{ Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET | (1 << 15), Name: packedName, }) } if c.multicastPktConnV6 != nil { msg.Questions = append(msg.Questions, dnsmessage.Question{ Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET | (1 << 15), Name: packedName, }) } rawQuery, err := msg.Pack() if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } c.writeToSocket(-1, rawQuery, false, false, writeTypeQuestion, nil) } //nolint:gocognit,gocyclo,cyclop func (c *Conn) writeToSocket( ifIndex int, b []byte, hasLoopbackData bool, hasIPv6Zone bool, wType writeType, unicastDst *net.UDPAddr, ) { var dst4, dst6 net.Addr if wType == writeTypeAnswer { //nolint:nestif if unicastDst == nil { dst4 = c.dstAddr4 dst6 = c.dstAddr6 } else { if unicastDst.IP.To4() == nil { dst6 = unicastDst } else { dst4 = unicastDst } } } if ifIndex != -1 { //nolint:nestif if wType == writeTypeQuestion { c.log.Errorf("[%s] Unexpected question using specific interface index %d; dropping question", c.name, ifIndex) return } ifc, ok := c.ifaces[ifIndex] if !ok { c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) return } if hasLoopbackData && ifc.Flags&net.FlagLoopback == 0 { // avoid accidentally tricking the destination that itself is the same as us c.log.Debugf("[%s] interface is not loopback %d", c.name, ifIndex) return } c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { if !hasIPv6Zone { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet on IPv4 interface %d: %v", c.name, ifIndex, err) } } else { c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet on IPv6 interface %d: %v", c.name, ifIndex, err) } } return } for ifcIdx := range c.ifaces { ifc := c.ifaces[ifcIdx] if hasLoopbackData { c.log.Debugf("[%s] Refusing to send loopback data with non-specific interface", c.name) continue } if wType == writeTypeQuestion { //nolint:nestif // we'll write via unicast if we can in case the responder chooses to respond to the address the request // came from (i.e. not respecting unicast-response bit). If we were to use the multicast packet // conn here, we'd be writing from a specific multicast address which won't be able to receive unicast // traffic (it only works when listening on 0.0.0.0/[::]). if c.unicastPktConnV4 == nil && c.unicastPktConnV6 == nil { c.log.Debugf("[%s] writing question to multicast IPv4/6 %s", c.name, c.dstAddr4) if ifc.supportsV4 && c.multicastPktConnV4 != nil { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifc.Index, err) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifc.Index, err) } } } if ifc.supportsV4 && c.unicastPktConnV4 != nil { c.log.Debugf("[%s] writing question to unicast IPv4 %s", c.name, c.dstAddr4) if _, err := c.unicastPktConnV4.WriteTo(b, &ifc.Interface, nil, c.dstAddr4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) } } if ifc.supportsV6 && c.unicastPktConnV6 != nil { c.log.Debugf("[%s] writing question to unicast IPv6 %s", c.name, c.dstAddr6) if _, err := c.unicastPktConnV6.WriteTo(b, &ifc.Interface, nil, c.dstAddr6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (unicast) on interface %d: %v", c.name, ifc.Index, err) } } } else { c.log.Debugf("[%s] writing answer to IPv4: %v, IPv6: %v", c.name, dst4, dst6) if ifc.supportsV4 && c.multicastPktConnV4 != nil && dst4 != nil { if !hasIPv6Zone { if _, err := c.multicastPktConnV4.WriteTo(b, &ifc.Interface, nil, dst4); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv4 interface %d: %v", c.name, ifIndex, err) } } else { c.log.Debugf("[%s] refusing to send mDNS packet with IPv6 zone over IPv4", c.name) } } if ifc.supportsV6 && c.multicastPktConnV6 != nil && dst6 != nil { if _, err := c.multicastPktConnV6.WriteTo(b, &ifc.Interface, nil, dst6); err != nil { c.log.Warnf("[%s] failed to send mDNS packet (multicast) on IPv6 interface %d: %v", c.name, ifIndex, err) } } } } } func createAnswer(id uint16, question dnsmessage.Question, addr netip.Addr, isUnicast bool, ) (dnsmessage.Message, error) { packedName, err := dnsmessage.NewName(question.Name.String()) if err != nil { return dnsmessage.Message{}, err } msg := dnsmessage.Message{ Header: dnsmessage.Header{ ID: id, Response: true, Authoritative: true, }, Answers: []dnsmessage.Resource{ { Header: dnsmessage.ResourceHeader{ Class: dnsmessage.ClassINET, Name: packedName, TTL: responseTTL, }, }, }, } // include question in answer if specified for this answer (such as unicast: Spec 6.7.) if isUnicast { msg.Questions = []dnsmessage.Question{question} } if addr.Is4() { ipBuf, err := ipv4ToBytes(addr) if err != nil { return dnsmessage.Message{}, err } msg.Answers[0].Header.Type = dnsmessage.TypeA msg.Answers[0].Body = &dnsmessage.AResource{ A: ipBuf, } } else if addr.Is6() { // we will lose the zone here, but the receiver can reconstruct it ipBuf, err := ipv6ToBytes(addr) if err != nil { return dnsmessage.Message{}, err } msg.Answers[0].Header.Type = dnsmessage.TypeAAAA msg.Answers[0].Body = &dnsmessage.AAAAResource{ AAAA: ipBuf, } } return msg, nil } func (c *Conn) sendAnswer(queryID uint16, question dnsmessage.Question, ifIndex int, result netip.Addr, dst *net.UDPAddr, isUnicast bool, ) { answer, err := createAnswer(queryID, question, result, isUnicast) if err != nil { c.log.Warnf("[%s] failed to create mDNS answer %v", c.name, err) return } rawAnswer, err := answer.Pack() if err != nil { c.log.Warnf("[%s] failed to construct mDNS packet %v", c.name, err) return } c.writeToSocket( ifIndex, rawAnswer, result.IsLoopback(), result.Is6() && result.Zone() != "", writeTypeAnswer, dst, ) } type ipControlMessage struct { IfIndex int Dst net.IP } type ipPacketConn interface { ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) Close() error } type ipPacketConn4 struct { name string conn *ipv4.PacketConn log logging.LeveledLogger } func (c ipPacketConn4) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { n, cm4, src, err := c.conn.ReadFrom(b) if err != nil || cm4 == nil { return n, nil, src, err } return n, &ipControlMessage{IfIndex: cm4.IfIndex, Dst: cm4.Dst}, src, err } func (c ipPacketConn4) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { var cm4 *ipv4.ControlMessage if cm != nil { cm4 = &ipv4.ControlMessage{ IfIndex: cm.IfIndex, } } if err := c.conn.SetMulticastInterface(via); err != nil { c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) return 0, err } return c.conn.WriteTo(b, cm4, dst) } func (c ipPacketConn4) Close() error { return c.conn.Close() } type ipPacketConn6 struct { name string conn *ipv6.PacketConn log logging.LeveledLogger } func (c ipPacketConn6) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { n, cm6, src, err := c.conn.ReadFrom(b) if err != nil || cm6 == nil { return n, nil, src, err } return n, &ipControlMessage{IfIndex: cm6.IfIndex, Dst: cm6.Dst}, src, err } func (c ipPacketConn6) WriteTo(b []byte, via *net.Interface, cm *ipControlMessage, dst net.Addr) (n int, err error) { var cm6 *ipv6.ControlMessage if cm != nil { cm6 = &ipv6.ControlMessage{ IfIndex: cm.IfIndex, } } if err := c.conn.SetMulticastInterface(via); err != nil { c.log.Warnf("[%s] failed to set multicast interface for %d: %v", c.name, via.Index, err) return 0, err } return c.conn.WriteTo(b, cm6, dst) } func (c ipPacketConn6) Close() error { return c.conn.Close() } //nolint:gocognit,gocyclo,cyclop,maintidx func (c *Conn) readLoop(name string, pktConn ipPacketConn, inboundBufferSize int, config *Config) { b := make([]byte, inboundBufferSize) for { n, cm, src, err := pktConn.ReadFrom(b) if err != nil { if errors.Is(err, net.ErrClosed) { return } c.log.Warnf("[%s] failed to ReadFrom %q %v", c.name, src, err) continue } c.log.Debugf("[%s] got read on %s from %s", c.name, name, src) var ifIndex int var pktDst net.IP if cm != nil { ifIndex = cm.IfIndex pktDst = cm.Dst } else { ifIndex = -1 } srcAddr, ok := src.(*net.UDPAddr) if !ok { c.log.Warnf("[%s] expected source address %s to be UDP but got %", c.name, src, src) continue } func() { var msg dnsmessage.Message err := msg.Unpack(b[:n]) if err != nil { c.log.Warnf("[%s] failed to parse mDNS packet %v", c.name, err) return } // Questions are often echoed with answers, therefore // If we have more questions than answers it is a question we might need to respond to if len(msg.Questions) > len(msg.Answers) { //nolint:nestif for _, question := range msg.Questions { if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA { continue } // https://datatracker.ietf.org/doc/html/rfc6762#section-6 // The destination UDP port in all Multicast DNS responses MUST be 5353, // and the destination address MUST be the mDNS IPv4 link-local // multicast address 224.0.0.251 or its IPv6 equivalent FF02::FB, except // when generating a reply to a query that explicitly requested a // unicast response isQU := (question.Class & (1 << 15)) != 0 // via the unicast-response bit isLegacy := srcAddr.Port != 5353 // by virtue of being a legacy query (Section 6.7) isDirect := len(pktDst) != 0 && !pktDst.Equal(c.dstAddr4.IP) && !pktDst.Equal(c.dstAddr6.IP) // by virtue of being a direct unicast query shouldReplyUnicast := isQU || isLegacy || isDirect var dst *net.UDPAddr if shouldReplyUnicast { dst = srcAddr } queryWantsV4 := question.Type == dnsmessage.TypeA for _, localName := range c.localNames { if strings.EqualFold(localName, question.Name.String()) { //nolint:nestif var localAddress *netip.Addr if config.LocalAddress != nil { // this means the LocalAddress does not support link-local since // we have no zone to set here. ipAddr, ok := netip.AddrFromSlice(config.LocalAddress) if !ok { c.log.Warnf("[%s] failed to convert config.LocalAddress '%s' to netip.Addr", c.name, config.LocalAddress) continue } if c.multicastPktConnV4 != nil { // don't want mapping since we also support IPv4/A ipAddr = ipAddr.Unmap() } localAddress = &ipAddr } else { // prefer the address of the interface if we know its index, but otherwise // derive it from the address we read from. We do this because even if // multicast loopback is in use or we send from a loopback interface, // there are still cases where the IP packet will contain the wrong // source IP (e.g. a LAN interface). // For example, we can have a packet that has: // Source: 192.168.65.3 // Destination: 224.0.0.251 // Interface Index: 1 // Interface Addresses @ 1: [127.0.0.1/8 ::1/128] if ifIndex != -1 { ifc, ok := c.ifaces[ifIndex] if !ok { c.log.Warnf("[%s] no interface for %d", c.name, ifIndex) return } var selectedAddrs []netip.Addr for _, addr := range ifc.ipAddrs { addrCopy := addr // match up respective IP types based on question if queryWantsV4 { if addrCopy.Is4In6() { // we may allow 4-in-6, but the question wants an A record addrCopy = addrCopy.Unmap() } if !addrCopy.Is4() { continue } } else { // queryWantsV6 if !addrCopy.Is6() { continue } if !isSupportedIPv6(addrCopy, c.multicastPktConnV4 == nil) { c.log.Debugf("[%s] interface %d address not a supported IPv6 address %s", c.name, ifIndex, &addrCopy) continue } } selectedAddrs = append(selectedAddrs, addrCopy) } if len(selectedAddrs) == 0 { c.log.Debugf( "[%s] failed to find suitable IP for interface %d; deriving address from source address c.name,instead", c.name, ifIndex, ) } else { // choose the best match var choice *netip.Addr for _, option := range selectedAddrs { optCopy := option if option.Is4() { // select first choice = &optCopy break } // we're okay with 4In6 for now but ideally we get a an actual IPv6. // Maybe in the future we never want this but it does look like Docker // can route IPv4 over IPv6. if choice == nil || !optCopy.Is4In6() { choice = &optCopy } if !optCopy.Is4In6() { break } // otherwise keep searching for an actual IPv6 } localAddress = choice } } if ifIndex == -1 || localAddress == nil { localAddress, err = interfaceForRemote(src.String()) if err != nil { c.log.Warnf("[%s] failed to get local interface to communicate with %s: %v", c.name, src.String(), err) continue } } } if queryWantsV4 { if !localAddress.Is4() { c.log.Debugf( "[%s] have IPv6 address %s to respond with but question is for A not c.name,AAAA", c.name, localAddress, ) continue } } else { if !localAddress.Is6() { c.log.Debugf( "[%s] have IPv4 address %s to respond with but question is for AAAA not c.name,A", c.name, localAddress, ) continue } if !isSupportedIPv6(*localAddress, c.multicastPktConnV4 == nil) { c.log.Debugf("[%s] got local interface address but not a supported IPv6 address %v", c.name, localAddress) continue } } if dst != nil && len(dst.IP) == net.IPv4len && localAddress.Is6() && localAddress.Zone() != "" && (localAddress.IsLinkLocalUnicast() || localAddress.IsLinkLocalMulticast()) { // This case happens when multicast v4 picks up an AAAA question that has a zone // in the address. Since we cannot send this zone over DNS (it's meaningless), // the other side can only infer this via the response interface on the other // side (some IPv6 interface). c.log.Debugf("[%s] refusing to send link-local address %s to an IPv4 destination %s", c.name, localAddress, dst) continue } c.log.Debugf( "[%s] sending response for %s on ifc %d of %s to %s", c.name, question.Name, ifIndex, *localAddress, dst, ) c.sendAnswer(msg.Header.ID, question, ifIndex, *localAddress, dst, shouldReplyUnicast) } } } } else { for _, answer := range msg.Answers { if answer.Header.Type != dnsmessage.TypeA && answer.Header.Type != dnsmessage.TypeAAAA { continue } c.mu.Lock() queries := make([]*query, len(c.queries)) copy(queries, c.queries) c.mu.Unlock() var answered []*query for _, query := range queries { queryCopy := query if strings.EqualFold(queryCopy.nameWithSuffix, answer.Header.Name.String()) { addr, err := addrFromAnswer(answer) if err != nil { c.log.Warnf("[%s] failed to parse mDNS answer %v", c.name, err) return } resultAddr := *addr // DNS records don't contain IPv6 zones. // We're trusting that since we're on the same link, that we will only // be sent link-local addresses from that source's interface's address. // If it's not present, we're out of luck since we cannot rely on the // interface zone to be the same as the source's. resultAddr = addrWithOptionalZone(resultAddr, srcAddr.Zone) select { case queryCopy.queryResultChan <- queryResult{answer.Header, resultAddr}: answered = append(answered, queryCopy) default: } } } c.mu.Lock() for queryIdx := len(c.queries) - 1; queryIdx >= 0; queryIdx-- { for answerIdx := len(answered) - 1; answerIdx >= 0; answerIdx-- { if c.queries[queryIdx] == answered[answerIdx] { c.queries = append(c.queries[:queryIdx], c.queries[queryIdx+1:]...) answered = append(answered[:answerIdx], answered[answerIdx+1:]...) queryIdx-- break } } } c.mu.Unlock() } } }() } } func (c *Conn) start(started chan<- struct{}, inboundBufferSize int, config *Config) { defer func() { c.mu.Lock() defer c.mu.Unlock() close(c.closed) }() var numReaders int readerStarted := make(chan struct{}) readerEnded := make(chan struct{}) if c.multicastPktConnV4 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("multi4", c.multicastPktConnV4, inboundBufferSize, config) }() } if c.multicastPktConnV6 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("multi6", c.multicastPktConnV6, inboundBufferSize, config) }() } if c.unicastPktConnV4 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("uni4", c.unicastPktConnV4, inboundBufferSize, config) }() } if c.unicastPktConnV6 != nil { numReaders++ go func() { defer func() { readerEnded <- struct{}{} }() readerStarted <- struct{}{} c.readLoop("uni6", c.unicastPktConnV6, inboundBufferSize, config) }() } for i := 0; i < numReaders; i++ { <-readerStarted } close(started) for i := 0; i < numReaders; i++ { <-readerEnded } } func addrFromAnswer(answer dnsmessage.Resource) (*netip.Addr, error) { switch answer.Header.Type { case dnsmessage.TypeA: if a, ok := answer.Body.(*dnsmessage.AResource); ok { addr, ok := netip.AddrFromSlice(a.A[:]) if ok { addr = addr.Unmap() // do not want 4-in-6 return &addr, nil } } return nil, errFailedToDecodeAddrFromAResource case dnsmessage.TypeAAAA: if a, ok := answer.Body.(*dnsmessage.AAAAResource); ok { addr, ok := netip.AddrFromSlice(a.AAAA[:]) if ok { return &addr, nil } } return nil, errFailedToDecodeAddrFromAAAAResource default: return nil, errUnhandledAnswerHeaderType } } func isSupportedIPv6(addr netip.Addr, ipv6Only bool) bool { if !addr.Is6() { return false } // IPv4-mapped-IPv6 addresses cannot be connected to unless // unmapped. if !ipv6Only && addr.Is4In6() { return false } return true } func addrWithOptionalZone(addr netip.Addr, zone string) netip.Addr { if zone == "" { return addr } if addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) { return addr.WithZone(zone) } return addr } mdns-2.1.0/conn_test.go000066400000000000000000000634071510645207400150140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js package mdns import ( "context" "net" "net/netip" "runtime" "testing" "time" "github.com/pion/logging" "github.com/pion/transport/v3/test" "github.com/stretchr/testify/assert" "golang.org/x/net/dns/dnsmessage" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) const ( localAddress = "1.2.3.4" isWindows = runtime.GOOS == "windows" ) func checkIPv4(t *testing.T, addr netip.Addr) { t.Helper() assert.Truef(t, addr.Is4(), "expected IPv4 for answer but got %s", addr) } func checkIPv6(t *testing.T, addr netip.Addr) { t.Helper() assert.Truef(t, addr.Is6(), "expected IPv6 for answer but got %s", addr) } func createListener4(t *testing.T) *net.UDPConn { t.Helper() addr, err := net.ResolveUDPAddr("udp", DefaultAddressIPv4) assert.NoError(t, err) sock, err := net.ListenUDP("udp4", addr) assert.NoError(t, err) // ensure multicast loopback is enabled so tests can observe their own packets. _ = ipv4.NewPacketConn(sock).SetMulticastLoopback(true) return sock } func createListener6(t *testing.T) *net.UDPConn { t.Helper() addr, err := net.ResolveUDPAddr("udp", DefaultAddressIPv6) assert.NoError(t, err) sock, err := net.ListenUDP("udp6", addr) assert.NoError(t, err) // Ensure multicast loopback is enabled so tests can observe their own packets. _ = ipv6.NewPacketConn(sock).SetMulticastLoopback(true) return sock } // firstUsableIPv4Addr returns the first interface that is up, supports multicast, // is not loopback, and one of its IPv4 addresses. Used to provide a concrete IPv4. // this is needed for windows because cross-stack ipv4/ipv6 is unreliable. func firstUsableIPv4Addr(t *testing.T) net.IP { t.Helper() ifaces, err := net.Interfaces() assert.NoError(t, err) for _, ifc := range ifaces { if ifc.Flags&net.FlagUp == 0 { continue } if ifc.Flags&net.FlagLoopback != 0 { continue } if ifc.Flags&net.FlagMulticast == 0 { continue } addrs, err := ifc.Addrs() assert.NoError(t, err) for _, a := range addrs { var ip net.IP switch v := a.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil { continue } if v4 := ip.To4(); v4 != nil { return v4 } } } assert.Fail(t, "no usable IPv4 interface found for test") return nil } // fakePkt implements ipPacketConn for tests without real sockets. type fakePkt struct { in chan struct{} data []byte src net.Addr cm *ipControlMessage close chan struct{} } var _ ipPacketConn = (*fakePkt)(nil) func (f *fakePkt) ReadFrom(b []byte) (n int, cm *ipControlMessage, src net.Addr, err error) { select { case <-f.in: copy(b, f.data) return len(f.data), f.cm, f.src, nil case <-f.close: return 0, nil, nil, net.ErrClosed case <-time.After(3 * time.Second): return 0, nil, nil, nil } } func (f *fakePkt) WriteTo(b []byte, _ *net.Interface, _ *ipControlMessage, _ net.Addr) (int, error) { return len(b), nil } func (f *fakePkt) Close() error { close(f.close) return nil } func TestValidCommunication(t *testing.T) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) bSock := createListener4(t) aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, }) assert.NoError(t, err) bServer, err := Server(ipv4.NewPacketConn(bSock), nil, &Config{}) assert.NoError(t, err) _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) checkIPv4(t, addr) _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) checkIPv4(t, addr) // test against regression from https://github.com/pion/mdns/commit/608f20b // where by properly sending mDNS responses to all interfaces, we significantly // increased the chance that we send a loopback response to a Query that is // unwillingly to use loopback addresses (the default in pion/ice). for i := 0; i < 100; i++ { _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) assert.NotEqual(t, "127.0.0.1", addr.String(), "unexpected loopback") checkIPv4(t, addr) } assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationWithAddressConfig(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, LocalAddress: net.ParseIP(localAddress), }) assert.NoError(t, err) _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, localAddress, addr.String(), "address mismatch: expected %s, but got %v\n", localAddress, addr) assert.NoError(t, aServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") } func TestValidCommunicationWithLoopbackAddressConfig(t *testing.T) { // loopbacks cannot join multicast groups on windows. if isWindows { t.Skip("not supported on windows") } lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) loopbackIP := net.ParseIP("127.0.0.1") aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, LocalAddress: loopbackIP, IncludeLoopback: true, // the test would fail if this was false }) assert.NoError(t, err) _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, loopbackIP.String(), addr.String(), "address mismatch: expected %s, but got %v\n", loopbackIP, addr) assert.NoError(t, aServer.Close()) } func TestValidCommunicationWithLoopbackInterface(t *testing.T) { // loopbacks cannot join multicast groups on windows. if runtime.GOOS == "windows" { t.Skip("not supported on windows") } lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) ifaces, err := net.Interfaces() assert.NoError(t, err) ifacesToUse := make([]net.Interface, 0, len(ifaces)) for _, ifc := range ifaces { if ifc.Flags&net.FlagLoopback != net.FlagLoopback { continue } ifcCopy := ifc ifacesToUse = append(ifacesToUse, ifcCopy) } // the following checks are unlikely to fail since most places where this code runs // will have a loopback if len(ifacesToUse) == 0 { t.Skip("expected at least one loopback interface, but got none") } aServer, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, IncludeLoopback: true, // the test would fail if this was false Interfaces: ifacesToUse, }) assert.NoError(t, err) _, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) var found bool for _, iface := range ifacesToUse { addrs, err := iface.Addrs() assert.NoError(t, err) for _, ifaceAddr := range addrs { ipAddr, ok := ifaceAddr.(*net.IPNet) assert.Truef(t, ok, "expected *net.IPNet address for loopback but got %T", addr) if addr.String() == ipAddr.IP.String() { found = true break } } if found { break } } assert.Truef(t, found, "address mismatch: expected loopback address, but got %v\n", addr) assert.NoError(t, aServer.Close()) } func TestValidCommunicationIPv6(t *testing.T) { //nolint:cyclop if runtime.GOARCH == "386" { t.Skip("IPv6 not supported on 386 for some reason") } lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() _, err := Server(nil, nil, &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, }) assert.ErrorIs(t, err, errNoPacketConn, "expected error if no PacketConn supplied to Server") aSock := createListener6(t) bSock := createListener6(t) aServer, err := Server(nil, ipv6.NewPacketConn(aSock), &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, }) assert.NoError(t, err) bServer, err := Server(nil, ipv6.NewPacketConn(bSock), &Config{}) assert.NoError(t, err) header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) checkIPv6(t, addr) if addr.Is4In6() { // probably within docker t.Logf("address %s is an IPv4-to-IPv6 mapped address even though the stack is IPv6", addr) } else if addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() { assert.NotEqualf(t, "", addr.Zone(), "expected link-local IPv6 to have zone but got %s", addr) } header, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) checkIPv6(t, addr) if !addr.Is4In6() { assert.NotEqualf(t, "", addr.Zone(), "expected IPv6 to have zone but got %s", addr) } assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv46(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock4 := createListener4(t) bSock4 := createListener4(t) aSock6 := createListener6(t) bSock6 := createListener6(t) aServer, err := Server(ipv4.NewPacketConn(aSock4), ipv6.NewPacketConn(aSock6), &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, }) assert.NoError(t, err) bServer, err := Server(ipv4.NewPacketConn(bSock4), ipv6.NewPacketConn(bSock6), &Config{}) assert.NoError(t, err) _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) _, addr, err = bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv46Mixed(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock4 := createListener4(t) bSock6 := createListener6(t) // we can always send from a 6-only server to a 4-only server but not always // the other way around because the IPv4-only server will only listen // on multicast for IPv4 questions, so it will never see an IPv6 originated // question that contains required information to respond (the zone, if link-local). // Therefore, the IPv4 server will refuse answering AAAA responses over // unicast/multicast IPv4 if the answer is an IPv6 link-local address. This is basically // the majority of cases unless a LocalAddress is set on the Config. // aServer is IPv4-only and will perform the query aServer, err := Server(ipv4.NewPacketConn(aSock4), nil, &Config{ Name: "aServer", }) assert.NoError(t, err) bCfg := &Config{ Name: "bServer", LocalNames: []string{"pion-mdns-1.local"}, } // for windows: provide a concrete IPv4 LocalAddress to allow answering an A record . // because windows cross-stack ipv4/ipv6 is unreliable. if isWindows { v4 := firstUsableIPv4Addr(t) bCfg.LocalAddress = v4 } bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), bCfg) assert.NoError(t, err) header, addr, err := aServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeA, header.Type, "expected A but got %s", header.Type) checkIPv4(t, addr) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv46MixedLocalAddress(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock4 := createListener4(t) bSock6 := createListener6(t) aServer, err := Server(ipv4.NewPacketConn(aSock4), nil, &Config{ LocalAddress: net.IPv4(1, 2, 3, 4), LocalNames: []string{"pion-mdns-1.local"}, }) assert.NoError(t, err) bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) assert.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() // we want ipv6 but all we can offer is an ipv4 mapped address, so it should fail until we support // allowing this explicitly via configuration on the aServer side _, _, err = bServer.QueryAddr(ctx, "pion-mdns-1.local") assert.ErrorIsf(t, err, errContextElapsed, "Query expired but returned unexpected error %v", err) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv66Mixed(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock6 := createListener6(t) bSock6 := createListener6(t) aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), &Config{ LocalNames: []string{"pion-mdns-1.local"}, }) assert.NoError(t, err) bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) assert.NoError(t, err) header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) assert.Falsef(t, addr.Is4In6(), "expected address to not be ipv4-to-ipv6 mapped: %v", addr) checkIPv6(t, addr) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv66MixedLocalAddress(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock6 := createListener6(t) bSock6 := createListener6(t) aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), &Config{ LocalAddress: net.IPv4(1, 2, 3, 4), LocalNames: []string{"pion-mdns-1.local"}, }) assert.NoError(t, err) bServer, err := Server(nil, ipv6.NewPacketConn(bSock6), &Config{}) assert.NoError(t, err) header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeAAAA, header.Type, "expected AAAA but got %s", header.Type) assert.Truef(t, addr.Is4In6(), "expected address to be ipv4-to-ipv6 mapped: %v", addr) // now unmap just for this check assert.Equalf(t, localAddress, addr.Unmap().String(), "unexpected local address: %v", addr) checkIPv6(t, addr) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestValidCommunicationIPv64Mixed(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock6 := createListener6(t) bSock4 := createListener4(t) aCfg := &Config{ LocalNames: []string{"pion-mdns-1.local", "pion-mdns-2.local"}, } // for windows: provide a concrete IPv4 LocalAddress to allow answering an A record . // because windows cross-stack ipv4/ipv6 is unreliable. if isWindows { v4 := firstUsableIPv4Addr(t) aCfg.LocalAddress = v4 } aServer, err := Server(nil, ipv6.NewPacketConn(aSock6), aCfg) assert.NoError(t, err) bServer, err := Server(ipv4.NewPacketConn(bSock4), nil, &Config{}) assert.NoError(t, err) _, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-1.local") assert.NoError(t, err) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) header, addr, err := bServer.QueryAddr(context.TODO(), "pion-mdns-2.local") assert.NoError(t, err) assert.Equalf(t, dnsmessage.TypeA, header.Type, "expected A but got %s", header.Type) assert.NotEqualf(t, localAddress, addr.String(), "unexpected local address: %v", addr) assert.NoError(t, aServer.Close()) assert.NoError(t, bServer.Close()) assert.Empty(t, aServer.queries, "Queries not cleaned up after aServer close") assert.Empty(t, bServer.queries, "Queries not cleaned up after bServer close") } func TestLocalNameCaseInsensitivity(t *testing.T) { isUnicast := false lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() fp := &fakePkt{in: make(chan struct{}, 1), close: make(chan struct{})} fp.src = &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5353} conn := &Conn{ name: "test", log: logging.NewDefaultLoggerFactory().NewLogger("mdns"), queryInterval: 100 * time.Millisecond, multicastPktConnV4: fp, dstAddr4: &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}, ifaces: map[int]netInterface{ 1: {Interface: net.Interface{Index: 1, Flags: net.FlagMulticast | net.FlagUp}, supportsV4: true}, }, closed: make(chan any), localNames: []string{"pion-mdns-1.local."}, } started := make(chan struct{}) go conn.start(started, 1500, &Config{LocalAddress: net.ParseIP("127.0.0.1")}) <-started name := "pion-mdns-1.local." answerMsg, err := createAnswer(0, dnsmessage.Question{ Name: dnsmessage.MustNewName(name), Type: dnsmessage.TypeA, }, netip.MustParseAddr("127.0.0.1"), isUnicast) assert.NoError(t, err) packed, err := answerMsg.Pack() assert.NoError(t, err) tests := []string{"pion-mdns-1.local", "PION-MDNS-1.local", "pion-MDNS-1.local"} for _, q := range tests { t.Run(q, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go func() { <-time.After(50 * time.Millisecond) fp.data = packed fp.in <- struct{}{} }() _, addr, err := conn.QueryAddr(ctx, q) assert.NoError(t, err) assert.Equalf(t, "127.0.0.1", addr.String(), "address mismatch: expected %s, but got %v\n", localAddress, addr) }) } assert.NoError(t, conn.Close()) } func TestCommunicationCaseInsensitivity(t *testing.T) { isUnicast := false lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() // Use an in-memory fake instead of real sockets to avoid OS quirks. fp := &fakePkt{in: make(chan struct{}, 1), close: make(chan struct{})} fp.src = &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5353} conn := &Conn{ name: "test", log: logging.NewDefaultLoggerFactory().NewLogger("mdns"), queryInterval: 100 * time.Millisecond, multicastPktConnV4: fp, dstAddr4: &net.UDPAddr{IP: net.IPv4(224, 0, 0, 251), Port: 5353}, ifaces: map[int]netInterface{ 1: { Interface: net.Interface{Index: 1, Flags: net.FlagMulticast | net.FlagUp}, supportsV4: true, }, }, closed: make(chan any), } started := make(chan struct{}) go conn.start(started, 1500, &Config{}) <-started name := "pion-MDNS-1.local." answerMsg, err := createAnswer(0, dnsmessage.Question{ Name: dnsmessage.MustNewName(name), Type: dnsmessage.TypeA, }, netip.MustParseAddr(localAddress), isUnicast) assert.NoError(t, err) packed, err := answerMsg.Pack() assert.NoError(t, err) tests := []string{"pion-mdns-1.local", "pion-MDNS-1.local"} for _, testName := range tests { t.Run(testName, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() ready := make(chan struct{}) go func() { <-time.After(50 * time.Millisecond) fp.data = packed fp.in <- struct{}{} close(ready) }() _, addr, err := conn.QueryAddr(ctx, testName) assert.NoError(t, err) assert.Equalf(t, localAddress, addr.String(), "unexpected local address: %v", addr) <-ready }) } assert.NoError(t, conn.Close()) } func TestMultipleClose(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) assert.NoError(t, err) assert.NoError(t, server.Close()) assert.NoError(t, server.Close()) assert.Empty(t, server.queries, "Queries not cleaned up after server close") } func TestQueryRespectTimeout(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) assert.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() _, _, err = server.QueryAddr(ctx, "invalid-host") assert.ErrorIsf(t, err, errContextElapsed, "Query expired but returned unexpected error %v", err) assert.NoError(t, server.Close()) assert.Empty(t, server.queries, "Queries not cleaned up after server close") } func TestQueryRespectClose(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() report := test.CheckRoutines(t) defer report() aSock := createListener4(t) server, err := Server(ipv4.NewPacketConn(aSock), nil, &Config{}) assert.NoError(t, err) go func() { time.Sleep(3 * time.Second) assert.NoError(t, server.Close()) }() _, _, err = server.QueryAddr(context.TODO(), "invalid-host") assert.ErrorIsf(t, err, errConnectionClosed, "Query on closed server but returned unexpected error %v", err) _, _, err = server.QueryAddr(context.TODO(), "invalid-host") assert.ErrorIsf(t, err, errConnectionClosed, "Query on closed server but returned unexpected error %v", err) assert.Empty(t, server.queries, "Queries not cleaned up after server close") } func testResourceParsing(t *testing.T, echoQuery bool) { //nolint:thelper lookForIP := func(t *testing.T, msg dnsmessage.Message, expectedIP []byte) { t.Helper() actualAddr, err := addrFromAnswer(msg.Answers[0]) if err != nil { assert.NoError(t, err) } if echoQuery { if len(msg.Questions) == 0 { assert.Fail(t, "Echoed query not included in answer") } } else { if len(msg.Questions) > 0 { assert.Fail(t, "Echoed query erroneously included in answer") } } assert.Equalf( t, expectedIP, actualAddr.AsSlice(), "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr.AsSlice(), ) } name := "test-server." t.Run("A Record", func(t *testing.T) { answer, err := createAnswer(1, dnsmessage.Question{ Name: dnsmessage.MustNewName(name), Type: dnsmessage.TypeA, }, mustAddr(t, net.IP{127, 0, 0, 1}), echoQuery) if err != nil { assert.NoError(t, err) } lookForIP(t, answer, []byte{127, 0, 0, 1}) }) t.Run("AAAA Record", func(t *testing.T) { answer, err := createAnswer(1, dnsmessage.Question{ Name: dnsmessage.MustNewName(name), Type: dnsmessage.TypeAAAA, }, netip.MustParseAddr("::1"), echoQuery) if err != nil { assert.NoError(t, err) } lookForIP(t, answer, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) }) } func TestResourceParsingWithEchoedQuery(t *testing.T) { testResourceParsing(t, true) } func TestResourceParsingWithoutEchoedQuery(t *testing.T) { testResourceParsing(t, false) } func mustAddr(t *testing.T, ip net.IP) netip.Addr { t.Helper() addr, ok := netip.AddrFromSlice(ip) assert.True(t, ok) return addr } func TestIPToBytes(t *testing.T) { //nolint:cyclop expectedIP := []byte{127, 0, 0, 1} actualAddr4, err := ipv4ToBytes(netip.MustParseAddr("127.0.0.1")) assert.NoError(t, err) assert.Equalf(t, expectedIP, actualAddr4[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr4) expectedIP = []byte{0, 0, 0, 1} actualAddr4, err = ipv4ToBytes(netip.MustParseAddr("0.0.0.1")) assert.NoError(t, err) assert.Equalf(t, expectedIP, actualAddr4[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr4) expectedIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} actualAddr6, err := ipv6ToBytes(netip.MustParseAddr("::1")) assert.NoError(t, err) assert.Equalf(t, expectedIP, actualAddr6[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr6) _, err = ipv4ToBytes(netip.MustParseAddr("::1")) assert.Error(t, err, "::1 should not be output to IPv4 bytes") expectedIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 127, 0, 0, 1} addr, ok := netip.AddrFromSlice(net.ParseIP("127.0.0.1")) assert.True(t, ok, "expected to be able to convert IP to netip.Addr") actualAddr6, err = ipv6ToBytes(addr) assert.NoError(t, err) assert.Equalf(t, expectedIP, actualAddr6[:], "Expected(%v) and Actual(%v) IP don't match", expectedIP, actualAddr6) } mdns-2.1.0/errors.go000066400000000000000000000007771510645207400143350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package mdns import "errors" var ( errJoiningMulticastGroup = errors.New("mDNS: failed to join multicast group") errConnectionClosed = errors.New("mDNS: connection is closed") errContextElapsed = errors.New("mDNS: context has elapsed") errNilConfig = errors.New("mDNS: config must not be nil") errFailedCast = errors.New("mDNS: failed to cast listener to UDPAddr") ) mdns-2.1.0/examples/000077500000000000000000000000001510645207400142755ustar00rootroot00000000000000mdns-2.1.0/examples/README.md000066400000000000000000000022631510645207400155570ustar00rootroot00000000000000## Example There are many ways to use the example, as described in the [README](../README.md) at the root of this repo. Note: the mDNS server responds to queries for `pion-test.local`. ### 1. Start a generic mDNS server Run the following from the root: ```sh go run examples/server/main.go ``` This spins up the mDNS server. ### 2. Query from the client Run the following from the root: #### Linux ```sh go run examples/query/main.go ``` #### macOS ``` dns-sd -q pion-test.local ``` #### Or using avahi ``` avahi-resolve -a pion-test.local ``` Once you've queried from the client, you should receive a basic response from the mDNS server, which is printed out by the client, comprised of three parts: 1. the answer to the query 2. the source of the mDNS server (aka the server's IP) 3. any related errors ## Example finished! An alternative to step 1 is to instead run: ```sh go run examples/server/publish_ip/main.go -ip=[IP] ``` where `[IP]` is a valid ip address (defaults to `1.2.3.4`). This determines what ip address the mDNS server is hosted on. At this point you can now see how easy it is to spin up an mDNS server, query it from a client. Now go do something with the response! :) mdns-2.1.0/examples/query/000077500000000000000000000000001510645207400154425ustar00rootroot00000000000000mdns-2.1.0/examples/query/main.go000066400000000000000000000026621510645207400167230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // This example program showcases the use of the mDNS client by querying a previously published address package main import ( "context" "fmt" "net" "os" "github.com/pion/mdns/v2" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) func main() { //nolint:cyclop var useV4, useV6 bool if len(os.Args) > 1 { switch os.Args[1] { case "-v4only": useV4 = true useV6 = false case "-v6only": useV4 = false useV6 = true default: useV4 = true useV6 = true } } else { useV4 = true useV6 = true } var packetConnV4 *ipv4.PacketConn if useV4 { addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { panic(err) } l4, err := net.ListenUDP("udp4", addr4) if err != nil { panic(err) } packetConnV4 = ipv4.NewPacketConn(l4) } var packetConnV6 *ipv6.PacketConn if useV6 { addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) if err != nil { panic(err) } l6, err := net.ListenUDP("udp6", addr6) if err != nil { panic(err) } packetConnV6 = ipv6.NewPacketConn(l6) } server, err := mdns.Server(packetConnV4, packetConnV6, &mdns.Config{}) if err != nil { panic(err) } // perform query answer, src, err := server.QueryAddr(context.TODO(), "pion-test.local") // print response fmt.Println(answer) fmt.Println(src) fmt.Println(err) } mdns-2.1.0/examples/server/000077500000000000000000000000001510645207400156035ustar00rootroot00000000000000mdns-2.1.0/examples/server/main.go000066400000000000000000000015211510645207400170550ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // This example program showcases the use of the mDNS server by publishing "pion-test.local" package main import ( "net" "github.com/pion/mdns/v2" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) func main() { addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { panic(err) } addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) if err != nil { panic(err) } l4, err := net.ListenUDP("udp4", addr4) if err != nil { panic(err) } l6, err := net.ListenUDP("udp6", addr6) if err != nil { panic(err) } _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ LocalNames: []string{"pion-test.local"}, }) if err != nil { panic(err) } select {} } mdns-2.1.0/examples/server/publish_ip/000077500000000000000000000000001510645207400177415ustar00rootroot00000000000000mdns-2.1.0/examples/server/publish_ip/main.go000066400000000000000000000020771510645207400212220ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // This example program allows to set an IP that deviates from the automatically determined interface address. // Use the "-ip" parameter to set an IP. If not set, the example server defaults to "1.2.3.4". package main import ( "flag" "net" "github.com/pion/mdns/v2" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) func main() { ip := flag.String("ip", "1.2.3.4", "IP address to be published") flag.Parse() addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { panic(err) } addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) if err != nil { panic(err) } l4, err := net.ListenUDP("udp4", addr4) if err != nil { panic(err) } l6, err := net.ListenUDP("udp6", addr6) if err != nil { panic(err) } _, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ LocalNames: []string{"pion-test.local"}, LocalAddress: net.ParseIP(*ip), }) if err != nil { panic(err) } select {} } mdns-2.1.0/go.mod000066400000000000000000000005621510645207400135700ustar00rootroot00000000000000module github.com/pion/mdns/v2 go 1.21 require ( github.com/pion/logging v0.2.4 github.com/pion/transport/v3 v3.1.1 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.35.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) mdns-2.1.0/go.sum000066400000000000000000000027711510645207400136210ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= mdns-2.1.0/mdns.go000066400000000000000000000002431510645207400137460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package mdns implements mDNS (multicast DNS) package mdns mdns-2.1.0/renovate.json000066400000000000000000000001731510645207400151760ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] }