discoverymgr : customize grandcat/zeroconf to apply passive discovery 82/203982/1
authordamon92 <damon92.lee@samsung.com>
Thu, 18 Apr 2019 01:56:51 +0000 (10:56 +0900)
committerdamon92 <damon92.lee@samsung.com>
Thu, 18 Apr 2019 01:56:51 +0000 (10:56 +0900)
Change-Id: I4f427ffd06cd6cbe5d4407e700e07764c691a5d9

19 files changed:
src/discoverymgr/discovery_execution.go
src/discoverymgr/discovery_init.go
src/discoverymgr/vendor/grandcat/zeroconf/.gitignore [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/LICENSE [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/README.md [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/client.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/connection.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/doc.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/.gitignore [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/server.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/register/.gitignore [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server [new file with mode: 0755]
src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/.gitignore [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/README.md [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/client.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/server.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/service.go [new file with mode: 0644]
src/discoverymgr/vendor/grandcat/zeroconf/utils.go [new file with mode: 0644]

index 795dc71..b1fcf52 100755 (executable)
@@ -20,24 +20,17 @@ import (
        "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})
@@ -48,9 +41,11 @@ func GetDeviceList() ([]DeviceReturnInfo, error) {
 
 //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)
@@ -70,16 +65,13 @@ func discoveryBGR() {
                        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 {
index 09b8ebd..d3bc856 100755 (executable)
@@ -24,7 +24,7 @@ import (
        "strings"
        "time"
 
-       "github.com/grandcat/zeroconf"
+       "grandcat/zeroconf"
 )
 
 var gServer *zeroconf.Server
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/.gitignore b/src/discoverymgr/vendor/grandcat/zeroconf/.gitignore
new file mode 100644 (file)
index 0000000..daf913b
--- /dev/null
@@ -0,0 +1,24 @@
+# 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
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/LICENSE b/src/discoverymgr/vendor/grandcat/zeroconf/LICENSE
new file mode 100644 (file)
index 0000000..9cd4e23
--- /dev/null
@@ -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.
+
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/README.md b/src/discoverymgr/vendor/grandcat/zeroconf/README.md
new file mode 100644 (file)
index 0000000..a4590b8
--- /dev/null
@@ -0,0 +1,108 @@
+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
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/client.go b/src/discoverymgr/vendor/grandcat/zeroconf/client.go
new file mode 100644 (file)
index 0000000..53df90a
--- /dev/null
@@ -0,0 +1,458 @@
+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
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/connection.go b/src/discoverymgr/vendor/grandcat/zeroconf/connection.go
new file mode 100644 (file)
index 0000000..598dbb9
--- /dev/null
@@ -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
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/doc.go b/src/discoverymgr/vendor/grandcat/zeroconf/doc.go
new file mode 100644 (file)
index 0000000..b3e5d47
--- /dev/null
@@ -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
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/.gitignore b/src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/.gitignore
new file mode 100644 (file)
index 0000000..e0aa7cd
--- /dev/null
@@ -0,0 +1 @@
+proxyservice
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/server.go b/src/discoverymgr/vendor/grandcat/zeroconf/examples/proxyservice/server.go
new file mode 100644 (file)
index 0000000..92f4d87
--- /dev/null
@@ -0,0 +1,58 @@
+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.")
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/.gitignore b/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/.gitignore
new file mode 100644 (file)
index 0000000..5d59440
--- /dev/null
@@ -0,0 +1,2 @@
+register
+
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server b/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server
new file mode 100755 (executable)
index 0000000..4ab77da
Binary files /dev/null and b/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server differ
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server.go b/src/discoverymgr/vendor/grandcat/zeroconf/examples/register/server.go
new file mode 100644 (file)
index 0000000..d9c7798
--- /dev/null
@@ -0,0 +1,54 @@
+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.")
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/.gitignore b/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/.gitignore
new file mode 100644 (file)
index 0000000..b60b107
--- /dev/null
@@ -0,0 +1,2 @@
+resolv
+
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/README.md b/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/README.md
new file mode 100644 (file)
index 0000000..06ab963
--- /dev/null
@@ -0,0 +1,26 @@
+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
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/client.go b/src/discoverymgr/vendor/grandcat/zeroconf/examples/resolv/client.go
new file mode 100644 (file)
index 0000000..83186bc
--- /dev/null
@@ -0,0 +1,45 @@
+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)
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/server.go b/src/discoverymgr/vendor/grandcat/zeroconf/server.go
new file mode 100644 (file)
index 0000000..a390d1a
--- /dev/null
@@ -0,0 +1,803 @@
+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
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/service.go b/src/discoverymgr/vendor/grandcat/zeroconf/service.go
new file mode 100644 (file)
index 0000000..b6451f1
--- /dev/null
@@ -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),
+       }
+}
diff --git a/src/discoverymgr/vendor/grandcat/zeroconf/utils.go b/src/discoverymgr/vendor/grandcat/zeroconf/utils.go
new file mode 100644 (file)
index 0000000..39dc158
--- /dev/null
@@ -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, ".")
+}