Help us shape the future of Go! Take the 2026 Go Developer Survey

mdns

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

README

mdns

A multicast DNS (RFC 6762) and DNS-SD (RFC 6763) responder and browser in pure Go.

Go Reference test Go

It announces services with subtypes, probes, renames on conflicts found while probing or later, and browses so that it finds responders that answer only by unicast as well as those that answer only by multicast, once or continuously, and reflects mDNS between network segments. It is built on golang.org/x/net and the standard library, without cgo. It was written for SigmaM2M, a oneM2M platform, where it serves OpenADR 3, Matter and several device integrations.

Features

Clauses are RFC 6762 unless marked RFC 6763.

Feature Status
Responder: PTR, SRV, TXT, A, AAAA, ANY; subtypes; _services._dns-sd._udp (RFC 6763 9) yes
Probing, conflict detection while probing, renaming (Options.Rename, at most 15) yes
Simultaneous-probe tie-breaking (8.2, 8.2.1) yes
Conflicts after probing (9): re-probe, rename or withdraw, Options.OnConflict events, Announcer.Service, Announcer.Err; 15 conflicts in 10 s pause probing 5 s (8.1) yes (since v0.2.0)
NSEC negative answers for our names (6.1), and NSEC or the other family's addresses as additional records (6.2) yes (since v0.2.0)
Legacy unicast replies (6.7), known-answer suppression (7.1) yes
QU questions answered by unicast unless the record was not multicast within a quarter of its TTL (5.4) yes (the 5.4 rule since v0.2.0)
Known answers over several packets with the TC bit: the responder waits 400-500 ms for them (7.2) yes (since v0.2.0)
Unique answers at once, shared answers after 20-120 ms and aggregated per interface (6) yes (since v0.2.0)
A record multicast at most once a second per interface, 250 ms in probe defence (6); probes answered by multicast yes (since v0.2.0)
Duplicate answer suppression (7.4) yes (since v0.2.0)
Responses over the MTU split into several packets (17) yes (since v0.2.0)
Packets sent to our unicast address only from the local link (11); responses only from port 5353 (6); messages with an error code ignored (18.11) yes (since v0.2.0)
TXT update without probing (UpdateTXT) yes
Re-join and re-announce on interface changes (checked every 10 s) yes; a new interface is announced on without a new probe
Browser: one-shot and multicast queries at once, several types in one query train yes
Known answers over several packets with the TC bit when they do not fit in one (7.2) yes (since v0.2.0)
Known answers only with at least half their TTL left (7.1) yes (since v0.2.0)
NSEC read: no more questions for types a responder says it lacks; cached records of those types dropped (6.1) yes (since v0.2.0)
Cache flush: per interface, merged multihomed answers; records from the last second kept (10.2); goodbyes kept one second (10.1) yes (the one-second rules since v0.2.0)
Continuous browsing (Watch): passive cache, refresh at 80 to 95 % of TTL (5.2), events yes
Passive observation of failures: records others ask for twice without an answer are dropped after 10 s (10.5) yes, in Watch (since v0.2.0)
Reconfirmation on request: Watcher.Reconfirm (10.4) yes (since v0.2.0)
Duplicate question suppression for refresh queries (7.3) yes, in Watch (since v0.2.0)
Service type enumeration as a browser: BrowseServiceTypes (RFC 6763 9) yes (since v0.2.0)
IPv4 and IPv6 yes
Off-link answers dropped by the browser on request (11): Options.StrictSource yes (since v0.3.0)
QU on the first query of a browse (5.4): Options.QU yes (since v0.3.0)
Browsing domain enumeration as a browser (RFC 6763 11): BrowseDomains yes (since v0.3.0)
Reflector between interfaces or VLANs: Reflect (not in RFC 6762; Avahi's practice) yes (since v0.3.0)

Off by default, on purpose:

  • QU questions from the browser (5.4, Options.QU). A unicast reply to port 5353 reaches only one of the sockets that share it, often the operating system's responder, so by default the browser asks without QU and gets unicast answers through its one-shot socket instead.
  • Off-link source checks in the browser (11, Options.StrictSource). A device with a wrong static address on the same segment should still be found, so by default the browser takes answers from any source; a host off the link that can reach the browser's one-shot port could then add records to its results. The responder always checks.
Why both ways at once

Responders on real networks answer a query in one of two ways, and a browser that queries only one way misses the others:

Responder Answers One-shot only (ephemeral port) From 5353 only Both (default)
hashicorp/mdns, a cast receiver unicast to the query's source found missed where the OS responder holds 5353 found
Go zeroconf forks, betamos, matter.js, a Spotify desktop responder multicast only missed found found
this module, mDNSResponder, Avahi both found found found

The 5353 sockets set SO_REUSEADDR and SO_REUSEPORT so they share the port with mDNSResponder, Avahi or systemd-resolved. When the port cannot be bound (another responder holds it without SO_REUSEPORT, or Windows), the browser carries on with one-shot queries. In a container, multicast needs the host network.

Choosing a Go mDNS library

Compared from each library's source at the versions named, in October 2026 (the rows on conflicts after probing, NSEC, TC, rate limits and failure observation by searching each source for them on 2026-10-11); "tested" rows were run on one macOS LAN. Check the projects themselves before relying on a row.

this module hashicorp/mdns v1.0.7 grandcat/zeroconf v1.0.0 libp2p/zeroconf v2.2.0 brutella/dnssd v1.2.14 pion/mdns v2.2.2
Announces unsolicited (not only answers queries) yes no yes yes yes yes
Probing, conflict renaming yes, with tie-breaking no no no yes, with tie-breaking no
Conflicts after probing (RFC 6762 9) yes, with events no no no yes no
NSEC negative answers sends and reads no no no sends no
Known answers over truncated (TC) packets as querier and responder no (TODO in source) no no as responder no
Shared-answer delay, aggregation, per-record rate limit yes no no no delay only no
Passive observation of failures, reconfirm yes (Watch) no no no no no
Subtypes, announce and browse (tested) yes no no yes no no
Answers one-shot (legacy unicast) queries (tested) yes yes no no yes yes
Finds unicast-only and multicast-only responders in one browse (tested) yes no no no no no
Browse returns only the asked type yes no, other types too yes yes yes no, caller filters
Continuous browse with change events yes (Watch) no partial yes yes yes
Goodbye on close yes no yes yes yes yes
Port other than 5353, for tests yes no no no no yes
context.Context yes timeouts only yes yes yes yes
Dependencies golang.org/x/net miekg/dns miekg/dns and two more miekg/dns miekg/dns, netlink pion/logging, pion/transport

Pick this module when you need to be found reliably and to find every kind of device on a real LAN: Matter, HomeKit-style or OpenADR announcements with subtypes and probing, or discovery of consumer devices whose responders answer only one way. Pick hashicorp/mdns if you already use it, only need simple lookups in a controlled network, and want the longest track record. Pick pion/mdns inside a pion WebRTC stack, and brutella/dnssd for a HomeKit accessory built on brutella/hap. brutella/dnssd also keeps results of Apple's Bonjour Conformance Test in its repository; this module has not been run against that test (Apple serves it only after an Apple ID sign-in on developer.apple.com, checked 2026-10-12), so where a passed conformance run is a requirement, brutella/dnssd is the one with evidence. This module's evidence instead is interop with mDNSResponder itself, python-zeroconf and Avahi (below).

Install

go get github.com/cB-Sigma/mdns@latest

Requires Go 1.27.1 or later.

Quick start

import "github.com/cB-Sigma/mdns"

// Once.
found, err := mdns.Browse(ctx, "_hue._tcp", mdns.Options{Wait: 3 * time.Second})

// Continuously.
w, err := mdns.Watch(ctx, []string{"_ewelink._tcp", "_shelly._tcp"}, mdns.Options{}, func(e mdns.Event) {
	fmt.Println(e.Kind, e.Type, e.Service.Instance, e.Service.TXT["seq"])
})
defer w.Close()

// Announce, with a subtype, renaming on a conflict, also one found later.
a, err := mdns.Announce(ctx, mdns.Service{Instance: "Site gateway", Host: "gateway", Port: 8443,
	TXT: map[string]string{"version": "1"}, Subtypes: []string{"_vtn"}}, "_openadr3._tcp",
	mdns.Options{Rename: mdns.RenameInstance, OnConflict: func(e mdns.ConflictEvent) {
		fmt.Println(e.Kind, e.Service.Instance)
	}})
defer a.Close()
_ = a.UpdateTXT(map[string]string{"version": "2"})

// The service types on the link.
types, err := mdns.BrowseServiceTypes(ctx, mdns.Options{Wait: 3 * time.Second})

// An instance a connection failed to: ask whether it is still there.
w.Reconfirm("_shelly._tcp", "shellyplus1-a8032ab1")

These mirror ExampleBrowse, ExampleWatch, ExampleAnnounce, ExampleConflictEvent, ExampleBrowseServiceTypes and ExampleWatcher_Reconfirm in example_test.go, which compile with the tests but do not run (they need a network). ExampleRenameInstance runs and checks its output.

Documentation

API reference: https://pkg.go.dev/github.com/cB-Sigma/mdns

Package What it does
mdns (module root) Announce (probe with tie-breaking, Options.Rename with RenameInstance, announce, answer with NSEC, delays, aggregation and rate limits, UpdateTXT, re-join on interface changes, goodbye on Close; conflicts after probing through Options.OnConflict, Announcer.Service, Announcer.Err), Browse and BrowseTypes (one-shot and multicast queries at once by default; Options.Mode for either alone), BrowseServiceTypes, BrowseDomains, Reflect (a reflector between interfaces), Options.StrictSource and Options.QU, Watch (continuous: cached announcements, refresh before expiry, passive observation of failures, Watcher.Reconfirm, Added/Updated/Removed events), TestInterface
internal/reuse SO_REUSEADDR and SO_REUSEPORT on the mDNS sockets
interop (nested module, tests only) this module against hashicorp/mdns and grandcat/zeroconf on the real port 5353, the system's mDNSResponder through dns-sd (macOS), and against Avahi and python-zeroconf in Linux containers, on several networks, an IPv6-only network and through the reflector (cmd/avahipeer, testdata/avahi, testdata/zeroconf)

Testing

go test -race ./...

Tests that need multicast pick a random port and an interface on which multicast loops back to the host; they skip when there is none, and with -short. Tests carry // Covers: SIG-... and // Covers: SIG-MTR-... comments: these are requirement IDs of the SigmaM2M project's compliance report and have no meaning outside it.

The browse and watch tests against the simulated devices are in mdns-sim, so this module does not depend on its simulator.

interop/ is a nested module with its own go.mod, so its dependencies stay out of this one. Its tests against hashicorp/mdns and grandcat/zeroconf use the real port 5353 and are skipped unless SIGMA_MDNS_INTEROP=1 is set. TestAvahi builds cmd/avahipeer (this module) for Linux and runs it in an Alpine container next to avahi-daemon, with a second Avahi container on the same user-defined Docker network; it needs Docker and network access for the image, and is skipped without Docker and with -short. Docker Desktop on macOS and Windows runs containers in a virtual machine, where multicast does not reach the host, so both peers run in containers.

cd interop && SIGMA_MDNS_INTEROP=1 go test -v ./...   # LAN peers, mDNSResponder (macOS), and the Docker tests
cd interop && go test -v -run TestAvahi ./...         # Avahi only
cd interop && go test -v -run 'TestZeroconf|TestMultiInterface|TestIPv6Only|TestReflector' ./...

The python-zeroconf tests (TestZeroconf, TestMultiInterface, TestIPv6Only, TestReflector) build an image of python-zeroconf 0.151.5 with avahipeer (testdata/zeroconf) and run containers on Docker networks: one, two (a container on both), an IPv6-only one, and two joined by our reflector. TestMDNSResponder drives the system's mDNSResponder through dns-sd on the real port 5353 and needs SIGMA_MDNS_INTEROP=1 and macOS.

Interop tested against
  • In process, on a test port: the simulated devices of each answer style in mdns-sim (written with dnsmessage rather than this module's encoder, but by the same author, so not independent).
  • On the real port 5353 on one macOS LAN (interop, 2026-10-06): hashicorp/mdns v1.0.7 and grandcat/zeroconf v1.0.0 as responders (found by the default browse; hashicorp missed from 5353 alone, zeroconf missed one-shot alone) and as browsers (both found our announcer).
  • Real devices on that LAN: a Sony BRAVIA cast receiver (_googlecast._tcp), a Sony audio device and a Spotify desktop responder (_spotify-connect._tcp) were all found by the default browse, the same instances dns-sd -B listed; one-shot alone missed the Spotify desktop responder.
  • matter.js 0.17.9, through the interop tests of SigmaM2M's Matter stack (runs made before this module was split out).
  • Avahi 0.8 on Linux (TestAvahi, Alpine 3.20.3 containers on a user-defined Docker bridge network, Docker Desktop 29.4.3 on arm64, 2026-10-11; v0.2.0): our browser found services from avahi-publish by default, one-shot only and multicast only, with TXT, host and address, and a subtype alone; BrowseServiceTypes listed Avahi's type; avahi-browse -r found and resolved our announcer, its subtype and its type in avahi-browse -a; avahi-resolve -4 resolved our host name; when avahi-publish claimed our instance name, Avahi renamed its service to "Sigma Box #2" and our announcer kept its name; when we claimed Avahi's name, we renamed to "Avahi Box (2)"; Avahi dropped our instance after our goodbye. Our side shared port 5353 with an avahi-daemon in its own container throughout. avahi-resolve -6 for our IPv4-only host, which we answer with NSEC, still waited for its 5 s timeout: Avahi 0.8 did not act on the NSEC.
  • v0.2.0 on the macOS LAN (2026-10-11): the hashicorp/mdns and grandcat/zeroconf runs above repeated with the same results, and BrowseServiceTypes listed six types (the Mac's own mDNSResponder types and a Sony BRAVIA's _googlecast._tcp and _androidtvremote2._tcp).
  • mDNSResponder (macOS 26.3.1, the system's, through dns-sd, LAN, 2026-10-12; v0.3.0): dns-sd -B found our announcer and, with _sigmadnssd._tcp,_vtn, its subtype; dns-sd -L resolved host, port and TXT; dns-sd -G v4 our address; dns-sd -G v6 for our IPv4-only host reported "No Such Record" (it took our NSEC, which Avahi 0.8 did not); our browsers in every mode and BrowseServiceTypes found a dns-sd -R registration; we renamed to "… (2)" when mDNSResponder held the name, and mDNSResponder renamed its registration when we held it; dns-sd -B showed our goodbye as Rmv.
  • python-zeroconf 0.151.5 on Linux (Docker, 2026-10-12; v0.3.0): it found and resolved our announcer and its subtype; our browsers found its service in every mode; it renamed to "Held Name-2" when we held the name, and we renamed to "ZC Held (2)" when it did; it saw our goodbye; 30 instances each way, which take responses over several packets, were all found both ways.
  • Several interfaces (Docker, v0.3.0): our announcer in a container on two networks was found by python-zeroconf on each network with that network's address only, and our browser found services on both.
  • An IPv6-only network (Docker, v0.3.0): our announcer was found by python-zeroconf over IPv6 with its IPv6 address, and our browser found python-zeroconf's service with its IPv6 address.
  • Our reflector (Docker, v0.3.0): with it, python-zeroconf and our browser on one network found a service registered on the other; without it they did not.
  • Not yet: systemd-resolved (it needs systemd in a container), Windows (no Windows host here), Apple's Bonjour Conformance Test (Apple ID sign-in needed). Truncated known-answer trains, rate limits and passive observation are exercised by the multi-packet cases above only indirectly; their exact behaviour is tested against the simulator and hand-built packets (same author).
  • mdns-sim: simulated mDNS/DNS-SD devices in each answer style, as a Go package and a command.

Status

v0.x: the API may change before v1.

Contributing

See CONTRIBUTING.md.

Security

See SECURITY.md to report a vulnerability privately.

Licence

Apache-2.0 (see LICENSE and NOTICE). Copyright 2026 Abhinav Gautam.

Documentation

Overview

Package mdns is a multicast DNS (RFC 6762) and DNS-SD (RFC 6763) responder and browser with subtype support, written on golang.org/x/net (dnsmessage, ipv4, ipv6) and the standard library. It began as SigmaM2M's OpenADR 3 local-VTN discovery and was extended for Matter; this module is the one copy both use (see PROVENANCE.md).

The responder (Announce) answers PTR, SRV, TXT, A, AAAA and ANY questions for the names it owns and NSEC negative answers for the types it does not have, probes before announcing with simultaneous-probe tie-breaking, renames after a conflict through Options.Rename, keeps watching for conflicts after probing, announces unsolicited, answers legacy unicast (one-shot) queries, applies known-answer suppression (also over known answers sent in several truncated packets), delays and aggregates shared answers, suppresses duplicate answers, multicasts each record at most once a second, re-announces a changed TXT record without probing again, re-joins the groups when interfaces change and sends goodbyes on Close.

The browser (Browse, BrowseTypes) queries both ways at once by default: one-shot queries from an ephemeral port, which responders that only answer by unicast reply to, and multicast queries from the mDNS port with the groups joined, shared with the operating system's responder, which catches responders that only answer by multicast. Long known-answer lists go out over several packets with the TC bit, and NSEC answers stop questions for records a responder says it does not have. Watch keeps browsing: it caches unsolicited announcements, re-queries before records expire, drops records whose owner stays silent when others ask for them (passive observation of failures), reconfirms records on request, suppresses its questions when another host has just asked them, and reports instances that appear, change and leave. BrowseServiceTypes lists the service types on the link.

Index

Examples

Constants

View Source
const DefaultPort = 5353

DefaultPort is the multicast DNS port (RFC 6762 clause 3).

View Source
const DefaultWait = 3 * time.Second

DefaultWait is how long Browse collects answers when neither opts.Wait nor a context deadline bounds it.

View Source
const MaxRenames = 15

MaxRenames bounds automatic renaming after conflicts (RFC 6762 clause 9 limits a responder to 15 conflicts in 10 seconds).

Variables

View Source
var ErrConflict = errors.New("mdns: name already in use on the link")

ErrConflict is returned by Announce when another responder on the link already answers for the service instance or host name with other data (RFC 6762 clause 9).

View Source
var MaxQueryInterval = time.Hour

MaxQueryInterval caps the gap between a Watcher's periodic queries (RFC 6762 clause 5.2 says at most one hour).

Functions

func BrowseServiceTypes added in v0.2.0

func BrowseServiceTypes(ctx context.Context, opts Options) ([]string, error)

BrowseServiceTypes lists the service types announced on the link, such as "_http._tcp", sorted, by asking for the DNS-SD service type enumeration name _services._dns-sd._udp.local. (RFC 6763 clause 9). It queries the way Browse does, with the same Options, and returns what answered before ctx ended or opts.Wait passed.

Example

BrowseServiceTypes lists the service types announced on the link.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/cB-Sigma/mdns"
)

func main() {
	types, err := mdns.BrowseServiceTypes(context.Background(), mdns.Options{Wait: 3 * time.Second})
	if err != nil {
		return
	}
	fmt.Println(types)
}

func BrowseTypes

func BrowseTypes(ctx context.Context, services []string, opts Options) (map[string][]Service, error)

BrowseTypes is Browse for several service types over one set of sockets and one query train; the result is keyed by the types as given.

func TestInterface

func TestInterface(port int) (*net.Interface, error)

TestInterface picks an interface on which multicast sent from this host on port comes back to this host, for tests that announce and browse in one process. It returns an error (tests skip) when none works.

Types

type Announcer

type Announcer struct {
	// contains filtered or unexported fields
}

Announcer advertises one service until Close or until its context ends.

After probing it keeps listening for conflicts (RFC 6762 clause 9): when another responder answers with other data for the instance or host name, it probes again and, if the name is taken, renames through Options.Rename or stops answering; Options.OnConflict hears about each step.

func Announce

func Announce(ctx context.Context, s Service, service string, opts Options) (*Announcer, error)

Announce advertises s as an instance of service (for example "_matter._tcp"). It joins 224.0.0.251 and ff02::fb on opts.Port (5353 by default) on the given interfaces (all multicast-capable ones when none), probes that the names are free, sends unsolicited announcements and answers queries. Close, or the end of ctx, sends goodbyes (TTL 0) and leaves.

The sockets set SO_REUSEADDR and SO_REUSEPORT where the platform has them, so the announcer can share 5353 with the operating system's own responder (mDNSResponder on macOS, Avahi or systemd-resolved on Linux).

When probing finds a conflict and opts.Rename is set, Announce asks it for a new service (new instance and/or host name) and probes again, up to MaxRenames times (RFC 6762 clause 9); otherwise it fails with ErrConflict. When it succeeds under another name than s, it calls opts.OnConflict with a ConflictRenamed event before returning.

Example

Announce advertises a service with a subtype and renames it if the name is taken; UpdateTXT changes its metadata without probing again.

package main

import (
	"context"

	"github.com/cB-Sigma/mdns"
)

func main() {
	ctx := context.Background()
	a, err := mdns.Announce(ctx, mdns.Service{Instance: "Site gateway", Host: "gateway", Port: 8443,
		TXT: map[string]string{"version": "1"}, Subtypes: []string{"_vtn"}}, "_openadr3._tcp",
		mdns.Options{Rename: mdns.RenameInstance})
	if err != nil {
		return
	}
	defer a.Close()
	_ = a.UpdateTXT(map[string]string{"version": "2"})
}

func (*Announcer) Close

func (a *Announcer) Close() error

Close sends goodbye records (TTL 0), leaves the groups and closes the sockets. It is safe to call more than once. An announcer that gave its names up after a conflict sends no goodbye, since the names now belong to another host.

func (*Announcer) Err added in v0.2.0

func (a *Announcer) Err() error

Err returns nil while the announcer holds its names, and an error wrapping ErrConflict once it has given them up after a conflict it could not rename its way out of (no Options.Rename, or MaxRenames reached).

func (*Announcer) Service added in v0.2.0

func (a *Announcer) Service() Service

Service returns the service as announced now: after a rename it carries the new instance or host name, after UpdateTXT the new metadata.

func (*Announcer) UpdateTXT

func (a *Announcer) UpdateTXT(txt map[string]string) error

UpdateTXT replaces the TXT metadata and announces the new record twice, one second apart, without probing again: the instance name stays the same, and the record's cache-flush bit makes caches replace the old one (RFC 6762 clauses 8.4 and 10.2).

type BrowseMode

type BrowseMode int

BrowseMode is how a browser sends its queries and hears the answers.

const (
	// BrowseBoth sends one-shot queries from an ephemeral port and
	// multicast queries from the mDNS port with the groups joined, and
	// listens on both. Responders that answer only by unicast to the
	// query's source (RFC 6762 clause 6.7) answer the first; responders
	// that answer only by multicast (clause 6) are heard on the second,
	// which also receives announcements. When the mDNS port cannot be
	// bound, the browser carries on with one-shot queries alone.
	BrowseBoth BrowseMode = iota
	// BrowseOneShot only sends one-shot queries from an ephemeral port
	// (RFC 6762 clause 5.1) and never binds the mDNS port.
	BrowseOneShot
	// BrowseMulticast only queries from the mDNS port with the groups
	// joined. A unicast reply to the mDNS port reaches only one of the
	// sockets bound there, so responders that answer by unicast may be
	// missed while the operating system's responder holds the port.
	BrowseMulticast
)

type ConflictEvent added in v0.2.0

type ConflictEvent struct {
	Kind ConflictKind
	// Service is the service as announced after this step.
	Service Service
	// Previous is the service given up, for ConflictRenamed.
	Previous Service
	// Err wraps ErrConflict, for ConflictWithdrawn.
	Err error
}

ConflictEvent is one step of an announcer's conflict handling, given to Options.OnConflict.

Example

OnConflict hears about a name conflict that appears after probing: the announcer probes again and renames, or gives the name up.

package main

import (
	"context"
	"fmt"

	"github.com/cB-Sigma/mdns"
)

func main() {
	ctx := context.Background()
	a, err := mdns.Announce(ctx, mdns.Service{Instance: "Site gateway", Host: "gateway", Port: 8443}, "_openadr3._tcp",
		mdns.Options{Rename: mdns.RenameInstance, OnConflict: func(e mdns.ConflictEvent) {
			switch e.Kind {
			case mdns.ConflictRenamed:
				fmt.Printf("now %q (was %q)\n", e.Service.Instance, e.Previous.Instance)
			case mdns.ConflictWithdrawn:
				fmt.Println("gave the name up:", e.Err)
			}
		}})
	if err != nil {
		return
	}
	defer a.Close()
}

type ConflictKind added in v0.2.0

type ConflictKind int

ConflictKind says what an announcer did about a name conflict.

const (
	// ConflictSeen: after probing, another responder answered with other
	// data for our instance or host name; the announcer stops answering
	// for the names and probes them again (RFC 6762 clause 9).
	ConflictSeen ConflictKind = iota + 1
	// ConflictKept: probing again found no other owner (the conflicting
	// record was stale); the names are announced again.
	ConflictKept
	// ConflictRenamed: the name is taken; the announcer now announces
	// Service, renamed through Options.Rename, instead of Previous.
	ConflictRenamed
	// ConflictWithdrawn: the name is taken and no rename was possible (no
	// Options.Rename, or MaxRenames reached); the announcer has stopped
	// answering and Err returns Err.
	ConflictWithdrawn
)

The conflict kinds, in the order they can happen.

func (ConflictKind) String added in v0.2.0

func (k ConflictKind) String() string

type Domains added in v0.3.0

type Domains struct {
	Browse, DefaultBrowse     []string // b._dns-sd._udp, db._dns-sd._udp
	Register, DefaultRegister []string // r._dns-sd._udp, dr._dns-sd._udp
	Legacy                    []string // lb._dns-sd._udp
}

Domains are the DNS-SD domains a link recommends (RFC 6763 clause 11): where to browse, where to register, and the legacy browsing domains, each as fully qualified domain names such as "example.com.". They point at unicast DNS-SD; on the link itself the domain is always "local.".

func BrowseDomains added in v0.3.0

func BrowseDomains(ctx context.Context, opts Options) (Domains, error)

BrowseDomains asks the link for its recommended DNS-SD domains, the way Browse asks for services and with the same Options.

type Event

type Event struct {
	Kind    EventKind
	Type    string
	Service Service
}

Event is one change seen by a Watcher. Type is the service type as given to Watch; for Removed, Service is the instance as last seen.

type EventKind

type EventKind int

EventKind says what happened to a watched instance.

const (
	// Added: an instance was found (its SRV record arrived).
	Added EventKind = iota + 1
	// Updated: its host, port, addresses or TXT record changed.
	Updated
	// Removed: it said goodbye (TTL 0) or its records expired without
	// being refreshed.
	Removed
)

The event kinds.

func (EventKind) String

func (k EventKind) String() string

type Options

type Options struct {
	// Interfaces to use; all up, multicast-capable, non point-to-point
	// interfaces when empty (loopback only when Loopback is set).
	Interfaces []net.Interface
	// Wait bounds Browse; DefaultWait when zero and ctx has no deadline.
	Wait time.Duration
	// Port is the multicast DNS port; 0 means 5353. Tests use another port
	// so they do not collide with the operating system's responder.
	Port int
	// Mode selects how Browse, BrowseTypes and Watch query; BrowseBoth
	// when zero.
	Mode BrowseMode
	// Rename, when set, picks a new name after a probing conflict; attempt
	// counts from 1. Announce retries with the returned service. It is
	// also used when a conflict appears after probing (RFC 6762 clause
	// 9); attempt then goes on counting from the renames before.
	Rename func(s Service, attempt int) Service
	// OnConflict, when set, is told about name conflicts an Announcer
	// meets after probing (seen, kept, renamed, withdrawn), and about a
	// rename during Announce itself. It is called from the Announcer's
	// own goroutines, one call at a time, and must not call Close.
	OnConflict func(ConflictEvent)
	// StrictSource makes Browse and Watch drop answers whose source
	// address is not on a link of this host (RFC 6762 clause 11), as the
	// responder always does for queries. Off by default, so that a device
	// with a misconfigured address on the segment is still found.
	StrictSource bool
	// QU sets the unicast-response bit on the first query of a browse
	// (RFC 6762 clause 5.4). Off by default: a unicast reply to port 5353
	// reaches only one of the sockets sharing it, often the operating
	// system's responder, so use it only where this process owns the port
	// or uses its own (Port).
	QU bool
	// Loopback turns on multicast loopback, so an announcer and a browser
	// on the same host see each other, and lets the default interface set
	// include the loopback interface.
	Loopback bool
}

Options configure Announce and Browse.

type Reflector added in v0.3.0

type Reflector struct {
	// contains filtered or unexported fields
}

Reflector repeats multicast DNS traffic between interfaces, so that devices on one network segment (a VLAN, a guest network) and browsers on another find each other, as Avahi's reflector does. Every mDNS message multicast on one interface is multicast unchanged on the others, IPv4 to IPv4 and IPv6 to IPv6; answers a responder sends by unicast to the reflector's own port 5353 (to a QU question it reflected) are multicast on the other interfaces too. Queries sent from other ports (one-shot queries, RFC 6762 6.7) stay on their link. Address records are not rewritten: a browser on another segment needs a route to the device. RFC 6762 does not define reflection; this is the common practice.

func Reflect added in v0.3.0

func Reflect(ctx context.Context, opts Options) (*Reflector, error)

Reflect starts a reflector on opts.Interfaces (all usable interfaces when empty; at least two are needed) and opts.Port. It runs until ctx ends or Close.

func (*Reflector) Close added in v0.3.0

func (r *Reflector) Close() error

Close stops the reflector.

func (*Reflector) Reflected added in v0.3.0

func (r *Reflector) Reflected() int64

Reflected counts the messages repeated so far (one per interface sent to).

type Service

type Service struct {
	Instance, Host string
	Port           int
	TXT            map[string]string
	IPs            []net.IP
	// Subtypes are DNS-SD subtype labels (RFC 6763 clause 7.1), for
	// example "_L840"; the responder answers PTR queries for
	// "<subtype>._sub.<service>.local." with this instance.
	Subtypes []string
}

Service is one DNS-SD service instance.

Instance is the instance label (for example "Site gateway"); it may hold spaces but no dots. Host is the host label, with or without ".local" ("myvtn" and "myvtn.local" are the same host). Port is the TCP port of the service. TXT holds the key/value metadata. IPs, when set, are the addresses the responder answers for Host; when empty it answers with the addresses of the interface a query arrived on.

Browse returns Host as "<label>.local" (no trailing dot).

func Browse

func Browse(ctx context.Context, service string, opts Options) ([]Service, error)

Browse looks for instances of service (for example "_matter._tcp" or a subtype such as "_L840._sub._matterc._udp") and returns what answered before ctx ended or opts.Wait passed.

By default (BrowseBoth) it sends one-shot queries from an ephemeral port to the mDNS group on each interface (RFC 6762 clause 5.1), to which responders reply by unicast, and the same query from the mDNS port with the groups joined (shared with the operating system's responder through SO_REUSEPORT), so responders that answer only by multicast are heard as well. Later queries list the complete instances already known (known-answer suppression) and ask directly for the SRV, TXT and address records still missing. Instances whose goodbye (TTL 0) arrives are dropped. Results are sorted by instance name; use Service.Addr to connect, which favours the .local host name.

Example

Browse finds the instances of a type: by default it queries one-shot from an ephemeral port and from the mDNS port at once.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/cB-Sigma/mdns"
)

func main() {
	ctx := context.Background()
	found, err := mdns.Browse(ctx, "_hue._tcp", mdns.Options{Wait: 3 * time.Second})
	if err != nil {
		return
	}
	for _, s := range found {
		fmt.Println(s.Instance, s.Addr(), s.TXT["bridgeid"])
	}
}

func RenameInstance

func RenameInstance(s Service, attempt int) Service

RenameInstance is an Options.Rename that keeps the host name and gives the instance a number: "Site gateway" becomes "Site gateway (2)", then "Site gateway (3)" (RFC 6762 clause 9 leaves the choice of the new name to the responder; this is the form DNS-SD browsers show).

Example

RenameInstance numbers the instance name after a conflict and keeps the host name.

package main

import (
	"fmt"

	"github.com/cB-Sigma/mdns"
)

func main() {
	s := mdns.Service{Instance: "Site gateway", Host: "gateway", Port: 8443}
	s = mdns.RenameInstance(s, 1)
	fmt.Println(s.Instance)
	fmt.Println(mdns.RenameInstance(s, 2).Instance)
	fmt.Println(s.Addr())
}
Output:
Site gateway (2)
Site gateway (3)
gateway:8443

func (Service) Addr

func (s Service) Addr() string

Addr returns host:port for connecting to the service. It favours the .local host name over an address, because DHCP-assigned addresses change; it falls back to the first IP only when no host name is known.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher browses continuously for one or more service types over one set of sockets. It queries at start like Browse, then at intervals that double up to MaxQueryInterval; it keeps every announcement heard on the mDNS port (with the default BrowseBoth or BrowseMulticast mode); it asks again for each record at 80, 85, 90 and 95 % of its TTL, so instances that only answer queries stay known; and it reports instances that appear, change and leave.

func Watch

func Watch(ctx context.Context, services []string, opts Options, fn func(Event)) (*Watcher, error)

Watch starts watching the service types until Close or the end of ctx. fn, when not nil, is called for every change, one call at a time, from the Watcher's own goroutine; it must not call Close. opts.Wait (or DefaultWait) sets when Settled is closed: by then the first queries have had the time a Browse gives them.

Example

Watch follows instances as they come, change and go.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/cB-Sigma/mdns"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()
	w, err := mdns.Watch(ctx, []string{"_ewelink._tcp", "_shelly._tcp"}, mdns.Options{}, func(e mdns.Event) {
		fmt.Println(e.Kind, e.Type, e.Service.Instance, e.Service.TXT["seq"])
	})
	if err != nil {
		return
	}
	defer w.Close()
	<-w.Settled()
	fmt.Println(len(w.Services("_ewelink._tcp")), "DIY devices so far")
	<-ctx.Done()
}

func (*Watcher) Close

func (w *Watcher) Close() error

Close stops watching and closes the sockets. It is safe to call more than once.

func (*Watcher) Done

func (w *Watcher) Done() <-chan struct{}

Done is closed when the Watcher has stopped.

func (*Watcher) Reconfirm added in v0.2.0

func (w *Watcher) Reconfirm(service, instance string) bool

Reconfirm asks the network whether instance (its label, as in Service.Instance) of service (one of the types given to Watch) is still there, for when a connection to it failed: the Watcher queries for its PTR, SRV, TXT and address records several times, and removes those that get no answer within ten seconds, reporting Removed when the instance goes (RFC 6762 clause 10.4). It returns false when the instance is not known.

Example

Reconfirm asks whether an instance a connection failed to is still there; the Watcher reports Removed if it does not answer.

package main

import (
	"context"
	"net"
	"time"

	"github.com/cB-Sigma/mdns"
)

func main() {
	ctx := context.Background()
	w, err := mdns.Watch(ctx, []string{"_hue._tcp"}, mdns.Options{}, nil)
	if err != nil {
		return
	}
	defer w.Close()
	<-w.Settled()
	for _, s := range w.Services("_hue._tcp") {
		c, err := net.DialTimeout("tcp", s.Addr(), 2*time.Second)
		if err != nil {
			w.Reconfirm("_hue._tcp", s.Instance)
			continue
		}
		_ = c.Close()
	}
}

func (*Watcher) Services

func (w *Watcher) Services(service string) []Service

Services returns the instances of service (one of the types given to Watch) known now, sorted by instance name.

func (*Watcher) Settled

func (w *Watcher) Settled() <-chan struct{}

Settled is closed once the first round of queries has had opts.Wait (or DefaultWait) to be answered, or when the Watcher stops.

Directories

Path Synopsis
internal
reuse
Package reuse sets the socket options that let several sockets (ours, the simulator's and the operating system's responder) bind the mDNS port together.
Package reuse sets the socket options that let several sockets (ours, the simulator's and the operating system's responder) bind the mDNS port together.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL