504
util/client.go
Normal file
504
util/client.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"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
|
||||
TTL int
|
||||
Type uint16
|
||||
|
||||
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.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"
|
||||
Type dnsmessage.Type // Lookup type, defaults to dns.TypePTR
|
||||
Interface *net.Interface // Multicast interface to use
|
||||
Entries chan<- *ServiceEntry // Entries Channel
|
||||
WantUnicastResponse bool // Unicast response desired, as per 5.4 in RFC
|
||||
}
|
||||
|
||||
// DefaultParams is used to return a default set of QueryParam's
|
||||
func DefaultParams(service string) *QueryParam {
|
||||
qp := &QueryParam{
|
||||
Service: service,
|
||||
Domain: "local",
|
||||
Entries: make(chan *ServiceEntry),
|
||||
WantUnicastResponse: false,
|
||||
}
|
||||
return qp
|
||||
}
|
||||
|
||||
// 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(ctx context.Context, params *QueryParam) error {
|
||||
// Create a new client
|
||||
client, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Set the multicast interface
|
||||
if params.Interface != nil {
|
||||
if err := client.setInterface(params.Interface, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure defaults are set
|
||||
if params.Domain == "" {
|
||||
params.Domain = "local"
|
||||
}
|
||||
|
||||
// Run the query
|
||||
return client.query(ctx, params)
|
||||
}
|
||||
|
||||
// Listen listens indefinitely for multicast updates
|
||||
func Listen(entries chan<- *ServiceEntry, exit chan struct{}) error {
|
||||
// Create a new client
|
||||
client, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
client.setInterface(nil, true)
|
||||
|
||||
// Start listening for response packets
|
||||
msgCh := make(chan []byte, 32)
|
||||
|
||||
go client.recv(client.ipv4UnicastConn, msgCh)
|
||||
go client.recv(client.ipv6UnicastConn, msgCh)
|
||||
go client.recv(client.ipv4MulticastConn, msgCh)
|
||||
go client.recv(client.ipv6MulticastConn, msgCh)
|
||||
|
||||
sentry := make(map[string]*ServiceEntry)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-exit:
|
||||
return nil
|
||||
case <-client.closedCh:
|
||||
return nil
|
||||
case msg := <-msgCh:
|
||||
fmt.Printf("%#+v\n", msg)
|
||||
entry := messageToEntry(msg, sentry)
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this entry is complete
|
||||
if entry.complete() {
|
||||
if entry.sent {
|
||||
continue
|
||||
}
|
||||
entry.sent = true
|
||||
entries <- entry
|
||||
sentry = make(map[string]*ServiceEntry)
|
||||
} else {
|
||||
// Fire off a node specific query
|
||||
/*
|
||||
h:
|
||||
-&dnsmessage.Header{RecursionDesired: false}
|
||||
m := dnsmessage.NewBuilder()
|
||||
m.SetQuestion(e.Name, dns.TypePTR)
|
||||
if err := client.sendQuery(m); err != nil {
|
||||
logger.Errorf("[ERR] mdns: Failed to query instance %s: %v", e.Name, err)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
// 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 {
|
||||
ipv4UnicastConn *net.UDPConn
|
||||
ipv6UnicastConn *net.UDPConn
|
||||
|
||||
ipv4MulticastConn *net.UDPConn
|
||||
ipv6MulticastConn *net.UDPConn
|
||||
|
||||
closed bool
|
||||
closedCh chan struct{} // TODO(reddaly): This doesn't appear to be used.
|
||||
closeLock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewClient creates a new mdns Client that can be used to query
|
||||
// for records
|
||||
func newClient() (*client, error) {
|
||||
// TODO(reddaly): At least attempt to bind to the port required in the spec.
|
||||
// Create a IPv4 listener
|
||||
uconn4, err4 := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
uconn6, err6 := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
|
||||
if err4 != nil && err6 != nil {
|
||||
logger.Errorf(context.TODO(), "[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6)
|
||||
}
|
||||
|
||||
if uconn4 == nil && uconn6 == nil {
|
||||
return nil, fmt.Errorf("failed to bind to any unicast udp port")
|
||||
}
|
||||
|
||||
if uconn4 == nil {
|
||||
uconn4 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
if uconn6 == nil {
|
||||
uconn6 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
mconn4, err4 := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
|
||||
mconn6, err6 := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
|
||||
if err4 != nil && err6 != nil {
|
||||
logger.Errorf(context.TODO(), "[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6)
|
||||
}
|
||||
|
||||
if mconn4 == nil && mconn6 == nil {
|
||||
return nil, fmt.Errorf("failed to bind to any multicast udp port")
|
||||
}
|
||||
|
||||
if mconn4 == nil {
|
||||
mconn4 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
if mconn6 == nil {
|
||||
mconn6 = &net.UDPConn{}
|
||||
}
|
||||
|
||||
p1 := ipv4.NewPacketConn(mconn4)
|
||||
p2 := ipv6.NewPacketConn(mconn6)
|
||||
p1.SetMulticastLoopback(true)
|
||||
p2.SetMulticastLoopback(true)
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var errCount1, errCount2 int
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
errCount1++
|
||||
}
|
||||
if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
errCount2++
|
||||
}
|
||||
}
|
||||
|
||||
if len(ifaces) == errCount1 && len(ifaces) == errCount2 {
|
||||
return nil, fmt.Errorf("Failed to join multicast group on all interfaces!")
|
||||
}
|
||||
|
||||
c := &client{
|
||||
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 {
|
||||
c.closeLock.Lock()
|
||||
defer c.closeLock.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
|
||||
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 sytem
|
||||
// default if not provided
|
||||
func (c *client) setInterface(iface *net.Interface, loopback bool) error {
|
||||
p := ipv4.NewPacketConn(c.ipv4UnicastConn)
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return err
|
||||
}
|
||||
p2 := ipv6.NewPacketConn(c.ipv6UnicastConn)
|
||||
if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return err
|
||||
}
|
||||
p = ipv4.NewPacketConn(c.ipv4MulticastConn)
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return err
|
||||
}
|
||||
p2 = ipv6.NewPacketConn(c.ipv6MulticastConn)
|
||||
if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if loopback {
|
||||
p.SetMulticastLoopback(true)
|
||||
p2.SetMulticastLoopback(true)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// query is used to perform a lookup and stream results
|
||||
func (c *client) query(ctx context.Context, 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 []byte, 32)
|
||||
go c.recv(c.ipv4UnicastConn, msgCh)
|
||||
go c.recv(c.ipv6UnicastConn, msgCh)
|
||||
go c.recv(c.ipv4MulticastConn, msgCh)
|
||||
go c.recv(c.ipv6MulticastConn, msgCh)
|
||||
|
||||
// buf := make([]byte, 2, 514)
|
||||
hdr := dnsmessage.Header{RecursionDesired: false}
|
||||
b := dnsmessage.NewBuilder(nil, hdr)
|
||||
//b.EnableCompression()
|
||||
name, err := dnsmessage.NewName(serviceAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := dnsmessage.Question{Name: name, Class: dnsmessage.ClassINET}
|
||||
if params.Type == 0 {
|
||||
q.Type = dnsmessage.TypePTR
|
||||
} else {
|
||||
q.Type = params.Type
|
||||
}
|
||||
//q.Class |= 1 << 15
|
||||
if err = b.StartQuestions(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = b.Question(q); err != nil {
|
||||
return err
|
||||
}
|
||||
bbuf, err := b.Finish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the query
|
||||
// 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 err := c.sendQuery(bbuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Map the in-progress responses
|
||||
inprogress := make(map[string]*ServiceEntry)
|
||||
|
||||
for {
|
||||
select {
|
||||
case rsp := <-msgCh:
|
||||
inp := messageToEntry(rsp, inprogress)
|
||||
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:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Fire off a node specific query
|
||||
// m := new(dns.Msg)
|
||||
// m.SetQuestion(inp.Name, inp.Type)
|
||||
// m.RecursionDesired = false
|
||||
var buf []byte
|
||||
if err := c.sendQuery(buf); err != nil {
|
||||
logger.Errorf(context.TODO(), "[ERR] mdns: Failed to query instance %s: %v", inp.Name, err)
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sendQuery is used to multicast a query out
|
||||
func (c *client) sendQuery(buf []byte) error {
|
||||
if c.ipv4UnicastConn != nil {
|
||||
c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr)
|
||||
}
|
||||
if c.ipv6UnicastConn != nil {
|
||||
c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recv is used to receive until we get a shutdown
|
||||
func (c *client) recv(l *net.UDPConn, msgCh chan []byte) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 65536)
|
||||
for {
|
||||
select {
|
||||
case <-c.closedCh:
|
||||
return
|
||||
default:
|
||||
c.closeLock.Lock()
|
||||
if c.closed {
|
||||
c.closeLock.Unlock()
|
||||
return
|
||||
}
|
||||
c.closeLock.Unlock()
|
||||
n, err := l.Read(buf)
|
||||
if err != nil {
|
||||
if logger.V(logger.DebugLevel) {
|
||||
logger.Debug(context.TODO(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
msgCh <- buf[:n]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// ensureName is used to ensure the named node is in progress
|
||||
func ensureName(inprogress map[string]*ServiceEntry, name string, typ uint16) *ServiceEntry {
|
||||
if inp, ok := inprogress[name]; ok {
|
||||
return inp
|
||||
}
|
||||
inp := &ServiceEntry{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
}
|
||||
inprogress[name] = inp
|
||||
return inp
|
||||
}
|
||||
|
||||
// alias is used to setup an alias between two entries
|
||||
func alias(inprogress map[string]*ServiceEntry, src, dst string, typ uint16) {
|
||||
srcEntry := ensureName(inprogress, src, typ)
|
||||
inprogress[dst] = srcEntry
|
||||
}
|
||||
*/
|
||||
func messageToEntry(m []byte, inprogress map[string]*ServiceEntry) *ServiceEntry {
|
||||
var inp *ServiceEntry
|
||||
/*
|
||||
for _, answer := range append(m.Answers, m.Additionals...) {
|
||||
// TODO(reddaly): Check that response corresponds to serviceAddr?
|
||||
switch answer.Header.Type {
|
||||
case dnsmessage.TypePTR:
|
||||
rr := answer.Body.(*dnsmessage.PTRResource)
|
||||
// Create new entry for this
|
||||
inp = ensureName(inprogress, rr.Ptr, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
case dnsmessage.TypeSRV:
|
||||
// Check for a target mismatch
|
||||
if rr.Target != rr.Hdr.Name {
|
||||
alias(inprogress, rr.Hdr.Name, rr.Target, rr.Hdr.Rrtype)
|
||||
}
|
||||
|
||||
// Get the port
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Host = rr.Target
|
||||
inp.Port = int(rr.Port)
|
||||
case dnsmessage.TypeTXT:
|
||||
// Pull out the txt
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.Info = strings.Join(rr.Txt, "|")
|
||||
inp.InfoFields = rr.Txt
|
||||
inp.hasTXT = true
|
||||
case dnsmessage.TypeA:
|
||||
// Pull out the IP
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.AddrV4 = rr.A
|
||||
case dnsmessage.TypeAAAA:
|
||||
// Pull out the IP
|
||||
inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype)
|
||||
if inp.complete() {
|
||||
continue
|
||||
}
|
||||
inp.AddrV6 = rr.AAAA
|
||||
}
|
||||
|
||||
if inp != nil {
|
||||
inp.TTL = int(answer.Header().Ttl)
|
||||
}
|
||||
}
|
||||
*/
|
||||
return inp
|
||||
}
|
84
util/dns_sd.go
Normal file
84
util/dns_sd.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package mdns
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// DNSSDService is a service that complies with the DNS-SD (RFC 6762) and MDNS
|
||||
// (RFC 6762) specs for local, multicast-DNS-based discovery.
|
||||
//
|
||||
// DNSSDService implements the Zone interface and wraps an MDNSService instance.
|
||||
// To deploy an mDNS service that is compliant with DNS-SD, it's recommended to
|
||||
// register only the wrapped instance with the server.
|
||||
//
|
||||
// Example usage:
|
||||
// service := &mdns.DNSSDService{
|
||||
// MDNSService: &mdns.MDNSService{
|
||||
// Instance: "My Foobar Service",
|
||||
// Service: "_foobar._tcp",
|
||||
// Port: 8000,
|
||||
// }
|
||||
// }
|
||||
// server, err := mdns.NewServer(&mdns.Config{Zone: service})
|
||||
// if err != nil {
|
||||
// log.Fatalf("Error creating server: %v", err)
|
||||
// }
|
||||
// defer server.Shutdown()
|
||||
type DNSSDService struct {
|
||||
MDNSService *MDNSService
|
||||
}
|
||||
|
||||
// Records returns DNS records in response to a DNS question.
|
||||
//
|
||||
// This function returns the DNS response of the underlying MDNSService
|
||||
// instance. It also returns a PTR record for a request for "
|
||||
// _services._dns-sd._udp.<Domain>", as described in section 9 of RFC 6763
|
||||
// ("Service Type Enumeration"), to allow browsing of the underlying MDNSService
|
||||
// instance.
|
||||
func (s *DNSSDService) Records(q dns.Question) []dns.RR {
|
||||
var recs []dns.RR
|
||||
if q.Name == "_services._dns-sd._udp."+s.MDNSService.Domain+"." {
|
||||
recs = s.dnssdMetaQueryRecords(q)
|
||||
}
|
||||
return append(recs, s.MDNSService.Records(q)...)
|
||||
}
|
||||
|
||||
// dnssdMetaQueryRecords returns the DNS records in response to a "meta-query"
|
||||
// issued to browse for DNS-SD services, as per section 9. of RFC6763.
|
||||
//
|
||||
// A meta-query has a name of the form "_services._dns-sd._udp.<Domain>" where
|
||||
// Domain is a fully-qualified domain, such as "local."
|
||||
func (s *DNSSDService) dnssdMetaQueryRecords(q dns.Question) []dns.RR {
|
||||
// Intended behavior, as described in the RFC:
|
||||
// ...it may be useful for network administrators to find the list of
|
||||
// advertised service types on the network, even if those Service Names
|
||||
// are just opaque identifiers and not particularly informative in
|
||||
// isolation.
|
||||
//
|
||||
// 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-abel
|
||||
// <Service> name, plus the same domain, e.g., "_http._tcp.<Domain>".
|
||||
// Including the domain in the PTR rdata allows for slightly better name
|
||||
// compression in Unicast DNS responses, but only the first two labels are
|
||||
// relevant for the purposes of service type enumeration. These two-label
|
||||
// service types can then be used to construct subsequent Service Instance
|
||||
// Enumeration PTR queries, in this <Domain> or others, to discover
|
||||
// instances of that service type.
|
||||
return []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Ptr: s.MDNSService.serviceAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Announcement returns DNS records that should be broadcast during the initial
|
||||
// availability of the service, as described in section 8.3 of RFC 6762.
|
||||
// TODO(reddaly): Add this when Announcement is added to the mdns.Zone interface.
|
||||
//func (s *DNSSDService) Announcement() []dns.RR {
|
||||
// return s.MDNSService.Announcement()
|
||||
//}
|
71
util/dns_sd_test.go
Normal file
71
util/dns_sd_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// +build ignore
|
||||
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type mockMDNSService struct{}
|
||||
|
||||
func (s *mockMDNSService) Records(q dns.Question) []dns.RR {
|
||||
return []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "fakerecord",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 42,
|
||||
},
|
||||
Ptr: "fake.local.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mockMDNSService) Announcement() []dns.RR {
|
||||
return []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "fakeannounce",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 42,
|
||||
},
|
||||
Ptr: "fake.local.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSSDServiceRecords(t *testing.T) {
|
||||
s := &DNSSDService{
|
||||
MDNSService: &MDNSService{
|
||||
serviceAddr: "_foobar._tcp.local.",
|
||||
Domain: "local",
|
||||
},
|
||||
}
|
||||
q := dns.Question{
|
||||
Name: "_services._dns-sd._udp.local.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
recs := s.Records(q)
|
||||
if got, want := len(recs), 1; got != want {
|
||||
t.Fatalf("s.Records(%v) returned %v records, want %v", q, got, want)
|
||||
}
|
||||
|
||||
want := dns.RR(&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "_services._dns-sd._udp.local.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Ptr: "_foobar._tcp.local.",
|
||||
})
|
||||
if got := recs[0]; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("s.Records()[0] = %v, want %v", got, want)
|
||||
}
|
||||
}
|
546
util/server.go
Normal file
546
util/server.go
Normal file
@@ -0,0 +1,546 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
registry "github.com/unistack-org/micro/v3/register"
|
||||
regutil "github.com/unistack-org/micro/v3/util/register"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
var (
|
||||
mdnsGroupIPv4 = net.ParseIP("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::"),
|
||||
Port: 5353,
|
||||
}
|
||||
|
||||
// mDNS endpoint addresses
|
||||
ipv4Addr = &net.UDPAddr{
|
||||
IP: mdnsGroupIPv4,
|
||||
Port: 5353,
|
||||
}
|
||||
ipv6Addr = &net.UDPAddr{
|
||||
IP: mdnsGroupIPv6,
|
||||
Port: 5353,
|
||||
}
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Port If it is not 0, replace the port 5353 with this port number.
|
||||
Port int
|
||||
}
|
||||
|
||||
// mDNS server is used to listen for mDNS queries and respond if we
|
||||
// have a matching local record
|
||||
type Server struct {
|
||||
config *Config
|
||||
|
||||
ipv4conn *net.UDPConn
|
||||
ipv6conn *net.UDPConn
|
||||
|
||||
shutdown bool
|
||||
shutdownCh chan struct{}
|
||||
shutdownLock sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
|
||||
updates chan *registry.Service
|
||||
services map[string][]*registry.Service
|
||||
records map[string][]dnsmessage.Resource
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewServer is used to create a new mDNS server from a config
|
||||
func NewServer(config *Config) (*Server, error) {
|
||||
setCustomPort(config.Port)
|
||||
|
||||
// Create the listeners
|
||||
// Create wildcard connections (because :5353 can be already taken by other apps)
|
||||
ipv4conn, _ := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
|
||||
ipv6conn, _ := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
|
||||
if ipv4conn == nil && ipv6conn == nil {
|
||||
return nil, fmt.Errorf("[ERR] mdns: Failed to bind to any udp port!")
|
||||
}
|
||||
|
||||
if ipv4conn == nil {
|
||||
ipv4conn = &net.UDPConn{}
|
||||
}
|
||||
if ipv6conn == nil {
|
||||
ipv6conn = &net.UDPConn{}
|
||||
}
|
||||
|
||||
// Join multicast groups to receive announcements
|
||||
p4 := ipv4.NewPacketConn(ipv4conn)
|
||||
p6 := ipv6.NewPacketConn(ipv6conn)
|
||||
p4.SetMulticastLoopback(true)
|
||||
p6.SetMulticastLoopback(true)
|
||||
|
||||
if config.Iface != nil {
|
||||
if err := p4.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p6.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errCount1, errCount2 := 0, 0
|
||||
for _, iface := range ifaces {
|
||||
if err := p4.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
|
||||
errCount1++
|
||||
}
|
||||
if err := p6.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
|
||||
errCount2++
|
||||
}
|
||||
}
|
||||
if len(ifaces) == errCount1 && len(ifaces) == errCount2 {
|
||||
return nil, fmt.Errorf("Failed to join multicast group on all interfaces!")
|
||||
}
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
config: config,
|
||||
ipv4conn: ipv4conn,
|
||||
ipv6conn: ipv6conn,
|
||||
shutdownCh: make(chan struct{}),
|
||||
records: make(map[string][]dnsmessage.Resource),
|
||||
services: make(map[string][]*registry.Service),
|
||||
updates: make(chan *registry.Service),
|
||||
}
|
||||
|
||||
go s.recv(s.ipv4conn)
|
||||
go s.recv(s.ipv6conn)
|
||||
|
||||
go s.update()
|
||||
//s.wg.Add(1)
|
||||
//go s.probe()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) update() {
|
||||
var err error
|
||||
var buf []byte
|
||||
for svc := range s.updates {
|
||||
fmt.Printf("update %#+v\n", svc)
|
||||
if err = s.serviceToPacket(svc, buf); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
} else {
|
||||
if s.sendResponse(buf, mdnsWildcardAddrIPv4); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown is used to shutdown the listener
|
||||
func (s *Server) Shutdown() error {
|
||||
s.shutdownLock.Lock()
|
||||
defer s.shutdownLock.Unlock()
|
||||
|
||||
if s.shutdown {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.shutdown = true
|
||||
close(s.shutdownCh)
|
||||
s.unregister()
|
||||
|
||||
if s.ipv4conn != nil {
|
||||
s.ipv4conn.Close()
|
||||
}
|
||||
if s.ipv6conn != nil {
|
||||
s.ipv6conn.Close()
|
||||
}
|
||||
|
||||
// s.wg.Wait()
|
||||
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 {
|
||||
s.shutdownLock.Lock()
|
||||
if s.shutdown {
|
||||
s.shutdownLock.Unlock()
|
||||
return
|
||||
}
|
||||
s.shutdownLock.Unlock()
|
||||
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(buf []byte, from net.Addr) error {
|
||||
var p dnsmessage.Parser
|
||||
hdr, err := p.Start(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.handleQuery(hdr, p, from)
|
||||
}
|
||||
|
||||
func (s *Server) LookupService(name string, opts ...registry.LookupOption) ([]*registry.Service, error) {
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Server) Register(service *registry.Service, opts ...registry.RegisterOption) error {
|
||||
name := service.Name + ".micro."
|
||||
s.Lock()
|
||||
svcs, ok := s.services[name]
|
||||
if !ok {
|
||||
s.services[name] = []*registry.Service{service}
|
||||
} else {
|
||||
s.services[name] = regutil.Merge([]*registry.Service{service}, svcs)
|
||||
}
|
||||
s.Unlock()
|
||||
s.updates <- service
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) serviceToPacket(svc *registry.Service, buf []byte) error {
|
||||
var err error
|
||||
var name dnsmessage.Name
|
||||
|
||||
b := dnsmessage.NewBuilder(buf, dnsmessage.Header{})
|
||||
b.EnableCompression()
|
||||
if err = b.StartAnswers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if name, err = dnsmessage.NewName(svc.Name + ".micro."); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = b.AResource(dnsmessage.ResourceHeader{Name: name, Class: dnsmessage.ClassINET, TTL: 60},
|
||||
dnsmessage.AResource{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err = b.Finish()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// handleQuery is used to handle an incoming query
|
||||
func (s *Server) handleQuery(hdr dnsmessage.Header, p dnsmessage.Parser, from net.Addr) error {
|
||||
if hdr.OpCode != 0 {
|
||||
// "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", hdr.OpCode, hdr)
|
||||
}
|
||||
if hdr.RCode != dnsmessage.RCodeSuccess {
|
||||
// "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", hdr.RCode, hdr)
|
||||
}
|
||||
|
||||
// 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 hdr.Truncated {
|
||||
return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", hdr)
|
||||
}
|
||||
|
||||
questions, err := p.AllQuestions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var unicastAnswer, multicastAnswer []dnsmessage.Resource
|
||||
|
||||
for _, question := range questions {
|
||||
mrecs, urecs := s.handleQuestion(question)
|
||||
fmt.Printf("%#+v %#+v\n", mrecs, urecs)
|
||||
multicastAnswer = append(multicastAnswer, mrecs...)
|
||||
unicastAnswer = append(unicastAnswer, urecs...)
|
||||
}
|
||||
|
||||
rsp := func(unistact bool, buf []byte) error {
|
||||
// See section 18 of RFC 6762 for rules about DNS headers.
|
||||
// 18.1: ID (Query Identifier)
|
||||
// 0 for multicast response, query.Id for unicast response
|
||||
id := uint16(0)
|
||||
if true /*unicast*/ {
|
||||
id = hdr.ID
|
||||
}
|
||||
|
||||
hdrnew := dnsmessage.Header{
|
||||
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: 0,
|
||||
// 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)
|
||||
}
|
||||
|
||||
b := dnsmessage.NewBuilder(buf, hdrnew)
|
||||
b.EnableCompression()
|
||||
|
||||
buf, err = b.Finish()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var buf1 []byte
|
||||
if err = rsp(false, buf1); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(buf1) > 0 {
|
||||
if err := s.sendResponse(buf1, from); err != nil {
|
||||
return fmt.Errorf("mdns: error sending multicast response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var buf2 []byte
|
||||
if err = rsp(true, buf2); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(buf2) > 0 {
|
||||
if err := s.sendResponse(buf2, from); 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 dnsmessage.Question) (multicastRecs, unicastRecs []dnsmessage.Resource) {
|
||||
records, ok := s.records[q.Name.String()]
|
||||
|
||||
// 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 ok {
|
||||
if q.Class&(1<<15) != 0 {
|
||||
return nil, records
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
services, ok := s.services[q.Name.String()]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", q.Name.String())
|
||||
fmt.Printf("%#+v\n", services)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (s *Server) probe() {
|
||||
defer s.wg.Done()
|
||||
|
||||
sd, ok := s.config.Zone.(*MDNSService)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain))
|
||||
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion(name, dns.TypePTR)
|
||||
q.RecursionDesired = false
|
||||
|
||||
srv := &dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Priority: 0,
|
||||
Weight: 0,
|
||||
Port: uint16(sd.Port),
|
||||
Target: sd.HostName,
|
||||
}
|
||||
txt := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: defaultTTL,
|
||||
},
|
||||
Txt: sd.TXT,
|
||||
}
|
||||
q.Ns = []dns.RR{srv, txt}
|
||||
|
||||
randomizer := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.SendMulticast(q); err != nil {
|
||||
log.Println("[ERR] mdns: failed to send probe:", err.Error())
|
||||
}
|
||||
time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond)
|
||||
}
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.MsgHdr.Response = true
|
||||
|
||||
// set for query
|
||||
q.SetQuestion(name, dns.TypeANY)
|
||||
|
||||
resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...)
|
||||
|
||||
// reset
|
||||
q.SetQuestion(name, dns.TypePTR)
|
||||
|
||||
// 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
|
||||
timer := time.NewTimer(timeout)
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.SendMulticast(resp); err != nil {
|
||||
log.Println("[ERR] mdns: failed to send announcement:", err.Error())
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
timeout *= 2
|
||||
timer.Reset(timeout)
|
||||
case <-s.shutdownCh:
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// multicastResponse us used to send a multicast response packet
|
||||
func (s *Server) SendMulticast(msg *dns.Msg) error {
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.ipv4conn != nil {
|
||||
s.ipv4conn.WriteToUDP(buf, ipv4Addr)
|
||||
}
|
||||
if s.ipv6conn != nil {
|
||||
s.ipv6conn.WriteToUDP(buf, ipv6Addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendResponse is used to send a response packet
|
||||
func (s *Server) sendResponse(buf []byte, from net.Addr) error {
|
||||
var err error
|
||||
|
||||
// TODO(reddaly): Respect the unicast argument, and allow sending responses
|
||||
// over multicast.
|
||||
|
||||
// Determine the socket to send from
|
||||
addr := from.(*net.UDPAddr)
|
||||
if addr.IP.To4() != nil {
|
||||
_, err = s.ipv4conn.WriteToUDP(buf, addr)
|
||||
} else {
|
||||
_, err = s.ipv6conn.WriteToUDP(buf, addr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) unregister() error {
|
||||
sd, ok := s.config.Zone.(*MDNSService)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&sd.TTL, 0)
|
||||
name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain))
|
||||
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion(name, dns.TypeANY)
|
||||
|
||||
resp := new(dns.Msg)
|
||||
resp.MsgHdr.Response = true
|
||||
resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...)
|
||||
|
||||
return s.SendMulticast(resp)
|
||||
}
|
||||
|
||||
func setCustomPort(port int) {
|
||||
if port != 0 {
|
||||
if mdnsWildcardAddrIPv4.Port != port {
|
||||
mdnsWildcardAddrIPv4.Port = port
|
||||
}
|
||||
if mdnsWildcardAddrIPv6.Port != port {
|
||||
mdnsWildcardAddrIPv6.Port = port
|
||||
}
|
||||
if ipv4Addr.Port != port {
|
||||
ipv4Addr.Port = port
|
||||
}
|
||||
if ipv6Addr.Port != port {
|
||||
ipv6Addr.Port = port
|
||||
}
|
||||
}
|
||||
}
|
65
util/server_test.go
Normal file
65
util/server_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// +build ignore
|
||||
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
registry "github.com/unistack-org/micro/v3/register"
|
||||
)
|
||||
|
||||
var (
|
||||
svc1 = ®istry.Service{
|
||||
Name: "foo",
|
||||
Version: "latest",
|
||||
Nodes: []*registry.Node{
|
||||
®istry.Node{
|
||||
Id: "1",
|
||||
Address: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestServer_StartStop(t *testing.T) {
|
||||
//s := makeService(t)
|
||||
srv, err := NewServer(&Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err = srv.Shutdown(); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_Lookup(t *testing.T) {
|
||||
srv1, err := NewServer(&Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer srv1.Shutdown()
|
||||
/*
|
||||
srv2, err := NewServer(&Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer srv2.Shutdown()
|
||||
*/
|
||||
if err = srv1.Register(svc1); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
/*
|
||||
select {}
|
||||
services, err := srv2.GetService("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
} else if len(services) == 0 {
|
||||
t.Fatalf("empty service")
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
fmt.Printf("%#+v\n", svc)
|
||||
}
|
||||
*/
|
||||
select {}
|
||||
}
|
309
util/zone.go
Normal file
309
util/zone.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package mdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"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
|
||||
TTL uint32
|
||||
serviceAddr string // Fully qualified service address
|
||||
instanceAddr string // Fully qualified instance address
|
||||
enumAddr string // _services._dns-sd._udp.<domain>
|
||||
}
|
||||
|
||||
// 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(trimDot(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(trimDot(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,
|
||||
TTL: defaultTTL,
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
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: atomic.LoadUint32(&m.TTL),
|
||||
},
|
||||
Txt: m.TXT,
|
||||
}
|
||||
return []dns.RR{txt}
|
||||
}
|
||||
return nil
|
||||
}
|
277
util/zone_test.go
Normal file
277
util/zone_test.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// +build ignore
|
||||
|
||||
package 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 !bytes.Equal(a4.AAAA, 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user