"context"
"log"
"net"
- "sync"
"time"
- "github.com/grandcat/zeroconf"
+ "grandcat/zeroconf"
)
-var deviceMap map[string][]string
-var mapMTX sync.Mutex
-
-func init() {
- deviceMap = make(map[string][]string)
-}
-
//GetDeviceList retune device list
func GetDeviceList() ([]DeviceReturnInfo, error) {
-
+ zeroconf.MapMTX.Lock()
+ defer zeroconf.MapMTX.Unlock()
var ret []DeviceReturnInfo
- for key, value := range deviceMap {
+ for key, value := range zeroconf.DeviceMap {
ret = append(ret, DeviceReturnInfo{
DeviceIP: key,
ServiceNames: value})
//GetDeviceListWithService retune device list
func GetDeviceListWithService(target string) ([]string, error) {
+ zeroconf.MapMTX.Lock()
+ defer zeroconf.MapMTX.Unlock()
var ret []string
- for key, value := range deviceMap {
+ for key, value := range zeroconf.DeviceMap {
for _, val := range value {
if val == target {
ret = append(ret, key)
continue
}
- mapMTX.Lock()
- // //Todo check the entity in map but not discovered
- // for k, v in range deviceMap {
-
- // }
+ zeroconf.MapMTX.Lock()
+ //@Todo check the entity in map but not discovered
for k, v := range data {
log.Println(logPrefix, "[discoveryBGR]", k, v)
- deviceMap[k] = v
+ zeroconf.DeviceMap[k] = v
}
- mapMTX.Unlock()
+ zeroconf.MapMTX.Unlock()
time.Sleep(time.Duration(discoveryPeriod) * time.Millisecond)
if discoveryPeriod > 60*60000 {
"strings"
"time"
- "github.com/grandcat/zeroconf"
+ "grandcat/zeroconf"
)
var gServer *zeroconf.Server
--- /dev/null
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
--- /dev/null
+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.
+
--- /dev/null
+ZeroConf: Service Discovery with mDNS
+=====================================
+ZeroConf is a pure Golang library that employs Multicast DNS-SD for
+
+* browsing and resolving services in your network
+* registering own services
+
+in the local network.
+
+It basically implements aspects of the standards
+[RFC 6762](https://tools.ietf.org/html/rfc6762) (mDNS) and
+[RFC 6763](https://tools.ietf.org/html/rfc6763) (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).
+Target environments: private LAN/Wifi, small or isolated networks.
+
+[![GoDoc](https://godoc.org/github.com/grandcat/zeroconf?status.svg)](https://godoc.org/github.com/grandcat/zeroconf)
+[![Go Report Card](https://goreportcard.com/badge/github.com/grandcat/zeroconf)](https://goreportcard.com/report/github.com/grandcat/zeroconf)
+
+## Install
+Nothing is as easy as that:
+```bash
+$ go get -u github.com/grandcat/zeroconf
+```
+This package requires **Go 1.7** (context in std lib) or later.
+
+## Browse for services in your local network
+
+```go
+// Discover all services on the network (e.g. _workstation._tcp)
+resolver, err := zeroconf.NewResolver(nil)
+if err != nil {
+ log.Fatalln("Failed to initialize resolver:", err.Error())
+}
+
+entries := make(chan *zeroconf.ServiceEntry)
+go func(results <-chan *zeroconf.ServiceEntry) {
+ for entry := range results {
+ log.Println(entry)
+ }
+ log.Println("No more entries.")
+}(entries)
+
+ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
+defer cancel()
+err = resolver.Browse(ctx, "_workstation._tcp", "local.", entries)
+if err != nil {
+ log.Fatalln("Failed to browse:", err.Error())
+}
+
+<-ctx.Done()
+```
+See https://github.com/grandcat/zeroconf/blob/master/examples/resolv/client.go.
+
+## Lookup a specific service instance
+
+```go
+// Example filled soon.
+```
+
+## Register a service
+
+```go
+server, err := zeroconf.Register("GoZeroconf", "_workstation._tcp", "local.", 42424, []string{"txtv=0", "lo=1", "la=2"}, nil)
+if err != nil {
+ panic(err)
+}
+defer server.Shutdown()
+
+// Clean exit.
+sig := make(chan os.Signal, 1)
+signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
+select {
+case <-sig:
+ // Exit by user
+case <-time.After(time.Second * 120):
+ // Exit by timeout
+}
+
+log.Println("Shutting down.")
+```
+See https://github.com/grandcat/zeroconf/blob/master/examples/register/server.go.
+
+## Features and ToDo's
+This list gives a quick impression about the state of this library.
+See what needs to be done and submit a pull request :)
+
+* [x] Browse / Lookup / Register services
+* [x] Multiple IPv6 / IPv4 addresses support
+* [x] Send multiple probes (exp. back-off) if no service answers (*)
+* [ ] Timestamp entries for TTL checks
+* [ ] Compare new multicasts with already received services
+
+_Notes:_
+
+(*) The denoted functionalities might not be 100% standard conform, but should not be a deal breaker.
+ Some test scenarios demonstrated that the overall robustness and performance increases when applying the suggested improvements.
+
+## Credits
+Great thanks to [hashicorp](https://github.com/hashicorp/mdns) and to [oleksandr](https://github.com/oleksandr/bonjour) and all contributing authors for the code this projects bases upon.
+Large parts of the code are still the same.
+
+However, there are several reasons why I decided to create a fork of the original project:
+The previous project seems to be unmaintained. There are several useful pull requests waiting. I merged most of them in this project.
+Still, the implementation has some bugs and lacks some other features that make it quite unreliable in real LAN environments when running continously.
+Last but not least, the aim for this project is to build a solution that targets standard conformance in the long term with the support of the community.
+Though, resiliency should remain a top goal.
\ No newline at end of file
--- /dev/null
+package zeroconf
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "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 {
+ log.Println("periodicQuery: abort due to timeout")
+ return nil
+ }
+ 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
+}
--- /dev/null
+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
+}
--- /dev/null
+// 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
--- /dev/null
+proxyservice
--- /dev/null
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "time"
+
+ "github.com/grandcat/zeroconf"
+)
+
+var (
+ name = flag.String("name", "GoZeroconfGo", "The name for the service.")
+ service = flag.String("service", "_workstation._tcp", "Set the service type of the new service.")
+ domain = flag.String("domain", "local.", "Set the network domain. Default should be fine.")
+ host = flag.String("host", "pc1", "Set host name for service.")
+ ip = flag.String("ip", "::1", "Set IP a service should be reachable.")
+ port = flag.Int("port", 42424, "Set the port the service is listening to.")
+ waitTime = flag.Int("wait", 10, "Duration in [s] to publish service for.")
+)
+
+func main() {
+ flag.Parse()
+
+ server, err := zeroconf.RegisterProxy(*name, *service, *domain, *port, *host, []string{*ip}, []string{"txtv=0", "lo=1", "la=2"}, nil)
+ if err != nil {
+ panic(err)
+ }
+ defer server.Shutdown()
+ log.Println("Published proxy service:")
+ log.Println("- Name:", *name)
+ log.Println("- Type:", *service)
+ log.Println("- Domain:", *domain)
+ log.Println("- Port:", *port)
+ log.Println("- Host:", *host)
+ log.Println("- IP:", *ip)
+
+ // Clean exit.
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
+ // Timeout timer.
+ var tc <-chan time.Time
+ if *waitTime > 0 {
+ tc = time.After(time.Second * time.Duration(*waitTime))
+ }
+
+ select {
+ case <-sig:
+ // Exit by user
+ case <-tc:
+ // Exit by timeout
+ }
+
+ log.Println("Shutting down.")
+}
--- /dev/null
+register
+
--- /dev/null
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "time"
+
+ "github.com/grandcat/zeroconf"
+)
+
+var (
+ name = flag.String("name", "GoZeroconfGo", "The name for the service.")
+ service = flag.String("service", "_orchestration._tcp", "Set the service type of the new service.")
+ domain = flag.String("domain", "local.", "Set the network domain. Default should be fine.")
+ port = flag.Int("port", 42424, "Set the port the service is listening to.")
+ waitTime = flag.Int("wait", 10, "Duration in [s] to publish service for.")
+)
+
+func main() {
+ flag.Parse()
+
+ server, err := zeroconf.Register(*name, *service, *domain, *port, []string{"txtv=0", "lo=1", "la=2"}, nil)
+ if err != nil {
+ panic(err)
+ }
+ defer server.Shutdown()
+ log.Println("Published service:")
+ log.Println("- Name:", *name)
+ log.Println("- Type:", *service)
+ log.Println("- Domain:", *domain)
+ log.Println("- Port:", *port)
+
+ // Clean exit.
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
+ // Timeout timer.
+ var tc <-chan time.Time
+ if *waitTime > 0 {
+ tc = time.After(time.Second * time.Duration(*waitTime))
+ }
+
+ select {
+ case <-sig:
+ // Exit by user
+ case <-tc:
+ // Exit by timeout
+ }
+
+ log.Println("Shutting down.")
+}
--- /dev/null
+Browse and Resolve
+==================
+Compile:
+```bash
+go build -v
+```
+
+Browse for available services in your local network:
+```bash
+./resolv
+```
+By default, it shows all working stations in your network running
+a mDNS service like Avahi.
+The output should look similar to this one:
+```
+2016/12/04 00:40:23 &{{stefanserver _workstation._tcp local. } stefan.local. 50051 [] 120 [192.168.42.42] [fd00::86a6:c8ff:fe62:4242]}
+2016/12/04 00:40:23 stefanserver
+2016/12/04 00:40:28 No more entries.
+```
+The `-wait` parameter enables to wait for a specific time until
+it stops listening for new services.
+
+For a list of all possible options, just have a look at:
+```bash
+./resolv --help
+```
\ No newline at end of file
--- /dev/null
+package main
+
+import (
+ "context"
+ "flag"
+ "log"
+ "time"
+
+ "github.com/grandcat/zeroconf"
+)
+
+var (
+ service = flag.String("service", "_workstation._tcp", "Set the service category to look for devices.")
+ domain = flag.String("domain", "local", "Set the search domain. For local networks, default is fine.")
+ waitTime = flag.Int("wait", 10, "Duration in [s] to run discovery.")
+)
+
+func main() {
+ flag.Parse()
+
+ // Discover all services on the network (e.g. _workstation._tcp)
+ resolver, err := zeroconf.NewResolver(nil)
+ if err != nil {
+ log.Fatalln("Failed to initialize resolver:", err.Error())
+ }
+
+ entries := make(chan *zeroconf.ServiceEntry)
+ go func(results <-chan *zeroconf.ServiceEntry) {
+ for entry := range results {
+ log.Println(entry)
+ }
+ log.Println("No more entries.")
+ }(entries)
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(*waitTime))
+ defer cancel()
+ err = resolver.Browse(ctx, *service, *domain, entries)
+ if err != nil {
+ log.Fatalln("Failed to browse:", err.Error())
+ }
+
+ <-ctx.Done()
+ // Wait some additional time to see debug messages on go routine shutdown.
+ time.Sleep(1 * time.Second)
+}
--- /dev/null
+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
+ serviceType = "_orchestration._tcp"
+ domain = "local"
+)
+
+var DeviceMap map[string][]string
+var MapMTX sync.Mutex
+
+func init() {
+ DeviceMap = make(map[string][]string)
+}
+
+// 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) {
+ log.Println("[Register][IN]")
+ defer log.Println("[Register][OUT]")
+ 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)
+}
+
+func registerAdvertisedEntity(msg *dns.Msg, srcIP string) error {
+ log.Println("[registerAdvertisedEntity][IN] ", srcIP)
+ defer log.Println("[registerAdvertisedEntity][OUT]")
+
+ var entry *ServiceEntry
+ sections := append(msg.Answer, msg.Ns...)
+ sections = append(sections, msg.Extra...)
+
+ params := defaultParams(serviceType)
+ params.Domain = domain
+
+ for _, answer := range sections {
+ switch rr := answer.(type) {
+ case *dns.TXT:
+ log.Println("[registerAdvertisedEntity][case *dns.TXT:]", rr.Hdr.Name)
+ if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
+ continue
+ } else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
+ continue
+ }
+ entry = NewServiceEntry(
+ trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
+ params.Service,
+ params.Domain)
+ entry.Text = rr.Txt
+ entry.TTL = rr.Hdr.Ttl
+ default:
+ log.Println("[registerAdvertisedEntity][case Not *dns.TXT:]")
+ continue
+ }
+ }
+ if entry == nil {
+ return errors.New("NO dns.TXT")
+ }
+ log.Println("[registerAdvertisedEntity]Discoverd Device: " + srcIP)
+ ServiceNames := make([]string, len(entry.Text))
+ ServiceNames = entry.Text
+ MapMTX.Lock()
+ DeviceMap[srcIP] = ServiceNames
+ MapMTX.Unlock()
+
+ return nil
+}
+
+// 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 {
+ deviceIP := from.String()
+ if strings.Contains(deviceIP, ".") {
+ deviceIPPort := strings.Split(deviceIP, ":")
+ return registerAdvertisedEntity(query, deviceIPPort[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)
+ }
+
+ 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
+}
--- /dev/null
+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),
+ }
+}
--- /dev/null
+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, ".")
+}