Compare commits

...
2 Commits
Author SHA1 Message Date
lukaszraczyloandGitHub 8bd14cce3b fix(mdns): answer hostname A queries after TTL (#96)
* fix(mdns): answer hostname A queries after TTL

Zeroconf answers only service queries. The A record for <alias>.local
expired from the OS cache after 120 seconds and lookups then failed.

Vendor zeroconf v1.0.0 with one change: it now answers A and AAAA
queries for the registered hostname. Fixes #95.

* fix(deps): update golang.org/x/net to v0.60.0

govulncheck reports several x/net advisories fixed in v0.60.0.
The Security Scan job fails on them.
2026-10-09 15:21:35 +01:00
github-actions[bot]andGitHub 7e3c112738 chore(deps): update module github.com/fxamacker/cbor/v2 to v2.9.6
Automated dependency update. All tests passed.
2026-10-09 03:24:20 +00:00
14 changed files with 1576 additions and 9 deletions
+4 -2
View File
@@ -29,7 +29,7 @@ require (
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.6 // indirect
github.com/go-openapi/jsonpointer v1.0.2 // indirect
github.com/go-openapi/jsonreference v1.0.3 // indirect
github.com/go-openapi/swag v0.29.2 // indirect
@@ -62,7 +62,7 @@ require (
github.com/xo/terminfo v1.2.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/oauth2 v0.37.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
@@ -80,3 +80,5 @@ require (
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
replace github.com/grandcat/zeroconf => ./third_party/zeroconf
+4 -7
View File
@@ -34,8 +34,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.6 h1:NHZPxuUjJXQjX9itA785y/5NKSlI4HQ+S7noZsmI7hs=
github.com/fxamacker/cbor/v2 v2.9.6/go.mod h1:4EaZp6ClVunETjQ4GHmGNzysDK91Lr+r6Yz6tJqzVCw=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.2 h1:NbW7xT2b+l7x+JefiPGDsZWsrLk1ogrz4AHK1TTzMt8=
@@ -81,8 +81,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -110,7 +108,6 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
@@ -142,8 +139,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+11
View File
@@ -0,0 +1,11 @@
version: "2"
# Vendored upstream code; only the documented patch is ours.
linters:
default: none
enable:
- govet
settings:
govet:
disable:
- composites
+2
View File
@@ -0,0 +1,2 @@
Vendored github.com/grandcat/zeroconf v1.0.0 (MIT, see LICENSE).
Patch: server.go handleQuestion answers A/AAAA queries for the registered hostname.
+27
View File
@@ -0,0 +1,27 @@
The MIT License (MIT)
Copyright (c) 2016 Stefan Smarzly
Copyright (c) 2014 Oleksandr Lobunets
Note: Copyright for portions of project zeroconf.sd are held by Oleksandr
Lobunets, 2014, as part of project bonjour. All other copyright for
project zeroconf.sd are held by Stefan Smarzly, 2016.
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.
+456
View File
@@ -0,0 +1,456 @@
package zeroconf
import (
"context"
"fmt"
"net"
"strings"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"time"
"github.com/cenkalti/backoff"
"github.com/miekg/dns"
)
// IPType specifies the IP traffic the client listens for.
// This does not guarantee that only mDNS entries of this sepcific
// type passes. E.g. typical mDNS packets distributed via IPv4, often contain
// both DNS A and AAAA entries.
type IPType uint8
// Options for IPType.
const (
IPv4 = 0x01
IPv6 = 0x02
IPv4AndIPv6 = (IPv4 | IPv6) //< Default option.
)
type clientOpts struct {
listenOn IPType
ifaces []net.Interface
}
// ClientOption fills the option struct to configure intefaces, etc.
type ClientOption func(*clientOpts)
// SelectIPTraffic selects the type of IP packets (IPv4, IPv6, or both) this
// instance listens for.
// This does not guarantee that only mDNS entries of this sepcific
// type passes. E.g. typical mDNS packets distributed via IPv4, may contain
// both DNS A and AAAA entries.
func SelectIPTraffic(t IPType) ClientOption {
return func(o *clientOpts) {
o.listenOn = t
}
}
// SelectIfaces selects the interfaces to query for mDNS records
func SelectIfaces(ifaces []net.Interface) ClientOption {
return func(o *clientOpts) {
o.ifaces = ifaces
}
}
// Resolver acts as entry point for service lookups and to browse the DNS-SD.
type Resolver struct {
c *client
}
// NewResolver creates a new resolver and joins the UDP multicast groups to
// listen for mDNS messages.
func NewResolver(options ...ClientOption) (*Resolver, error) {
// Apply default configuration and load supplied options.
var conf = clientOpts{
listenOn: IPv4AndIPv6,
}
for _, o := range options {
if o != nil {
o(&conf)
}
}
c, err := newClient(conf)
if err != nil {
return nil, err
}
return &Resolver{
c: c,
}, nil
}
// Browse for all services of a given type in a given domain.
func (r *Resolver) Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry) error {
params := defaultParams(service)
if domain != "" {
params.Domain = domain
}
params.Entries = entries
ctx, cancel := context.WithCancel(ctx)
go r.c.mainloop(ctx, params)
err := r.c.query(params)
if err != nil {
cancel()
return err
}
// If previous probe was ok, it should be fine now. In case of an error later on,
// the entries' queue is closed.
go func() {
if err := r.c.periodicQuery(ctx, params); err != nil {
cancel()
}
}()
return nil
}
// Lookup a specific service by its name and type in a given domain.
func (r *Resolver) Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry) error {
params := defaultParams(service)
params.Instance = instance
if domain != "" {
params.Domain = domain
}
params.Entries = entries
ctx, cancel := context.WithCancel(ctx)
go r.c.mainloop(ctx, params)
err := r.c.query(params)
if err != nil {
// cancel mainloop
cancel()
return err
}
// If previous probe was ok, it should be fine now. In case of an error later on,
// the entries' queue is closed.
go func() {
if err := r.c.periodicQuery(ctx, params); err != nil {
cancel()
}
}()
return nil
}
// defaultParams returns a default set of QueryParams.
func defaultParams(service string) *LookupParams {
return NewLookupParams("", service, "local", make(chan *ServiceEntry))
}
// Client structure encapsulates both IPv4/IPv6 UDP connections.
type client struct {
ipv4conn *ipv4.PacketConn
ipv6conn *ipv6.PacketConn
ifaces []net.Interface
}
// Client structure constructor
func newClient(opts clientOpts) (*client, error) {
ifaces := opts.ifaces
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
// IPv4 interfaces
var ipv4conn *ipv4.PacketConn
if (opts.listenOn & IPv4) > 0 {
var err error
ipv4conn, err = joinUdp4Multicast(ifaces)
if err != nil {
return nil, err
}
}
// IPv6 interfaces
var ipv6conn *ipv6.PacketConn
if (opts.listenOn & IPv6) > 0 {
var err error
ipv6conn, err = joinUdp6Multicast(ifaces)
if err != nil {
return nil, err
}
}
return &client{
ipv4conn: ipv4conn,
ipv6conn: ipv6conn,
ifaces: ifaces,
}, nil
}
// Start listeners and waits for the shutdown signal from exit channel
func (c *client) mainloop(ctx context.Context, params *LookupParams) {
// start listening for responses
msgCh := make(chan *dns.Msg, 32)
if c.ipv4conn != nil {
go c.recv(ctx, c.ipv4conn, msgCh)
}
if c.ipv6conn != nil {
go c.recv(ctx, c.ipv6conn, msgCh)
}
// Iterate through channels from listeners goroutines
var entries, sentEntries map[string]*ServiceEntry
sentEntries = make(map[string]*ServiceEntry)
for {
select {
case <-ctx.Done():
// Context expired. Notify subscriber that we are done here.
params.done()
c.shutdown()
return
case msg := <-msgCh:
entries = make(map[string]*ServiceEntry)
sections := append(msg.Answer, msg.Ns...)
sections = append(sections, msg.Extra...)
for _, answer := range sections {
switch rr := answer.(type) {
case *dns.PTR:
if params.ServiceName() != rr.Hdr.Name {
continue
}
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Ptr {
continue
}
if _, ok := entries[rr.Ptr]; !ok {
entries[rr.Ptr] = NewServiceEntry(
trimDot(strings.Replace(rr.Ptr, rr.Hdr.Name, "", -1)),
params.Service,
params.Domain)
}
entries[rr.Ptr].TTL = rr.Hdr.Ttl
case *dns.SRV:
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
continue
} else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
continue
}
if _, ok := entries[rr.Hdr.Name]; !ok {
entries[rr.Hdr.Name] = NewServiceEntry(
trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
params.Service,
params.Domain)
}
entries[rr.Hdr.Name].HostName = rr.Target
entries[rr.Hdr.Name].Port = int(rr.Port)
entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl
case *dns.TXT:
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
continue
} else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
continue
}
if _, ok := entries[rr.Hdr.Name]; !ok {
entries[rr.Hdr.Name] = NewServiceEntry(
trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
params.Service,
params.Domain)
}
entries[rr.Hdr.Name].Text = rr.Txt
entries[rr.Hdr.Name].TTL = rr.Hdr.Ttl
}
}
// Associate IPs in a second round as other fields should be filled by now.
for _, answer := range sections {
switch rr := answer.(type) {
case *dns.A:
for k, e := range entries {
if e.HostName == rr.Hdr.Name {
entries[k].AddrIPv4 = append(entries[k].AddrIPv4, rr.A)
}
}
case *dns.AAAA:
for k, e := range entries {
if e.HostName == rr.Hdr.Name {
entries[k].AddrIPv6 = append(entries[k].AddrIPv6, rr.AAAA)
}
}
}
}
}
if len(entries) > 0 {
for k, e := range entries {
if e.TTL == 0 {
delete(entries, k)
delete(sentEntries, k)
continue
}
if _, ok := sentEntries[k]; ok {
continue
}
// If this is an DNS-SD query do not throw PTR away.
// It is expected to have only PTR for enumeration
if params.ServiceRecord.ServiceTypeName() != params.ServiceRecord.ServiceName() {
// Require at least one resolved IP address for ServiceEntry
// TODO: wait some more time as chances are high both will arrive.
if len(e.AddrIPv4) == 0 && len(e.AddrIPv6) == 0 {
continue
}
}
// Submit entry to subscriber and cache it.
// This is also a point to possibly stop probing actively for a
// service entry.
params.Entries <- e
sentEntries[k] = e
params.disableProbing()
}
// reset entries
entries = make(map[string]*ServiceEntry)
}
}
}
// Shutdown client will close currently open connections and channel implicitly.
func (c *client) shutdown() {
if c.ipv4conn != nil {
c.ipv4conn.Close()
}
if c.ipv6conn != nil {
c.ipv6conn.Close()
}
}
// Data receiving routine reads from connection, unpacks packets into dns.Msg
// structures and sends them to a given msgCh channel
func (c *client) recv(ctx context.Context, l interface{}, msgCh chan *dns.Msg) {
var readFrom func([]byte) (n int, src net.Addr, err error)
switch pConn := l.(type) {
case *ipv6.PacketConn:
readFrom = func(b []byte) (n int, src net.Addr, err error) {
n, _, src, err = pConn.ReadFrom(b)
return
}
case *ipv4.PacketConn:
readFrom = func(b []byte) (n int, src net.Addr, err error) {
n, _, src, err = pConn.ReadFrom(b)
return
}
default:
return
}
buf := make([]byte, 65536)
var fatalErr error
for {
// Handles the following cases:
// - ReadFrom aborts with error due to closed UDP connection -> causes ctx cancel
// - ReadFrom aborts otherwise.
// TODO: the context check can be removed. Verify!
if ctx.Err() != nil || fatalErr != nil {
return
}
n, _, err := readFrom(buf)
if err != nil {
fatalErr = err
continue
}
msg := new(dns.Msg)
if err := msg.Unpack(buf[:n]); err != nil {
// log.Printf("[WARN] mdns: Failed to unpack packet: %v", err)
continue
}
select {
case msgCh <- msg:
// Submit decoded DNS message and continue.
case <-ctx.Done():
// Abort.
return
}
}
}
// periodicQuery sens multiple probes until a valid response is received by
// the main processing loop or some timeout/cancel fires.
// TODO: move error reporting to shutdown function as periodicQuery is called from
// go routine context.
func (c *client) periodicQuery(ctx context.Context, params *LookupParams) error {
if params.stopProbing == nil {
return nil
}
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = 4 * time.Second
bo.MaxInterval = 60 * time.Second
bo.Reset()
for {
// Do periodic query.
if err := c.query(params); err != nil {
return err
}
// Backoff and cancel logic.
wait := bo.NextBackOff()
if wait == backoff.Stop {
return fmt.Errorf("periodicQuery: abort due to timeout")
}
select {
case <-time.After(wait):
// Wait for next iteration.
case <-params.stopProbing:
// Chan is closed (or happened in the past).
// Done here. Received a matching mDNS entry.
return nil
case <-ctx.Done():
return ctx.Err()
}
}
}
// Performs the actual query by service name (browse) or service instance name (lookup),
// start response listeners goroutines and loops over the entries channel.
func (c *client) query(params *LookupParams) error {
var serviceName, serviceInstanceName string
serviceName = fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain))
if params.Instance != "" {
serviceInstanceName = fmt.Sprintf("%s.%s", params.Instance, serviceName)
}
// send the query
m := new(dns.Msg)
if serviceInstanceName != "" {
m.Question = []dns.Question{
dns.Question{serviceInstanceName, dns.TypeSRV, dns.ClassINET},
dns.Question{serviceInstanceName, dns.TypeTXT, dns.ClassINET},
}
m.RecursionDesired = false
} else {
m.SetQuestion(serviceName, dns.TypePTR)
m.RecursionDesired = false
}
if err := c.sendQuery(m); err != nil {
return err
}
return nil
}
// Pack the dns.Msg and write to available connections (multicast)
func (c *client) sendQuery(msg *dns.Msg) error {
buf, err := msg.Pack()
if err != nil {
return err
}
if c.ipv4conn != nil {
var wcm ipv4.ControlMessage
for ifi := range c.ifaces {
wcm.IfIndex = c.ifaces[ifi].Index
c.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
}
}
if c.ipv6conn != nil {
var wcm ipv6.ControlMessage
for ifi := range c.ifaces {
wcm.IfIndex = c.ifaces[ifi].Index
c.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
}
}
return nil
}
+115
View File
@@ -0,0 +1,115 @@
package zeroconf
import (
"fmt"
"net"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
// Multicast groups used by mDNS
mdnsGroupIPv4 = net.IPv4(224, 0, 0, 251)
mdnsGroupIPv6 = net.ParseIP("ff02::fb")
// mDNS wildcard addresses
mdnsWildcardAddrIPv4 = &net.UDPAddr{
IP: net.ParseIP("224.0.0.0"),
Port: 5353,
}
mdnsWildcardAddrIPv6 = &net.UDPAddr{
IP: net.ParseIP("ff02::"),
// IP: net.ParseIP("fd00::12d3:26e7:48db:e7d"),
Port: 5353,
}
// mDNS endpoint addresses
ipv4Addr = &net.UDPAddr{
IP: mdnsGroupIPv4,
Port: 5353,
}
ipv6Addr = &net.UDPAddr{
IP: mdnsGroupIPv6,
Port: 5353,
}
)
func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) {
udpConn, err := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
if err != nil {
return nil, err
}
// Join multicast groups to receive announcements
pkConn := ipv6.NewPacketConn(udpConn)
pkConn.SetControlMessage(ipv6.FlagInterface, true)
if len(interfaces) == 0 {
interfaces = listMulticastInterfaces()
}
// log.Println("Using multicast interfaces: ", interfaces)
var failedJoins int
for _, iface := range interfaces {
if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
// log.Println("Udp6 JoinGroup failed for iface ", iface)
failedJoins++
}
}
if failedJoins == len(interfaces) {
pkConn.Close()
return nil, fmt.Errorf("udp6: failed to join any of these interfaces: %v", interfaces)
}
return pkConn, nil
}
func joinUdp4Multicast(interfaces []net.Interface) (*ipv4.PacketConn, error) {
udpConn, err := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
if err != nil {
// log.Printf("[ERR] bonjour: Failed to bind to udp4 mutlicast: %v", err)
return nil, err
}
// Join multicast groups to receive announcements
pkConn := ipv4.NewPacketConn(udpConn)
pkConn.SetControlMessage(ipv4.FlagInterface, true)
if len(interfaces) == 0 {
interfaces = listMulticastInterfaces()
}
// log.Println("Using multicast interfaces: ", interfaces)
var failedJoins int
for _, iface := range interfaces {
if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
// log.Println("Udp4 JoinGroup failed for iface ", iface)
failedJoins++
}
}
if failedJoins == len(interfaces) {
pkConn.Close()
return nil, fmt.Errorf("udp4: failed to join any of these interfaces: %v", interfaces)
}
return pkConn, nil
}
func listMulticastInterfaces() []net.Interface {
var interfaces []net.Interface
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
for _, ifi := range ifaces {
if (ifi.Flags & net.FlagUp) == 0 {
continue
}
if (ifi.Flags & net.FlagMulticast) > 0 {
interfaces = append(interfaces, ifi)
}
}
return interfaces
}
+14
View File
@@ -0,0 +1,14 @@
// Package zeroconf is a pure Golang library that employs Multicast DNS-SD for
// browsing and resolving services in your network and registering own services
// in the local network.
//
// It basically implements aspects of the standards
// RFC 6762 (mDNS) and
// RFC 6763 (DNS-SD).
// Though it does not support all requirements yet, the aim is to provide a
// complient solution in the long-term with the community.
//
// By now, it should be compatible to [Avahi](http://avahi.org/) (tested) and
// Apple's Bonjour (untested). Should work in the most office, home and private
// environments.
package zeroconf
+10
View File
@@ -0,0 +1,10 @@
module github.com/grandcat/zeroconf
go 1.13
require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/miekg/dns v1.1.27
github.com/pkg/errors v0.9.1
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
)
+24
View File
@@ -0,0 +1,24 @@
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+42
View File
@@ -0,0 +1,42 @@
package zeroconf
import (
"net"
"testing"
"github.com/miekg/dns"
)
func TestHandleQuestionAnswersHostnameA(t *testing.T) {
svc := NewServiceEntry("alias", "_kportal._tcp", "local.")
svc.HostName = "alias.local."
svc.AddrIPv4 = []net.IP{net.IPv4(127, 0, 0, 1)}
s := &Server{service: svc, ttl: 3200}
q := dns.Question{Name: "alias.local.", Qtype: dns.TypeA, Qclass: dns.ClassINET}
resp := new(dns.Msg)
if err := s.handleQuestion(q, resp, new(dns.Msg), 0); err != nil {
t.Fatal(err)
}
if len(resp.Answer) != 1 {
t.Fatalf("want 1 answer, got %d", len(resp.Answer))
}
a, ok := resp.Answer[0].(*dns.A)
if !ok || !a.A.Equal(net.IPv4(127, 0, 0, 1)) || a.Hdr.Name != "alias.local." {
t.Fatalf("unexpected answer: %v", resp.Answer[0])
}
}
func TestHandleQuestionIgnoresOtherTypesForHostname(t *testing.T) {
svc := NewServiceEntry("alias", "_kportal._tcp", "local.")
svc.HostName = "alias.local."
svc.AddrIPv4 = []net.IP{net.IPv4(127, 0, 0, 1)}
s := &Server{service: svc, ttl: 3200}
q := dns.Question{Name: "alias.local.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}
resp := new(dns.Msg)
_ = s.handleQuestion(q, resp, new(dns.Msg), 0)
if len(resp.Answer) != 0 {
t.Fatalf("want no answer, got %d", len(resp.Answer))
}
}
+750
View File
@@ -0,0 +1,750 @@
package zeroconf
import (
"fmt"
"log"
"math/rand"
"net"
"os"
"strings"
"sync"
"time"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"errors"
"github.com/miekg/dns"
)
const (
// Number of Multicast responses sent for a query message (default: 1 < x < 9)
multicastRepetitions = 2
)
// Register a service by given arguments. This call will take the system's hostname
// and lookup IP by that hostname.
func Register(instance, service, domain string, port int, text []string, ifaces []net.Interface) (*Server, error) {
entry := NewServiceEntry(instance, service, domain)
entry.Port = port
entry.Text = text
if entry.Instance == "" {
return nil, fmt.Errorf("Missing service instance name")
}
if entry.Service == "" {
return nil, fmt.Errorf("Missing service name")
}
if entry.Domain == "" {
entry.Domain = "local."
}
if entry.Port == 0 {
return nil, fmt.Errorf("Missing port")
}
var err error
if entry.HostName == "" {
entry.HostName, err = os.Hostname()
if err != nil {
return nil, fmt.Errorf("Could not determine host")
}
}
if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
}
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
for _, iface := range ifaces {
v4, v6 := addrsForInterface(&iface)
entry.AddrIPv4 = append(entry.AddrIPv4, v4...)
entry.AddrIPv6 = append(entry.AddrIPv6, v6...)
}
if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil {
return nil, fmt.Errorf("Could not determine host IP addresses")
}
s, err := newServer(ifaces)
if err != nil {
return nil, err
}
s.service = entry
go s.mainloop()
go s.probe()
return s, nil
}
// RegisterProxy registers a service proxy. This call will skip the hostname/IP lookup and
// will use the provided values.
func RegisterProxy(instance, service, domain string, port int, host string, ips []string, text []string, ifaces []net.Interface) (*Server, error) {
entry := NewServiceEntry(instance, service, domain)
entry.Port = port
entry.Text = text
entry.HostName = host
if entry.Instance == "" {
return nil, fmt.Errorf("Missing service instance name")
}
if entry.Service == "" {
return nil, fmt.Errorf("Missing service name")
}
if entry.HostName == "" {
return nil, fmt.Errorf("Missing host name")
}
if entry.Domain == "" {
entry.Domain = "local"
}
if entry.Port == 0 {
return nil, fmt.Errorf("Missing port")
}
if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
}
for _, ip := range ips {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return nil, fmt.Errorf("Failed to parse given IP: %v", ip)
} else if ipv4 := ipAddr.To4(); ipv4 != nil {
entry.AddrIPv4 = append(entry.AddrIPv4, ipAddr)
} else if ipv6 := ipAddr.To16(); ipv6 != nil {
entry.AddrIPv6 = append(entry.AddrIPv6, ipAddr)
} else {
return nil, fmt.Errorf("The IP is neither IPv4 nor IPv6: %#v", ipAddr)
}
}
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
s, err := newServer(ifaces)
if err != nil {
return nil, err
}
s.service = entry
go s.mainloop()
go s.probe()
return s, nil
}
const (
qClassCacheFlush uint16 = 1 << 15
)
// Server structure encapsulates both IPv4/IPv6 UDP connections
type Server struct {
service *ServiceEntry
ipv4conn *ipv4.PacketConn
ipv6conn *ipv6.PacketConn
ifaces []net.Interface
shouldShutdown chan struct{}
shutdownLock sync.Mutex
shutdownEnd sync.WaitGroup
isShutdown bool
ttl uint32
}
// Constructs server structure
func newServer(ifaces []net.Interface) (*Server, error) {
ipv4conn, err4 := joinUdp4Multicast(ifaces)
if err4 != nil {
log.Printf("[zeroconf] no suitable IPv4 interface: %s", err4.Error())
}
ipv6conn, err6 := joinUdp6Multicast(ifaces)
if err6 != nil {
log.Printf("[zeroconf] no suitable IPv6 interface: %s", err6.Error())
}
if err4 != nil && err6 != nil {
// No supported interface left.
return nil, fmt.Errorf("No supported interface")
}
s := &Server{
ipv4conn: ipv4conn,
ipv6conn: ipv6conn,
ifaces: ifaces,
ttl: 3200,
shouldShutdown: make(chan struct{}),
}
return s, nil
}
// Start listeners and waits for the shutdown signal from exit channel
func (s *Server) mainloop() {
if s.ipv4conn != nil {
go s.recv4(s.ipv4conn)
}
if s.ipv6conn != nil {
go s.recv6(s.ipv6conn)
}
}
// Shutdown closes all udp connections and unregisters the service
func (s *Server) Shutdown() {
s.shutdown()
}
// SetText updates and announces the TXT records
func (s *Server) SetText(text []string) {
s.service.Text = text
s.announceText()
}
// TTL sets the TTL for DNS replies
func (s *Server) TTL(ttl uint32) {
s.ttl = ttl
}
// Shutdown server will close currently open connections & channel
func (s *Server) shutdown() error {
s.shutdownLock.Lock()
defer s.shutdownLock.Unlock()
if s.isShutdown {
return errors.New("Server is already shutdown")
}
err := s.unregister()
if err != nil {
return err
}
close(s.shouldShutdown)
if s.ipv4conn != nil {
s.ipv4conn.Close()
}
if s.ipv6conn != nil {
s.ipv6conn.Close()
}
// Wait for connection and routines to be closed
s.shutdownEnd.Wait()
s.isShutdown = true
return nil
}
// recv is a long running routine to receive packets from an interface
func (s *Server) recv4(c *ipv4.PacketConn) {
if c == nil {
return
}
buf := make([]byte, 65536)
s.shutdownEnd.Add(1)
defer s.shutdownEnd.Done()
for {
select {
case <-s.shouldShutdown:
return
default:
var ifIndex int
n, cm, from, err := c.ReadFrom(buf)
if err != nil {
continue
}
if cm != nil {
ifIndex = cm.IfIndex
}
if err := s.parsePacket(buf[:n], ifIndex, from); err != nil {
// log.Printf("[ERR] zeroconf: failed to handle query v4: %v", err)
}
}
}
}
// recv is a long running routine to receive packets from an interface
func (s *Server) recv6(c *ipv6.PacketConn) {
if c == nil {
return
}
buf := make([]byte, 65536)
s.shutdownEnd.Add(1)
defer s.shutdownEnd.Done()
for {
select {
case <-s.shouldShutdown:
return
default:
var ifIndex int
n, cm, from, err := c.ReadFrom(buf)
if err != nil {
continue
}
if cm != nil {
ifIndex = cm.IfIndex
}
if err := s.parsePacket(buf[:n], ifIndex, from); err != nil {
// log.Printf("[ERR] zeroconf: failed to handle query v6: %v", err)
}
}
}
}
// parsePacket is used to parse an incoming packet
func (s *Server) parsePacket(packet []byte, ifIndex int, from net.Addr) error {
var msg dns.Msg
if err := msg.Unpack(packet); err != nil {
// log.Printf("[ERR] zeroconf: Failed to unpack packet: %v", err)
return err
}
return s.handleQuery(&msg, ifIndex, from)
}
// handleQuery is used to handle an incoming query
func (s *Server) handleQuery(query *dns.Msg, ifIndex int, from net.Addr) error {
// Ignore questions with authoritative section for now
if len(query.Ns) > 0 {
return nil
}
// Handle each question
var err error
for _, q := range query.Question {
resp := dns.Msg{}
resp.SetReply(query)
resp.Compress = true
resp.RecursionDesired = false
resp.Authoritative = true
resp.Question = nil // RFC6762 section 6 "responses MUST NOT contain any questions"
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
if err = s.handleQuestion(q, &resp, query, ifIndex); err != nil {
// log.Printf("[ERR] zeroconf: failed to handle question %v: %v", q, err)
continue
}
// Check if there is an answer
if len(resp.Answer) == 0 {
continue
}
if isUnicastQuestion(q) {
// Send unicast
if e := s.unicastResponse(&resp, ifIndex, from); e != nil {
err = e
}
} else {
// Send mulicast
if e := s.multicastResponse(&resp, ifIndex); e != nil {
err = e
}
}
}
return err
}
// RFC6762 7.1. Known-Answer Suppression
func isKnownAnswer(resp *dns.Msg, query *dns.Msg) bool {
if len(resp.Answer) == 0 || len(query.Answer) == 0 {
return false
}
if resp.Answer[0].Header().Rrtype != dns.TypePTR {
return false
}
answer := resp.Answer[0].(*dns.PTR)
for _, known := range query.Answer {
hdr := known.Header()
if hdr.Rrtype != answer.Hdr.Rrtype {
continue
}
ptr := known.(*dns.PTR)
if ptr.Ptr == answer.Ptr && hdr.Ttl >= answer.Hdr.Ttl/2 {
// log.Printf("skipping known answer: %v", ptr)
return true
}
}
return false
}
// handleQuestion is used to handle an incoming question
func (s *Server) handleQuestion(q dns.Question, resp *dns.Msg, query *dns.Msg, ifIndex int) error {
if s.service == nil {
return nil
}
switch q.Name {
case s.service.ServiceTypeName():
s.serviceTypeName(resp, s.ttl)
if isKnownAnswer(resp, query) {
resp.Answer = nil
}
case s.service.ServiceName():
s.composeBrowsingAnswers(resp, ifIndex)
if isKnownAnswer(resp, query) {
resp.Answer = nil
}
case s.service.ServiceInstanceName():
s.composeLookupAnswers(resp, s.ttl, ifIndex, false)
// kportal patch: answer direct A/AAAA lookups for the registered hostname
// so the address record can be refreshed after its TTL expires.
case s.service.HostName:
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA || q.Qtype == dns.TypeANY {
resp.Answer = s.appendAddrs(resp.Answer, s.ttl, ifIndex, false)
}
}
return nil
}
func (s *Server) composeBrowsingAnswers(resp *dns.Msg, ifIndex int) {
ptr := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Ptr: s.service.ServiceInstanceName(),
}
resp.Answer = append(resp.Answer, ptr)
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
resp.Extra = append(resp.Extra, srv, txt)
resp.Extra = s.appendAddrs(resp.Extra, s.ttl, ifIndex, false)
}
func (s *Server) composeLookupAnswers(resp *dns.Msg, ttl uint32, ifIndex int, flushCache bool) {
// From RFC6762
// The most significant bit of the rrclass for a record in the Answer
// Section of a response message is the Multicast DNS cache-flush bit
// and is discussed in more detail below in Section 10.2, "Announcements
// to Flush Outdated Cache Entries".
ptr := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceInstanceName(),
}
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: ttl,
},
Txt: s.service.Text,
}
dnssd := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceTypeName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceName(),
}
resp.Answer = append(resp.Answer, srv, txt, ptr, dnssd)
resp.Answer = s.appendAddrs(resp.Answer, ttl, ifIndex, flushCache)
}
func (s *Server) serviceTypeName(resp *dns.Msg, ttl uint32) {
// From RFC6762
// 9. Service Type Enumeration
//
// For this purpose, a special meta-query is defined. A DNS query for
// PTR records with the name "_services._dns-sd._udp.<Domain>" yields a
// set of PTR records, where the rdata of each PTR record is the two-
// label <Service> name, plus the same domain, e.g.,
// "_http._tcp.<Domain>".
dnssd := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceTypeName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceName(),
}
resp.Answer = append(resp.Answer, dnssd)
}
// Perform probing & announcement
//TODO: implement a proper probing & conflict resolution
func (s *Server) probe() {
q := new(dns.Msg)
q.SetQuestion(s.service.ServiceInstanceName(), dns.TypePTR)
q.RecursionDesired = false
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
q.Ns = []dns.RR{srv, txt}
randomizer := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < multicastRepetitions; i++ {
if err := s.multicastResponse(q, 0); err != nil {
log.Println("[ERR] zeroconf: failed to send probe:", err.Error())
}
time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond)
}
// From RFC6762
// The Multicast DNS responder MUST send at least two unsolicited
// responses, one second apart. To provide increased robustness against
// packet loss, a responder MAY send up to eight unsolicited responses,
// provided that the interval between unsolicited responses increases by
// at least a factor of two with every response sent.
timeout := 1 * time.Second
for i := 0; i < multicastRepetitions; i++ {
for _, intf := range s.ifaces {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
// TODO: make response authoritative if we are the publisher
resp.Compress = true
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
s.composeLookupAnswers(resp, s.ttl, intf.Index, true)
if err := s.multicastResponse(resp, intf.Index); err != nil {
log.Println("[ERR] zeroconf: failed to send announcement:", err.Error())
}
}
time.Sleep(timeout)
timeout *= 2
}
}
// announceText sends a Text announcement with cache flush enabled
func (s *Server) announceText() {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
resp.Answer = []dns.RR{txt}
s.multicastResponse(resp, 0)
}
func (s *Server) unregister() error {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
s.composeLookupAnswers(resp, 0, 0, true)
return s.multicastResponse(resp, 0)
}
func (s *Server) appendAddrs(list []dns.RR, ttl uint32, ifIndex int, flushCache bool) []dns.RR {
v4 := s.service.AddrIPv4
v6 := s.service.AddrIPv6
if len(v4) == 0 && len(v6) == 0 {
iface, _ := net.InterfaceByIndex(ifIndex)
if iface != nil {
a4, a6 := addrsForInterface(iface)
v4 = append(v4, a4...)
v6 = append(v6, a6...)
}
}
if ttl > 0 {
// RFC6762 Section 10 says A/AAAA records SHOULD
// use TTL of 120s, to account for network interface
// and IP address changes.
ttl = 120
}
var cacheFlushBit uint16
if flushCache {
cacheFlushBit = qClassCacheFlush
}
for _, ipv4 := range v4 {
a := &dns.A{
Hdr: dns.RR_Header{
Name: s.service.HostName,
Rrtype: dns.TypeA,
Class: dns.ClassINET | cacheFlushBit,
Ttl: ttl,
},
A: ipv4,
}
list = append(list, a)
}
for _, ipv6 := range v6 {
aaaa := &dns.AAAA{
Hdr: dns.RR_Header{
Name: s.service.HostName,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET | cacheFlushBit,
Ttl: ttl,
},
AAAA: ipv6,
}
list = append(list, aaaa)
}
return list
}
func addrsForInterface(iface *net.Interface) ([]net.IP, []net.IP) {
var v4, v6, v6local []net.IP
addrs, _ := iface.Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
v4 = append(v4, ipnet.IP)
} else {
switch ip := ipnet.IP.To16(); ip != nil {
case ip.IsGlobalUnicast():
v6 = append(v6, ipnet.IP)
case ip.IsLinkLocalUnicast():
v6local = append(v6local, ipnet.IP)
}
}
}
}
if len(v6) == 0 {
v6 = v6local
}
return v4, v6
}
// unicastResponse is used to send a unicast response packet
func (s *Server) unicastResponse(resp *dns.Msg, ifIndex int, from net.Addr) error {
buf, err := resp.Pack()
if err != nil {
return err
}
addr := from.(*net.UDPAddr)
if addr.IP.To4() != nil {
if ifIndex != 0 {
var wcm ipv4.ControlMessage
wcm.IfIndex = ifIndex
_, err = s.ipv4conn.WriteTo(buf, &wcm, addr)
} else {
_, err = s.ipv4conn.WriteTo(buf, nil, addr)
}
return err
} else {
if ifIndex != 0 {
var wcm ipv6.ControlMessage
wcm.IfIndex = ifIndex
_, err = s.ipv6conn.WriteTo(buf, &wcm, addr)
} else {
_, err = s.ipv6conn.WriteTo(buf, nil, addr)
}
return err
}
}
// multicastResponse us used to send a multicast response packet
func (s *Server) multicastResponse(msg *dns.Msg, ifIndex int) error {
buf, err := msg.Pack()
if err != nil {
return err
}
if s.ipv4conn != nil {
var wcm ipv4.ControlMessage
if ifIndex != 0 {
wcm.IfIndex = ifIndex
s.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
} else {
for _, intf := range s.ifaces {
wcm.IfIndex = intf.Index
s.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
}
}
}
if s.ipv6conn != nil {
var wcm ipv6.ControlMessage
if ifIndex != 0 {
wcm.IfIndex = ifIndex
s.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
} else {
for _, intf := range s.ifaces {
wcm.IfIndex = intf.Index
s.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
}
}
}
return nil
}
func isUnicastQuestion(q dns.Question) bool {
// From RFC6762
// 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.)
return q.Qclass&qClassCacheFlush != 0
}
+109
View File
@@ -0,0 +1,109 @@
package zeroconf
import (
"fmt"
"net"
"sync"
)
// ServiceRecord contains the basic description of a service, which contains instance name, service type & domain
type ServiceRecord struct {
Instance string `json:"name"` // Instance name (e.g. "My web page")
Service string `json:"type"` // Service name (e.g. _http._tcp.)
Domain string `json:"domain"` // If blank, assumes "local"
// private variable populated on ServiceRecord creation
serviceName string
serviceInstanceName string
serviceTypeName string
}
// ServiceName returns a complete service name (e.g. _foobar._tcp.local.), which is composed
// of a service name (also referred as service type) and a domain.
func (s *ServiceRecord) ServiceName() string {
return s.serviceName
}
// ServiceInstanceName returns a complete service instance name (e.g. MyDemo\ Service._foobar._tcp.local.),
// which is composed from service instance name, service name and a domain.
func (s *ServiceRecord) ServiceInstanceName() string {
return s.serviceInstanceName
}
// ServiceTypeName returns the complete identifier for a DNS-SD query.
func (s *ServiceRecord) ServiceTypeName() string {
return s.serviceTypeName
}
// NewServiceRecord constructs a ServiceRecord.
func NewServiceRecord(instance, service, domain string) *ServiceRecord {
s := &ServiceRecord{
Instance: instance,
Service: service,
Domain: domain,
serviceName: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)),
}
// Cache service instance name
if instance != "" {
s.serviceInstanceName = fmt.Sprintf("%s.%s", trimDot(s.Instance), s.ServiceName())
}
// Cache service type name domain
typeNameDomain := "local"
if len(s.Domain) > 0 {
typeNameDomain = trimDot(s.Domain)
}
s.serviceTypeName = fmt.Sprintf("_services._dns-sd._udp.%s.", typeNameDomain)
return s
}
// LookupParams contains configurable properties to create a service discovery request
type LookupParams struct {
ServiceRecord
Entries chan<- *ServiceEntry // Entries Channel
stopProbing chan struct{}
once sync.Once
}
// NewLookupParams constructs a LookupParams.
func NewLookupParams(instance, service, domain string, entries chan<- *ServiceEntry) *LookupParams {
return &LookupParams{
ServiceRecord: *NewServiceRecord(instance, service, domain),
Entries: entries,
stopProbing: make(chan struct{}),
}
}
// Notify subscriber that no more entries will arrive. Mostly caused
// by an expired context.
func (l *LookupParams) done() {
close(l.Entries)
}
func (l *LookupParams) disableProbing() {
l.once.Do(func() { close(l.stopProbing) })
}
// ServiceEntry represents a browse/lookup result for client API.
// It is also used to configure service registration (server API), which is
// used to answer multicast queries.
type ServiceEntry struct {
ServiceRecord
HostName string `json:"hostname"` // Host machine DNS name
Port int `json:"port"` // Service Port
Text []string `json:"text"` // Service info served as a TXT record
TTL uint32 `json:"ttl"` // TTL of the service record
AddrIPv4 []net.IP `json:"-"` // Host machine IPv4 address
AddrIPv6 []net.IP `json:"-"` // Host machine IPv6 address
}
// NewServiceEntry constructs a ServiceEntry.
func NewServiceEntry(instance, service, domain string) *ServiceEntry {
return &ServiceEntry{
ServiceRecord: *NewServiceRecord(instance, service, domain),
}
}
+8
View File
@@ -0,0 +1,8 @@
package zeroconf
import "strings"
// trimDot is used to trim the dots from the start or end of a string
func trimDot(s string) string {
return strings.Trim(s, ".")
}