diff --git a/README.md b/README.md index f639e8cd..35c4c7b4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -ZeroConf: Service Discovery with mDNS -===================================== +# ZeroConf: Service Discovery with mDNS + ZeroConf is a pure Golang library that employs Multicast DNS-SD for * browsing and resolving services in your network @@ -13,17 +13,20 @@ It basically implements aspects of the standards Though it does not support all requirements yet, the aim is to provide a compliant 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. +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) [![Build Status](https://travis-ci.com/grandcat/zeroconf.svg?branch=master)](https://travis-ci.com/grandcat/zeroconf) ## Install + Nothing is as easy as that: + ```bash -$ go get -u github.com/grandcat/zeroconf +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 @@ -52,9 +55,10 @@ if err != nil { <-ctx.Done() ``` + A subtype may added to service name to narrow the set of results. E.g. to browse `_workstation._tcp` with subtype `_windows`, use`_workstation._tcp,_windows`. -See https://github.com/grandcat/zeroconf/blob/master/examples/resolv/client.go. +See [examples/resolv/client.go](examples/resolv/client.go). ## Lookup a specific service instance @@ -83,11 +87,13 @@ case <-time.After(time.Second * 120): log.Println("Shutting down.") ``` + Multiple subtypes may be added to service name, separated by commas. E.g `_workstation._tcp,_windows` has subtype `_windows`. -See https://github.com/grandcat/zeroconf/blob/master/examples/register/server.go. +See [examples/register/server.go](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 :) @@ -103,11 +109,12 @@ _Notes:_ Some tests showed improvements in overall robustness and performance with the features enabled. ## 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. +Still, the implementation has some bugs and lacks some other features that make it quite unreliable in real LAN environments when running continuously. 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. diff --git a/client.go b/client.go index e4f649bd..8b1ce01c 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ import ( ) // IPType specifies the IP traffic the client listens for. -// This does not guarantee that only mDNS entries of this sepcific +// This does not guarantee that only mDNS entries of this specific // type passes. E.g. typical mDNS packets distributed via IPv4, often contain // both DNS A and AAAA entries. type IPType uint8 @@ -31,12 +31,12 @@ type clientOpts struct { ifaces []net.Interface } -// ClientOption fills the option struct to configure intefaces, etc. +// ClientOption fills the option struct to configure interfaces, 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 +// This does not guarantee that only mDNS entries of this specific // type passes. E.g. typical mDNS packets distributed via IPv4, may contain // both DNS A and AAAA entries. func SelectIPTraffic(t IPType) ClientOption { @@ -154,7 +154,7 @@ func newClient(opts clientOpts) (*client, error) { var ipv4conn *ipv4.PacketConn if (opts.listenOn & IPv4) > 0 { var err error - ipv4conn, err = joinUdp4Multicast(ifaces) + ipv4conn, err = joinUDP4Multicast(ifaces) if err != nil { return nil, err } @@ -163,7 +163,7 @@ func newClient(opts clientOpts) (*client, error) { var ipv6conn *ipv6.PacketConn if (opts.listenOn & IPv6) > 0 { var err error - ipv6conn, err = joinUdp6Multicast(ifaces) + ipv6conn, err = joinUDP6Multicast(ifaces) if err != nil { return nil, err } @@ -295,8 +295,6 @@ func (c *client) mainloop(ctx context.Context, params *LookupParams) { sentEntries[k] = e params.disableProbing() } - // reset entries - entries = make(map[string]*ServiceEntry) } } } @@ -413,8 +411,8 @@ func (c *client) query(params *LookupParams) error { if params.Instance != "" { // service instance name lookup serviceInstanceName = fmt.Sprintf("%s.%s", params.Instance, serviceName) m.Question = []dns.Question{ - dns.Question{serviceInstanceName, dns.TypeSRV, dns.ClassINET}, - dns.Question{serviceInstanceName, dns.TypeTXT, dns.ClassINET}, + {Name: serviceInstanceName, Qtype: dns.TypeSRV, Qclass: dns.ClassINET}, + {Name: serviceInstanceName, Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, } } else if len(params.Subtypes) > 0 { // service subtype browse m.SetQuestion(params.Subtypes[0], dns.TypePTR) @@ -438,15 +436,19 @@ func (c *client) sendQuery(msg *dns.Msg) error { 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.ifaces[ifi].Flags&net.FlagUp != 0 { + 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) + if c.ifaces[ifi].Flags&net.FlagUp != 0 { + wcm.IfIndex = c.ifaces[ifi].Index + c.ipv6conn.WriteTo(buf, &wcm, ipv6Addr) + } } } return nil diff --git a/connection.go b/connection.go index 598dbb99..cba6054e 100644 --- a/connection.go +++ b/connection.go @@ -35,7 +35,7 @@ var ( } ) -func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) { +func joinUDP6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) { udpConn, err := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) if err != nil { return nil, err @@ -43,7 +43,10 @@ func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) { // Join multicast groups to receive announcements pkConn := ipv6.NewPacketConn(udpConn) - pkConn.SetControlMessage(ipv6.FlagInterface, true) + err = pkConn.SetControlMessage(ipv6.FlagInterface, true) + if err != nil { + return nil, err + } if len(interfaces) == 0 { interfaces = listMulticastInterfaces() @@ -65,16 +68,19 @@ func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) { return pkConn, nil } -func joinUdp4Multicast(interfaces []net.Interface) (*ipv4.PacketConn, error) { +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) + // log.Printf("[ERR] bonjour: Failed to bind to udp4 multicast: %v", err) return nil, err } // Join multicast groups to receive announcements pkConn := ipv4.NewPacketConn(udpConn) - pkConn.SetControlMessage(ipv4.FlagInterface, true) + err = pkConn.SetControlMessage(ipv4.FlagInterface, true) + if err != nil { + return nil, err + } if len(interfaces) == 0 { interfaces = listMulticastInterfaces() diff --git a/doc.go b/doc.go index b3e5d47a..c2a73195 100644 --- a/doc.go +++ b/doc.go @@ -6,7 +6,7 @@ // 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. +// compliant 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 diff --git a/server.go b/server.go index fc6650cf..54bae78e 100644 --- a/server.go +++ b/server.go @@ -29,23 +29,23 @@ func Register(instance, service, domain string, port int, text []string, ifaces entry.Text = text if entry.Instance == "" { - return nil, fmt.Errorf("Missing service instance name") + return nil, fmt.Errorf("missing service instance name") } if entry.Service == "" { - return nil, fmt.Errorf("Missing service name") + return nil, fmt.Errorf("missing service name") } if entry.Domain == "" { entry.Domain = "local." } if entry.Port == 0 { - return nil, fmt.Errorf("Missing port") + 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") + return nil, fmt.Errorf("could not determine host") } } @@ -64,7 +64,7 @@ func Register(instance, service, domain string, port int, text []string, ifaces } if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil { - return nil, fmt.Errorf("Could not determine host IP addresses") + return nil, fmt.Errorf("could not determine host IP addresses") } s, err := newServer(ifaces) @@ -88,19 +88,19 @@ func RegisterProxy(instance, service, domain string, port int, host string, ips entry.HostName = host if entry.Instance == "" { - return nil, fmt.Errorf("Missing service instance name") + return nil, fmt.Errorf("missing service instance name") } if entry.Service == "" { - return nil, fmt.Errorf("Missing service name") + return nil, fmt.Errorf("missing service name") } if entry.HostName == "" { - return nil, fmt.Errorf("Missing host name") + return nil, fmt.Errorf("missing host name") } if entry.Domain == "" { entry.Domain = "local" } if entry.Port == 0 { - return nil, fmt.Errorf("Missing port") + return nil, fmt.Errorf("missing port") } if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) { @@ -110,13 +110,13 @@ func RegisterProxy(instance, service, domain string, port int, host string, ips for _, ip := range ips { ipAddr := net.ParseIP(ip) if ipAddr == nil { - return nil, fmt.Errorf("Failed to parse given IP: %v", ip) + 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) + return nil, fmt.Errorf("the IP is neither IPv4 nor IPv6: %#v", ipAddr) } } @@ -156,17 +156,17 @@ type Server struct { // Constructs server structure func newServer(ifaces []net.Interface) (*Server, error) { - ipv4conn, err4 := joinUdp4Multicast(ifaces) + ipv4conn, err4 := joinUDP4Multicast(ifaces) if err4 != nil { log.Printf("[zeroconf] no suitable IPv4 interface: %s", err4.Error()) } - ipv6conn, err6 := joinUdp6Multicast(ifaces) + 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") + return nil, fmt.Errorf("no supported interface") } s := &Server{ @@ -191,14 +191,14 @@ func (s *Server) mainloop() { } // Shutdown closes all udp connections and unregisters the service -func (s *Server) Shutdown() { - s.shutdown() +func (s *Server) Shutdown() error { + return s.shutdown() } // SetText updates and announces the TXT records -func (s *Server) SetText(text []string) { +func (s *Server) SetText(text []string) error { s.service.Text = text - s.announceText() + return s.announceText() } // TTL sets the TTL for DNS replies @@ -331,7 +331,7 @@ func (s *Server) handleQuery(query *dns.Msg, ifIndex int, from net.Addr) error { err = e } } else { - // Send mulicast + // Send multicast if e := s.multicastResponse(&resp, ifIndex); e != nil { err = e } @@ -592,7 +592,7 @@ func (s *Server) probe() { } // announceText sends a Text announcement with cache flush enabled -func (s *Server) announceText() { +func (s *Server) announceText() error { resp := new(dns.Msg) resp.MsgHdr.Response = true @@ -607,7 +607,7 @@ func (s *Server) announceText() { } resp.Answer = []dns.RR{txt} - s.multicastResponse(resp, 0) + return s.multicastResponse(resp, 0) } func (s *Server) unregister() error { @@ -706,16 +706,16 @@ func (s *Server) unicastResponse(resp *dns.Msg, ifIndex int, from net.Addr) erro _, err = s.ipv4conn.WriteTo(buf, nil, addr) } return err + } + if ifIndex != 0 { + var wcm ipv6.ControlMessage + wcm.IfIndex = ifIndex + _, err = s.ipv6conn.WriteTo(buf, &wcm, addr) } 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 + _, err = s.ipv6conn.WriteTo(buf, nil, addr) } + return err + } // multicastResponse us used to send a multicast response packet diff --git a/service_test.go b/service_test.go index e0ae1bf7..20da4460 100644 --- a/service_test.go +++ b/service_test.go @@ -2,6 +2,7 @@ package zeroconf import ( "context" + "fmt" "log" "testing" "time" @@ -23,13 +24,16 @@ func startMDNS(ctx context.Context, port int, name, service, domain string) { if err != nil { panic(errors.Wrap(err, "while registering mdns service")) } - defer server.Shutdown() + defer func() { + if err := server.Shutdown(); err != nil { + panic(errors.Wrap(err, "while shutting mdns service")) + } + }() log.Printf("Published service: %s, type: %s, domain: %s", name, service, domain) <-ctx.Done() log.Printf("Shutting down.") - } func TestBasic(t *testing.T) { @@ -79,12 +83,13 @@ func TestNoRegister(t *testing.T) { t.Fatalf("Expected create resolver success, but got %v", err) } - // before register, mdns resolve shuold not have any entry + // before register, mdns resolve should not have any entry entries := make(chan *ServiceEntry) + var resultError error go func(results <-chan *ServiceEntry) { s := <-results if s != nil { - t.Fatalf("Expected empty service entries but got %v", *s) + resultError = fmt.Errorf("Expected empty service entries but got %v", *s) } }(entries) @@ -94,6 +99,9 @@ func TestNoRegister(t *testing.T) { } <-ctx.Done() cancel() + if resultError != nil { + t.Fatal(resultError) + } } func TestSubtype(t *testing.T) {