pax_global_header00006660000000000000000000000064141646377770014540gustar00rootroot0000000000000052 comment=c99a446652273d3db8d75933780b816ec1c2a4f7 mdns-1.0.5/000077500000000000000000000000001416463777700125045ustar00rootroot00000000000000mdns-1.0.5/.gitignore000066400000000000000000000004031416463777700144710ustar00rootroot00000000000000# 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 mdns-1.0.5/LICENSE000066400000000000000000000020671416463777700135160ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Armon Dadgar 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. mdns-1.0.5/README.md000066400000000000000000000024261416463777700137670ustar00rootroot00000000000000mdns ==== Simple mDNS client/server library in Golang. mDNS or Multicast DNS can be used to discover services on the local network without the use of an authoritative DNS server. This enables peer-to-peer discovery. It is important to note that many networks restrict the use of multicasting, which prevents mDNS from functioning. Notably, multicast cannot be used in any sort of cloud, or shared infrastructure environment. However it works well in most office, home, or private infrastructure environments. Using the library is very simple, here is an example of publishing a service entry: // Setup our service export host, _ := os.Hostname() info := []string{"My awesome service"} service, _ := mdns.NewMDNSService(host, "_foobar._tcp", "", "", 8000, nil, info) // Create the mDNS server, defer shutdown server, _ := mdns.NewServer(&mdns.Config{Zone: service}) defer server.Shutdown() Doing a lookup for service providers is also very simple: // Make a channel for results and start listening entriesCh := make(chan *mdns.ServiceEntry, 4) go func() { for entry := range entriesCh { fmt.Printf("Got new entry: %v\n", entry) } }() // Start the lookup mdns.Lookup("_foobar._tcp", entriesCh) close(entriesCh) mdns-1.0.5/client.go000066400000000000000000000241121416463777700143110ustar00rootroot00000000000000package mdns import ( "fmt" "log" "net" "strings" "sync/atomic" "time" "github.com/miekg/dns" "golang.org/x/net/ipv4" "golang.org/x/net/ipv6" ) // ServiceEntry is returned after we query for a service type ServiceEntry struct { Name string Host string AddrV4 net.IP AddrV6 net.IP Port int Info string InfoFields []string Addr net.IP // @Deprecated hasTXT bool sent bool } // complete is used to check if we have all the info we need func (s *ServiceEntry) complete() bool { return (s.AddrV4 != nil || s.AddrV6 != nil || s.Addr != nil) && s.Port != 0 && s.hasTXT } // QueryParam is used to customize how a Lookup is performed type QueryParam struct { Service string // Service to lookup Domain string // Lookup domain, default "local" Timeout time.Duration // Lookup timeout, default 1 second Interface *net.Interface // Multicast interface to use Entries chan<- *ServiceEntry // Entries Channel WantUnicastResponse bool // Unicast response desired, as per 5.4 in RFC DisableIPv4 bool // Whether to disable usage of IPv4 for MDNS operations. Does not affect discovered addresses. DisableIPv6 bool // Whether to disable usage of IPv6 for MDNS operations. Does not affect discovered addresses. } // DefaultParams is used to return a default set of QueryParam's func DefaultParams(service string) *QueryParam { return &QueryParam{ Service: service, Domain: "local", Timeout: time.Second, Entries: make(chan *ServiceEntry), WantUnicastResponse: false, // TODO(reddaly): Change this default. DisableIPv4: false, DisableIPv6: false, } } // Query looks up a given service, in a domain, waiting at most // for a timeout before finishing the query. The results are streamed // to a channel. Sends will not block, so clients should make sure to // either read or buffer. func Query(params *QueryParam) error { // Create a new client client, err := newClient(!params.DisableIPv4, !params.DisableIPv6) if err != nil { return err } defer client.Close() // Set the multicast interface if params.Interface != nil { if err := client.setInterface(params.Interface); err != nil { return err } } // Ensure defaults are set if params.Domain == "" { params.Domain = "local" } if params.Timeout == 0 { params.Timeout = time.Second } // Run the query return client.query(params) } // Lookup is the same as Query, however it uses all the default parameters func Lookup(service string, entries chan<- *ServiceEntry) error { params := DefaultParams(service) params.Entries = entries return Query(params) } // Client provides a query interface that can be used to // search for service providers using mDNS type client struct { use_ipv4 bool use_ipv6 bool ipv4UnicastConn *net.UDPConn ipv6UnicastConn *net.UDPConn ipv4MulticastConn *net.UDPConn ipv6MulticastConn *net.UDPConn closed int32 closedCh chan struct{} // TODO(reddaly): This doesn't appear to be used. } // NewClient creates a new mdns Client that can be used to query // for records func newClient(v4 bool, v6 bool) (*client, error) { if !v4 && !v6 { return nil, fmt.Errorf("Must enable at least one of IPv4 and IPv6 querying") } // TODO(reddaly): At least attempt to bind to the port required in the spec. // Create a IPv4 listener var uconn4 *net.UDPConn var uconn6 *net.UDPConn var mconn4 *net.UDPConn var mconn6 *net.UDPConn var err error if v4 { uconn4, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) if err != nil { log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err) } } if v6 { uconn6, err = net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0}) if err != nil { log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err) } } if uconn4 == nil && uconn6 == nil { return nil, fmt.Errorf("failed to bind to any unicast udp port") } if v4 { mconn4, err = net.ListenMulticastUDP("udp4", nil, ipv4Addr) if err != nil { log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err) } } if v6 { mconn6, err = net.ListenMulticastUDP("udp6", nil, ipv6Addr) if err != nil { log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err) } } if mconn4 == nil && mconn6 == nil { return nil, fmt.Errorf("failed to bind to any multicast udp port") } c := &client{ use_ipv4: v4, use_ipv6: v6, ipv4MulticastConn: mconn4, ipv6MulticastConn: mconn6, ipv4UnicastConn: uconn4, ipv6UnicastConn: uconn6, closedCh: make(chan struct{}), } return c, nil } // Close is used to cleanup the client func (c *client) Close() error { if !atomic.CompareAndSwapInt32(&c.closed, 0, 1) { // something else already closed it return nil } log.Printf("[INFO] mdns: Closing client %v", *c) close(c.closedCh) if c.ipv4UnicastConn != nil { c.ipv4UnicastConn.Close() } if c.ipv6UnicastConn != nil { c.ipv6UnicastConn.Close() } if c.ipv4MulticastConn != nil { c.ipv4MulticastConn.Close() } if c.ipv6MulticastConn != nil { c.ipv6MulticastConn.Close() } return nil } // setInterface is used to set the query interface, uses system // default if not provided func (c *client) setInterface(iface *net.Interface) error { if c.use_ipv4 { p := ipv4.NewPacketConn(c.ipv4UnicastConn) if err := p.SetMulticastInterface(iface); err != nil { return err } p = ipv4.NewPacketConn(c.ipv4MulticastConn) if err := p.SetMulticastInterface(iface); err != nil { return err } } if c.use_ipv6 { p2 := ipv6.NewPacketConn(c.ipv6UnicastConn) if err := p2.SetMulticastInterface(iface); err != nil { return err } p2 = ipv6.NewPacketConn(c.ipv6MulticastConn) if err := p2.SetMulticastInterface(iface); err != nil { return err } } return nil } // query is used to perform a lookup and stream results func (c *client) query(params *QueryParam) error { // Create the service name serviceAddr := fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain)) // Start listening for response packets msgCh := make(chan *dns.Msg, 32) if c.use_ipv4 { go c.recv(c.ipv4UnicastConn, msgCh) go c.recv(c.ipv4MulticastConn, msgCh) } if c.use_ipv6 { go c.recv(c.ipv6UnicastConn, msgCh) go c.recv(c.ipv6MulticastConn, msgCh) } // Send the query m := new(dns.Msg) m.SetQuestion(serviceAddr, dns.TypePTR) // RFC 6762, section 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.) if params.WantUnicastResponse { m.Question[0].Qclass |= 1 << 15 } m.RecursionDesired = false if err := c.sendQuery(m); err != nil { return err } // Map the in-progress responses inprogress := make(map[string]*ServiceEntry) // Listen until we reach the timeout finish := time.After(params.Timeout) for { select { case resp := <-msgCh: var inp *ServiceEntry for _, answer := range append(resp.Answer, resp.Extra...) { // TODO(reddaly): Check that response corresponds to serviceAddr? switch rr := answer.(type) { case *dns.PTR: // Create new entry for this inp = ensureName(inprogress, rr.Ptr) case *dns.SRV: // Check for a target mismatch if rr.Target != rr.Hdr.Name { alias(inprogress, rr.Hdr.Name, rr.Target) } // Get the port inp = ensureName(inprogress, rr.Hdr.Name) inp.Host = rr.Target inp.Port = int(rr.Port) case *dns.TXT: // Pull out the txt inp = ensureName(inprogress, rr.Hdr.Name) inp.Info = strings.Join(rr.Txt, "|") inp.InfoFields = rr.Txt inp.hasTXT = true case *dns.A: // Pull out the IP inp = ensureName(inprogress, rr.Hdr.Name) inp.Addr = rr.A // @Deprecated inp.AddrV4 = rr.A case *dns.AAAA: // Pull out the IP inp = ensureName(inprogress, rr.Hdr.Name) inp.Addr = rr.AAAA // @Deprecated inp.AddrV6 = rr.AAAA } } if inp == nil { continue } // Check if this entry is complete if inp.complete() { if inp.sent { continue } inp.sent = true select { case params.Entries <- inp: default: } } else { // Fire off a node specific query m := new(dns.Msg) m.SetQuestion(inp.Name, dns.TypePTR) m.RecursionDesired = false if err := c.sendQuery(m); err != nil { log.Printf("[ERR] mdns: Failed to query instance %s: %v", inp.Name, err) } } case <-finish: return nil } } } // sendQuery is used to multicast a query out func (c *client) sendQuery(q *dns.Msg) error { buf, err := q.Pack() if err != nil { return err } if c.ipv4UnicastConn != nil { _, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr) if err != nil { return err } } if c.ipv6UnicastConn != nil { _, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr) if err != nil { return err } } return nil } // recv is used to receive until we get a shutdown func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) { if l == nil { return } buf := make([]byte, 65536) for atomic.LoadInt32(&c.closed) == 0 { n, err := l.Read(buf) if atomic.LoadInt32(&c.closed) == 1 { return } if err != nil { log.Printf("[ERR] mdns: Failed to read packet: %v", err) continue } msg := new(dns.Msg) if err := msg.Unpack(buf[:n]); err != nil { log.Printf("[ERR] mdns: Failed to unpack packet: %v", err) continue } select { case msgCh <- msg: case <-c.closedCh: return } } } // ensureName is used to ensure the named node is in progress func ensureName(inprogress map[string]*ServiceEntry, name string) *ServiceEntry { if inp, ok := inprogress[name]; ok { return inp } inp := &ServiceEntry{ Name: name, } inprogress[name] = inp return inp } // alias is used to setup an alias between two entries func alias(inprogress map[string]*ServiceEntry, src, dst string) { srcEntry := ensureName(inprogress, src) inprogress[dst] = srcEntry } mdns-1.0.5/go.mod000066400000000000000000000002121416463777700136050ustar00rootroot00000000000000module github.com/hashicorp/mdns require ( github.com/miekg/dns v1.1.41 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 ) go 1.13 mdns-1.0.5/go.sum000066400000000000000000000032221416463777700136360ustar00rootroot00000000000000github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= mdns-1.0.5/server.go000066400000000000000000000200631416463777700143420ustar00rootroot00000000000000package mdns import ( "fmt" "log" "net" "strings" "sync/atomic" "github.com/miekg/dns" ) const ( ipv4mdns = "224.0.0.251" ipv6mdns = "ff02::fb" mdnsPort = 5353 forceUnicastResponses = false ) var ( ipv4Addr = &net.UDPAddr{ IP: net.ParseIP(ipv4mdns), Port: mdnsPort, } ipv6Addr = &net.UDPAddr{ IP: net.ParseIP(ipv6mdns), Port: mdnsPort, } ) // Config is used to configure the mDNS server type Config struct { // Zone must be provided to support responding to queries Zone Zone // Iface if provided binds the multicast listener to the given // interface. If not provided, the system default multicase interface // is used. Iface *net.Interface // LogEmptyResponses indicates the server should print an informative message // when there is an mDNS query for which the server has no response. LogEmptyResponses bool } // mDNS server is used to listen for mDNS queries and respond if we // have a matching local record type Server struct { config *Config ipv4List *net.UDPConn ipv6List *net.UDPConn shutdown int32 shutdownCh chan struct{} } // NewServer is used to create a new mDNS server from a config func NewServer(config *Config) (*Server, error) { // Create the listeners ipv4List, _ := net.ListenMulticastUDP("udp4", config.Iface, ipv4Addr) ipv6List, _ := net.ListenMulticastUDP("udp6", config.Iface, ipv6Addr) // Check if we have any listener if ipv4List == nil && ipv6List == nil { return nil, fmt.Errorf("No multicast listeners could be started") } s := &Server{ config: config, ipv4List: ipv4List, ipv6List: ipv6List, shutdownCh: make(chan struct{}), } if ipv4List != nil { go s.recv(s.ipv4List) } if ipv6List != nil { go s.recv(s.ipv6List) } return s, nil } // Shutdown is used to shutdown the listener func (s *Server) Shutdown() error { if !atomic.CompareAndSwapInt32(&s.shutdown, 0, 1) { // something else already closed us return nil } close(s.shutdownCh) if s.ipv4List != nil { s.ipv4List.Close() } if s.ipv6List != nil { s.ipv6List.Close() } return nil } // recv is a long running routine to receive packets from an interface func (s *Server) recv(c *net.UDPConn) { if c == nil { return } buf := make([]byte, 65536) for atomic.LoadInt32(&s.shutdown) == 0 { n, from, err := c.ReadFrom(buf) if err != nil { continue } if err := s.parsePacket(buf[:n], from); err != nil { log.Printf("[ERR] mdns: Failed to handle query: %v", err) } } } // parsePacket is used to parse an incoming packet func (s *Server) parsePacket(packet []byte, from net.Addr) error { var msg dns.Msg if err := msg.Unpack(packet); err != nil { log.Printf("[ERR] mdns: Failed to unpack packet: %v", err) return err } return s.handleQuery(&msg, from) } // handleQuery is used to handle an incoming query func (s *Server) handleQuery(query *dns.Msg, from net.Addr) error { if query.Opcode != dns.OpcodeQuery { // "In both multicast query and multicast response messages, the OPCODE MUST // be zero on transmission (only standard queries are currently supported // over multicast). Multicast DNS messages received with an OPCODE other // than zero MUST be silently ignored." Note: OpcodeQuery == 0 return fmt.Errorf("mdns: received query with non-zero Opcode %v: %v", query.Opcode, *query) } if query.Rcode != 0 { // "In both multicast query and multicast response messages, the Response // Code MUST be zero on transmission. Multicast DNS messages received with // non-zero Response Codes MUST be silently ignored." return fmt.Errorf("mdns: received query with non-zero Rcode %v: %v", query.Rcode, *query) } // TODO(reddaly): Handle "TC (Truncated) Bit": // In query messages, if the TC bit is set, it means that additional // Known-Answer records may be following shortly. A responder SHOULD // record this fact, and wait for those additional Known-Answer records, // before deciding whether to respond. If the TC bit is clear, it means // that the querying host has no additional Known Answers. if query.Truncated { return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", *query) } var unicastAnswer, multicastAnswer []dns.RR // Handle each question for _, q := range query.Question { mrecs, urecs := s.handleQuestion(q) multicastAnswer = append(multicastAnswer, mrecs...) unicastAnswer = append(unicastAnswer, urecs...) } // See section 18 of RFC 6762 for rules about DNS headers. resp := func(unicast bool) *dns.Msg { // 18.1: ID (Query Identifier) // 0 for multicast response, query.Id for unicast response id := uint16(0) if unicast { id = query.Id } var answer []dns.RR if unicast { answer = unicastAnswer } else { answer = multicastAnswer } if len(answer) == 0 { return nil } return &dns.Msg{ MsgHdr: dns.MsgHdr{ Id: id, // 18.2: QR (Query/Response) Bit - must be set to 1 in response. Response: true, // 18.3: OPCODE - must be zero in response (OpcodeQuery == 0) Opcode: dns.OpcodeQuery, // 18.4: AA (Authoritative Answer) Bit - must be set to 1 Authoritative: true, // The following fields must all be set to 0: // 18.5: TC (TRUNCATED) Bit // 18.6: RD (Recursion Desired) Bit // 18.7: RA (Recursion Available) Bit // 18.8: Z (Zero) Bit // 18.9: AD (Authentic Data) Bit // 18.10: CD (Checking Disabled) Bit // 18.11: RCODE (Response Code) }, // 18.12 pertains to questions (handled by handleQuestion) // 18.13 pertains to resource records (handled by handleQuestion) // 18.14: Name Compression - responses should be compressed (though see // caveats in the RFC), so set the Compress bit (part of the dns library // API, not part of the DNS packet) to true. Compress: true, Answer: answer, } } if s.config.LogEmptyResponses && len(multicastAnswer) == 0 && len(unicastAnswer) == 0 { questions := make([]string, len(query.Question)) for i, q := range query.Question { questions[i] = q.Name } log.Printf("no responses for query with questions: %s", strings.Join(questions, ", ")) } if mresp := resp(false); mresp != nil { if err := s.sendResponse(mresp, from, false); err != nil { return fmt.Errorf("mdns: error sending multicast response: %v", err) } } if uresp := resp(true); uresp != nil { if err := s.sendResponse(uresp, from, true); err != nil { return fmt.Errorf("mdns: error sending unicast response: %v", err) } } return nil } // handleQuestion is used to handle an incoming question // // The response to a question may be transmitted over multicast, unicast, or // both. The return values are DNS records for each transmission type. func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) { records := s.config.Zone.Records(q) if len(records) == 0 { return nil, nil } // Handle unicast and multicast responses. // TODO(reddaly): The decision about sending over unicast vs. multicast is not // yet fully compliant with RFC 6762. For example, the unicast bit should be // ignored if the records in question are close to TTL expiration. For now, // we just use the unicast bit to make the decision, as per the spec: // RFC 6762, section 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.) if q.Qclass&(1<<15) != 0 || forceUnicastResponses { return nil, records } return records, nil } // sendResponse is used to send a response packet func (s *Server) sendResponse(resp *dns.Msg, from net.Addr, unicast bool) error { // TODO(reddaly): Respect the unicast argument, and allow sending responses // over multicast. buf, err := resp.Pack() if err != nil { return err } // Determine the socket to send from addr := from.(*net.UDPAddr) if addr.IP.To4() != nil { _, err = s.ipv4List.WriteToUDP(buf, addr) return err } else { _, err = s.ipv6List.WriteToUDP(buf, addr) return err } } mdns-1.0.5/server_test.go000066400000000000000000000024521416463777700154030ustar00rootroot00000000000000package mdns import ( "fmt" "testing" "time" ) func TestServer_StartStop(t *testing.T) { s := makeService(t) serv, err := NewServer(&Config{Zone: s}) if err != nil { t.Fatalf("err: %v", err) } defer serv.Shutdown() } func TestServer_Lookup(t *testing.T) { serv, err := NewServer(&Config{Zone: makeServiceWithServiceName(t, "_foobar._tcp")}) if err != nil { t.Fatalf("err: %v", err) } defer serv.Shutdown() entries := make(chan *ServiceEntry, 1) errCh := make(chan error, 1) defer close(errCh) go func() { select { case e := <-entries: if e.Name != "hostname._foobar._tcp.local." { errCh <- fmt.Errorf("Entry has the wrong name: %+v", e) return } if e.Port != 80 { errCh <- fmt.Errorf("Entry has the wrong port: %+v", e) return } if e.Info != "Local web server" { errCh <- fmt.Errorf("Entry as the wrong Info: %+v", e) return } errCh <- nil case <-time.After(80 * time.Millisecond): errCh <- fmt.Errorf("Timed out waiting for response") } }() params := &QueryParam{ Service: "_foobar._tcp", Domain: "local", Timeout: 50 * time.Millisecond, Entries: entries, DisableIPv6: true, } err = Query(params) if err != nil { t.Fatalf("err: %v", err) } err = <-errCh if err != nil { t.Fatalf("err: %v", err) } } mdns-1.0.5/zone.go000066400000000000000000000170431416463777700140130ustar00rootroot00000000000000package mdns import ( "fmt" "net" "os" "strings" "github.com/miekg/dns" ) const ( // defaultTTL is the default TTL value in returned DNS records in seconds. defaultTTL = 120 ) // Zone is the interface used to integrate with the server and // to serve records dynamically type Zone interface { // Records returns DNS records in response to a DNS question. Records(q dns.Question) []dns.RR } // MDNSService is used to export a named service by implementing a Zone type MDNSService struct { Instance string // Instance name (e.g. "hostService name") Service string // Service name (e.g. "_http._tcp.") Domain string // If blank, assumes "local" HostName string // Host machine DNS name (e.g. "mymachine.net.") Port int // Service Port IPs []net.IP // IP addresses for the service's host TXT []string // Service TXT records serviceAddr string // Fully qualified service address instanceAddr string // Fully qualified instance address enumAddr string // _services._dns-sd._udp. } // validateFQDN returns an error if the passed string is not a fully qualified // hdomain name (more specifically, a hostname). func validateFQDN(s string) error { if len(s) == 0 { return fmt.Errorf("FQDN must not be blank") } if s[len(s)-1] != '.' { return fmt.Errorf("FQDN must end in period: %s", s) } // TODO(reddaly): Perform full validation. return nil } // NewMDNSService returns a new instance of MDNSService. // // If domain, hostName, or ips is set to the zero value, then a default value // will be inferred from the operating system. // // TODO(reddaly): This interface may need to change to account for "unique // record" conflict rules of the mDNS protocol. Upon startup, the server should // check to ensure that the instance name does not conflict with other instance // names, and, if required, select a new name. There may also be conflicting // hostName A/AAAA records. func NewMDNSService(instance, service, domain, hostName string, port int, ips []net.IP, txt []string) (*MDNSService, error) { // Sanity check inputs if instance == "" { return nil, fmt.Errorf("missing service instance name") } if service == "" { return nil, fmt.Errorf("missing service name") } if port == 0 { return nil, fmt.Errorf("missing service port") } // Set default domain if domain == "" { domain = "local." } if err := validateFQDN(domain); err != nil { return nil, fmt.Errorf("domain %q is not a fully-qualified domain name: %v", domain, err) } // Get host information if no host is specified. if hostName == "" { var err error hostName, err = os.Hostname() if err != nil { return nil, fmt.Errorf("could not determine host: %v", err) } hostName = fmt.Sprintf("%s.", hostName) } if err := validateFQDN(hostName); err != nil { return nil, fmt.Errorf("hostName %q is not a fully-qualified domain name: %v", hostName, err) } if len(ips) == 0 { var err error ips, err = net.LookupIP(hostName) if err != nil { // Try appending the host domain suffix and lookup again // (required for Linux-based hosts) tmpHostName := fmt.Sprintf("%s%s", hostName, domain) ips, err = net.LookupIP(tmpHostName) if err != nil { return nil, fmt.Errorf("could not determine host IP addresses for %s", hostName) } } } for _, ip := range ips { if ip.To4() == nil && ip.To16() == nil { return nil, fmt.Errorf("invalid IP address in IPs list: %v", ip) } } return &MDNSService{ Instance: instance, Service: service, Domain: domain, HostName: hostName, Port: port, IPs: ips, TXT: txt, serviceAddr: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)), instanceAddr: fmt.Sprintf("%s.%s.%s.", instance, trimDot(service), trimDot(domain)), enumAddr: fmt.Sprintf("_services._dns-sd._udp.%s.", trimDot(domain)), }, nil } // trimDot is used to trim the dots from the start or end of a string func trimDot(s string) string { return strings.Trim(s, ".") } // Records returns DNS records in response to a DNS question. func (m *MDNSService) Records(q dns.Question) []dns.RR { switch q.Name { case m.enumAddr: return m.serviceEnum(q) case m.serviceAddr: return m.serviceRecords(q) case m.instanceAddr: return m.instanceRecords(q) case m.HostName: if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA { return m.instanceRecords(q) } fallthrough default: return nil } } func (m *MDNSService) serviceEnum(q dns.Question) []dns.RR { switch q.Qtype { case dns.TypeANY: fallthrough case dns.TypePTR: rr := &dns.PTR{ Hdr: dns.RR_Header{ Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: defaultTTL, }, Ptr: m.serviceAddr, } return []dns.RR{rr} default: return nil } } // serviceRecords is called when the query matches the service name func (m *MDNSService) serviceRecords(q dns.Question) []dns.RR { switch q.Qtype { case dns.TypeANY: fallthrough case dns.TypePTR: // Build a PTR response for the service rr := &dns.PTR{ Hdr: dns.RR_Header{ Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: defaultTTL, }, Ptr: m.instanceAddr, } servRec := []dns.RR{rr} // Get the instance records instRecs := m.instanceRecords(dns.Question{ Name: m.instanceAddr, Qtype: dns.TypeANY, }) // Return the service record with the instance records return append(servRec, instRecs...) default: return nil } } // serviceRecords is called when the query matches the instance name func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR { switch q.Qtype { case dns.TypeANY: // Get the SRV, which includes A and AAAA recs := m.instanceRecords(dns.Question{ Name: m.instanceAddr, Qtype: dns.TypeSRV, }) // Add the TXT record recs = append(recs, m.instanceRecords(dns.Question{ Name: m.instanceAddr, Qtype: dns.TypeTXT, })...) return recs case dns.TypeA: var rr []dns.RR for _, ip := range m.IPs { if ip4 := ip.To4(); ip4 != nil { rr = append(rr, &dns.A{ Hdr: dns.RR_Header{ Name: m.HostName, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: defaultTTL, }, A: ip4, }) } } return rr case dns.TypeAAAA: var rr []dns.RR for _, ip := range m.IPs { if ip.To4() != nil { // TODO(reddaly): IPv4 addresses could be encoded in IPv6 format and // putinto AAAA records, but the current logic puts ipv4-encodable // addresses into the A records exclusively. Perhaps this should be // configurable? continue } if ip16 := ip.To16(); ip16 != nil { rr = append(rr, &dns.AAAA{ Hdr: dns.RR_Header{ Name: m.HostName, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: defaultTTL, }, AAAA: ip16, }) } } return rr case dns.TypeSRV: // Create the SRV Record srv := &dns.SRV{ Hdr: dns.RR_Header{ Name: q.Name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: defaultTTL, }, Priority: 10, Weight: 1, Port: uint16(m.Port), Target: m.HostName, } recs := []dns.RR{srv} // Add the A record recs = append(recs, m.instanceRecords(dns.Question{ Name: m.instanceAddr, Qtype: dns.TypeA, })...) // Add the AAAA record recs = append(recs, m.instanceRecords(dns.Question{ Name: m.instanceAddr, Qtype: dns.TypeAAAA, })...) return recs case dns.TypeTXT: txt := &dns.TXT{ Hdr: dns.RR_Header{ Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: defaultTTL, }, Txt: m.TXT, } return []dns.RR{txt} } return nil } mdns-1.0.5/zone_test.go000066400000000000000000000147701416463777700150560ustar00rootroot00000000000000package mdns import ( "bytes" "net" "reflect" "testing" "github.com/miekg/dns" ) func makeService(t *testing.T) *MDNSService { return makeServiceWithServiceName(t, "_http._tcp") } func makeServiceWithServiceName(t *testing.T, service string) *MDNSService { m, err := NewMDNSService( "hostname", service, "local.", "testhost.", 80, // port []net.IP{net.IP([]byte{192, 168, 0, 42}), net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc")}, []string{"Local web server"}) // TXT if err != nil { t.Fatalf("err: %v", err) } return m } func TestNewMDNSService_BadParams(t *testing.T) { for _, test := range []struct { testName string hostName string domain string }{ { "NewMDNSService should fail when passed hostName that is not a legal fully-qualified domain name", "hostname", // not legal FQDN - should be "hostname." or "hostname.local.", etc. "local.", // legal }, { "NewMDNSService should fail when passed domain that is not a legal fully-qualified domain name", "hostname.", // legal "local", // should be "local." }, } { _, err := NewMDNSService( "instance name", "_http._tcp", test.domain, test.hostName, 80, // port []net.IP{net.IP([]byte{192, 168, 0, 42})}, []string{"Local web server"}) // TXT if err == nil { t.Fatalf("%s: error expected, but got none", test.testName) } } } func TestMDNSService_BadAddr(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "random", Qtype: dns.TypeANY, } recs := s.Records(q) if len(recs) != 0 { t.Fatalf("bad: %v", recs) } } func TestMDNSService_ServiceAddr(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "_http._tcp.local.", Qtype: dns.TypeANY, } recs := s.Records(q) if got, want := len(recs), 5; got != want { t.Fatalf("got %d records, want %d: %v", got, want, recs) } if ptr, ok := recs[0].(*dns.PTR); !ok { t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) } else if got, want := ptr.Ptr, "hostname._http._tcp.local."; got != want { t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) } if _, ok := recs[1].(*dns.SRV); !ok { t.Errorf("recs[1] should be SRV record, got: %v, all reccords: %v", recs[1], recs) } if _, ok := recs[2].(*dns.A); !ok { t.Errorf("recs[2] should be A record, got: %v, all records: %v", recs[2], recs) } if _, ok := recs[3].(*dns.AAAA); !ok { t.Errorf("recs[3] should be AAAA record, got: %v, all records: %v", recs[3], recs) } if _, ok := recs[4].(*dns.TXT); !ok { t.Errorf("recs[4] should be TXT record, got: %v, all records: %v", recs[4], recs) } q.Qtype = dns.TypePTR if recs2 := s.Records(q); !reflect.DeepEqual(recs, recs2) { t.Fatalf("PTR question should return same result as ANY question: ANY => %v, PTR => %v", recs, recs2) } } func TestMDNSService_InstanceAddr_ANY(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "hostname._http._tcp.local.", Qtype: dns.TypeANY, } recs := s.Records(q) if len(recs) != 4 { t.Fatalf("bad: %v", recs) } if _, ok := recs[0].(*dns.SRV); !ok { t.Fatalf("bad: %v", recs[0]) } if _, ok := recs[1].(*dns.A); !ok { t.Fatalf("bad: %v", recs[1]) } if _, ok := recs[2].(*dns.AAAA); !ok { t.Fatalf("bad: %v", recs[2]) } if _, ok := recs[3].(*dns.TXT); !ok { t.Fatalf("bad: %v", recs[3]) } } func TestMDNSService_InstanceAddr_SRV(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "hostname._http._tcp.local.", Qtype: dns.TypeSRV, } recs := s.Records(q) if len(recs) != 3 { t.Fatalf("bad: %v", recs) } srv, ok := recs[0].(*dns.SRV) if !ok { t.Fatalf("bad: %v", recs[0]) } if _, ok := recs[1].(*dns.A); !ok { t.Fatalf("bad: %v", recs[1]) } if _, ok := recs[2].(*dns.AAAA); !ok { t.Fatalf("bad: %v", recs[2]) } if srv.Port != uint16(s.Port) { t.Fatalf("bad: %v", recs[0]) } } func TestMDNSService_InstanceAddr_A(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "hostname._http._tcp.local.", Qtype: dns.TypeA, } recs := s.Records(q) if len(recs) != 1 { t.Fatalf("bad: %v", recs) } a, ok := recs[0].(*dns.A) if !ok { t.Fatalf("bad: %v", recs[0]) } if !bytes.Equal(a.A, []byte{192, 168, 0, 42}) { t.Fatalf("bad: %v", recs[0]) } } func TestMDNSService_InstanceAddr_AAAA(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "hostname._http._tcp.local.", Qtype: dns.TypeAAAA, } recs := s.Records(q) if len(recs) != 1 { t.Fatalf("bad: %v", recs) } a4, ok := recs[0].(*dns.AAAA) if !ok { t.Fatalf("bad: %v", recs[0]) } ip6 := net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc") if got := len(ip6); got != net.IPv6len { t.Fatalf("test IP failed to parse (len = %d, want %d)", got, net.IPv6len) } if !a4.AAAA.Equal(ip6) { t.Fatalf("bad: %v", recs[0]) } } func TestMDNSService_InstanceAddr_TXT(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "hostname._http._tcp.local.", Qtype: dns.TypeTXT, } recs := s.Records(q) if len(recs) != 1 { t.Fatalf("bad: %v", recs) } txt, ok := recs[0].(*dns.TXT) if !ok { t.Fatalf("bad: %v", recs[0]) } if got, want := txt.Txt, s.TXT; !reflect.DeepEqual(got, want) { t.Fatalf("TXT record mismatch for %v: got %v, want %v", recs[0], got, want) } } func TestMDNSService_HostNameQuery(t *testing.T) { s := makeService(t) for _, test := range []struct { q dns.Question want []dns.RR }{ { dns.Question{Name: "testhost.", Qtype: dns.TypeA}, []dns.RR{&dns.A{ Hdr: dns.RR_Header{ Name: "testhost.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 120, }, A: net.IP([]byte{192, 168, 0, 42}), }}, }, { dns.Question{Name: "testhost.", Qtype: dns.TypeAAAA}, []dns.RR{&dns.AAAA{ Hdr: dns.RR_Header{ Name: "testhost.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 120, }, AAAA: net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc"), }}, }, } { if got := s.Records(test.q); !reflect.DeepEqual(got, test.want) { t.Errorf("hostname query failed: s.Records(%v) = %v, want %v", test.q, got, test.want) } } } func TestMDNSService_serviceEnum_PTR(t *testing.T) { s := makeService(t) q := dns.Question{ Name: "_services._dns-sd._udp.local.", Qtype: dns.TypePTR, } recs := s.Records(q) if len(recs) != 1 { t.Fatalf("bad: %v", recs) } if ptr, ok := recs[0].(*dns.PTR); !ok { t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) } else if got, want := ptr.Ptr, "_http._tcp.local."; got != want { t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) } }