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, probes before announcing with simultaneous-probe tie-breaking, renames after a conflict through Options.Rename, announces unsolicited, answers legacy unicast (one-shot) queries, applies known-answer suppression, 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. Watch keeps browsing: it caches unsolicited announcements, re-queries before records expire and reports instances that appear, change and leave.
Not built: NSEC negative answers, known answers split over truncated packets, and the response aggregation and rate limits a responder for a large network would need.
Index ¶
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 ¶
Types ¶
type Announcer ¶
type Announcer struct {
// contains filtered or unexported fields
}
Announcer advertises one service until Close or until its context ends.
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.
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.
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 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.
Rename func(s Service, attempt int) Service
// 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 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.
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. |