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 ¶
- Constants
- Variables
- func BrowseServiceTypes(ctx context.Context, opts Options) ([]string, error)
- func BrowseTypes(ctx context.Context, services []string, opts Options) (map[string][]Service, error)
- func TestInterface(port int) (*net.Interface, error)
- type Announcer
- type BrowseMode
- type ConflictEvent
- type ConflictKind
- type Domains
- type Event
- type EventKind
- type Options
- type Reflector
- type Service
- type Watcher
Examples ¶
Constants ¶
const DefaultPort = 5353
DefaultPort is the multicast DNS port (RFC 6762 clause 3).
const DefaultWait = 3 * time.Second
DefaultWait is how long Browse collects answers when neither opts.Wait nor a context deadline bounds it.
const MaxRenames = 15
MaxRenames bounds automatic renaming after conflicts (RFC 6762 clause 9 limits a responder to 15 conflicts in 10 seconds).
Variables ¶
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).
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
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)
}
Output:
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 ¶
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"})
}
Output:
func (*Announcer) Close ¶
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
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
Service returns the service as announced now: after a rename it carries the new instance or host name, after UpdateTXT the new metadata.
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()
}
Output:
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.".
type Event ¶
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.
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
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.
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 ¶
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"])
}
}
Output:
func RenameInstance ¶
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
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 ¶
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()
}
Output:
func (*Watcher) Close ¶
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
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()
}
}
Output:
Source Files
¶
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. |