├── .gitignore ├── .travis.yml ├── DETAILS ├── LICENSE ├── README.md ├── browse.go ├── browser_test.go ├── cache.go ├── cmd ├── bct │ ├── BonjourConformanceTest │ ├── ConformanceTestResults │ ├── README.md │ └── main.go ├── browse │ └── main.go ├── debug │ └── main.go ├── dnssd │ └── dnssd.go ├── filter-ifaces │ └── main.go ├── register │ └── main.go └── resolve │ └── main.go ├── dns.go ├── go.mod ├── go.sum ├── log └── log.go ├── mdns.go ├── netlink_linux.go ├── netlink_others.go ├── probe.go ├── probe_test.go ├── resolve.go ├── responder.go ├── responder_debug.go ├── responder_test.go ├── service.go ├── serviceHandle.go └── service_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.11.x 5 | - master 6 | os: 7 | - linux 8 | - osx 9 | dist: trusty 10 | install: true 11 | script: 12 | - env GO111MODULE=on go build 13 | - env GO111MODULE=on go test -------------------------------------------------------------------------------- /DETAILS: -------------------------------------------------------------------------------- 1 | # Multicast DNS 2 | 3 | - Every DNS resource record has a TTL, which is the number of seconds for which the resource record may be cached. 4 | - mDNS allows host names of form `.local` 5 | - The domain "local" is a link-local Multicast DNS domain. 6 | > This means that names within that domain are only meaningful on the link they originate. Any DNS query for a name ending with ".local" must be sent to the mDNS IPv4 link-local mutlicast adress "224.0.0.251" (or "FF02::FB" for IPv6) 7 | > […] 8 | > DNS queries for names not ending "local." may be sent to the mDNS multicast address. 9 | > […] 10 | > Any DNS query for a name ending with "254.169.in-addr.arpa." MUST be sent to the mDNS IPv4 link-local multicast address 224.0.0.251 or the mDNS IPv6 multicast address FF02::FB. 11 | > Likewise, any DNS query for a name within the reverse mapping domains for IPv6 link-local addresses ("8.e.f.ip6.arpa.", "9.e.f.ip6.arpa.", "a.e.f.ip6.arpa.", and "b.e.f.ip6.arpa.") MUST be sent to the mDNS IPv6 link-local multicast address FF02::FB or the mDNS IPv4 link-local multicast address 224.0.0.251. 12 | [rfc6762][mdns] 13 | - Probing 14 | 15 | # DNS-SD 16 | 17 | DNS-SD specifies how services services can be described and found using standard DNS records. 18 | 19 | - Service instance names must not contain any ASCII control characters (0x00-0x1F and 0x7F), it can contain spaces or any other Net-Unicode (what's that?). The label is limited to 63 bytes. 20 | 21 | - If a service instance name is rejected by the DNS server, we should retry the query using the "Punnycode" algorithm. 22 | 23 | - The characters of a service instance name, consisting of ``, ``, and ``, should be escaped to enuse DNS label boundaries. 24 | - Dots in should be escaped, like "." becomes "\." 25 | - Backslashes in should be escaped, like "\" becomes "\\" 26 | 27 | - If more than one SRV records are returned when searching for a particular service instance, we must interpret the priority and weight fields of the SRV record. But it's common that those fields are set to zero. 28 | 29 | - TXT record 30 | - Every DNS-SD SRV record must have a TXT record, with the same name containing key-value pairs (=) or a single zero byte. 31 | - TXT record strings starting with an "=" character or having no "=" character are ignored 32 | - key must be at least one character, no more than 9 characters longs, printable US_ASCII values (0x20-0x7E), cases are ignored, must be unique (only use the first) 33 | - Examples 34 | - "": key is not present 35 | - "myKey": key present, with no value 36 | - "myKey=": key present, with empty value 37 | - "myKey=myValue": key present, with no empty value 38 | - value 39 | - must not be enclosed with quotation mark 40 | - is binary data (doesn't matter if US-ASCII oder UTF-8), display as hex alongside (UTF-8) 41 | - 42 | - When using mDNS, TXT records can be up to 8900 bytes long, because the maximum packet size is 9000 bytes. DNS-SD recommends the following TXT record sizes. 43 | - < 200 bytes 44 | - < 400 bytes, to fit into a single 512-byte DNS message 45 | - < 1300 bytes, to fit into a single 1500-byte Eterhnet packet 46 | - > 1300 is not recommended 47 | - (Sidenote: Hardware can offer mDNS offloading, only if TXT records are not larger than 256 bytes.) 48 | - If there is a need to indicate the application protocol version, use the key "protovers". It's just a recommendation though from RFC6763 though. 49 | - Service name: <1st label>.<2nd label> 50 | - 1st label: Name of the service starting with an underscore "_" 51 | - 2nd label: Transport protocol, "_tcp" for TCP based transport protocols, otherwise "_udp" for all other transport protocols (which is weird) 52 | - Must not be empty, and shouldn't be longer than 15 characters (without the mandatory underscore) 53 | - One ore more letter, and digits, and no consecutive hyphens (--) 54 | - RFC6763 Section 9 defines a meta-query for problem diagnostics and network management (not needed yet) 55 | - RFC6763 Section 10: A mDNS client should answer mDNS queries for its PTR, SRV and TXT names ending with "local.". 56 | - RFC6763 Section 12: Additional records can be placed in the addition section of a DNS message. It's recommended to improve network efficiency (TODO later) 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Matthias Hochgatterer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNS-SD 2 | 3 | [![Build Status](https://travis-ci.org/brutella/hc.svg)](https://travis-ci.org/brutella/dnssd) 4 | 5 | This library implements [Multicast DNS][mdns] and [DNS-Based Service Discovery][dnssd] to provide zero-configuration operations. It lets you announce and find services in a specific link-local domain. 6 | 7 | [mdns]: https://tools.ietf.org/html/rfc6762 8 | [dnssd]: https://tools.ietf.org/html/rfc6763 9 | 10 | ## Usage 11 | 12 | #### Create a mDNS responder 13 | 14 | The following code creates a service with name "My Website._http._tcp.local." for the host "My Computer" which has all IPs from network interface "eth0". The service is added to a responder. 15 | 16 | ```go 17 | import ( 18 | "context" 19 | "github.com/brutella/dnssd" 20 | ) 21 | 22 | cfg := dnssd.Config{ 23 | Name: "My Website", 24 | Type: "_http._tcp", 25 | Domain: "local", 26 | Host: "My Computer", 27 | Ifaces: []string{"eth0"},, 28 | Port: 12345, 29 | } 30 | sv, _ := dnssd.NewService(cfg) 31 | ``` 32 | 33 | In most cases you only need to specify the name, type and port of the service. 34 | 35 | ```go 36 | cfg := dnssd.Config{ 37 | Name: "My Website", 38 | Type: "_http._tcp", 39 | Port: 12345, 40 | } 41 | sv, _ := dnssd.NewService(cfg) 42 | ``` 43 | 44 | Then you create a responder and add the service to it. 45 | ```go 46 | rp, _ := dnssd.NewResponder() 47 | hdl, _ := rp.Add(sv) 48 | 49 | ctx, cancel := context.WithCancel(context.Background()) 50 | defer cancel() 51 | 52 | rp.Respond(ctx) 53 | ``` 54 | 55 | When calling `Respond` the responder probes for the service instance name and host name to be unqiue in the network. 56 | Once probing is finished, the service will be announced. 57 | 58 | #### Update TXT records 59 | 60 | Once a service is added to a responder, you can use the `hdl` to update properties. 61 | 62 | ```go 63 | hdl.UpdateText(map[string]string{"key1": "value1", "key2": "value2"}, rsp) 64 | ``` 65 | 66 | ## `dnssd` command 67 | 68 | The command line tool in `cmd/dnssd` lets you browse, register and resolve services similar to [dns-sd](https://www.unix.com/man-page/osx/1/dns-sd/). 69 | 70 | ### Install 71 | You can install the tool with 72 | 73 | `go install github.com/brutella/dnssd/cmd/dnssd` 74 | 75 | ### Usage 76 | 77 | **Registering a service on your local machine** 78 | 79 | Lets register a printer service (`_printer._tcp`) running on your local computer at port 515 with the name "Private Printer". 80 | 81 | ```sh 82 | dnssd register -Name="Private Printer" -Type="_printer._tcp" -Port=515 83 | ``` 84 | 85 | **Registering a proxy service** 86 | 87 | If the service is running on a different machine on your local network, you have to specify the hostname and IP. 88 | Lets say the printer service is running on the printer with the hostname `ABCD` and IPv4 address `192.168.1.53`, you can register a proxy which announce that service on your network. 89 | 90 | ```sh 91 | dnssd register -Name="Private Printer" -Type="_printer._tcp" -Port=515 -IP=192.168.1.53 -Host=ABCD 92 | ``` 93 | 94 | Use option `-Interface`, if you want to announce the service only on a specific network interface. 95 | This might be necessary if your local machine is connected to multiple subnets and your announced service is only available on a specific subnet. 96 | 97 | ```sh 98 | dnssd register -Name="Private Printer" -Type="_printer._tcp" -Port=515 -IP=192.168.1.53 -Host=ABCD -Interface=en0 99 | ``` 100 | 101 | **Browsing for a service** 102 | 103 | If you want to browse for a service type, you can use the `browse` command. 104 | 105 | ```sh 106 | dnssd browse -Type="_printer._tcp" 107 | ``` 108 | 109 | **Resolving a service instance** 110 | 111 | If you know the name of a service instance, you can resolve its hostname with the `resolve` command. 112 | 113 | ```sh 114 | dnssd resolve -Name="Private Printer" -Type="_printer._tcp" 115 | ``` 116 | 117 | ## Conformance 118 | 119 | This library passes the [multicast DNS tests](https://github.com/brutella/dnssd/blob/36a2d8c541aab14895fc5492d5ad8ec447a67c47/_cmd/bct/ConformanceTestResults) of Apple's Bonjour Conformance Test. 120 | 121 | ## TODO 122 | 123 | - [ ] Support hot plugging 124 | - [ ] Support negative responses (RFC6762 6.1) 125 | - [ ] Handle txt records case insensitive 126 | - [ ] Remove outdated services from cache regularly 127 | - [ ] Make sure that hostnames are FQDNs 128 | 129 | # Contact 130 | 131 | Matthias Hochgatterer 132 | 133 | Github: [https://github.com/brutella](https://github.com/brutella/) 134 | 135 | Twitter: [https://twitter.com/brutella](https://twitter.com/brutella) 136 | 137 | 138 | # License 139 | 140 | *dnssd* is available under the MIT license. See the LICENSE file for more info. 141 | -------------------------------------------------------------------------------- /browse.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "github.com/brutella/dnssd/log" 5 | "github.com/miekg/dns" 6 | 7 | "context" 8 | "fmt" 9 | "net" 10 | ) 11 | 12 | // BrowseEntry represents a discovered service instance. 13 | type BrowseEntry struct { 14 | IPs []net.IP 15 | Host string 16 | Port int 17 | IfaceName string 18 | Name string 19 | Type string 20 | Domain string 21 | Text map[string]string 22 | } 23 | 24 | // AddFunc is called when a service instance was found. 25 | type AddFunc func(BrowseEntry) 26 | 27 | // RmvFunc is called when a service instance disappared. 28 | type RmvFunc func(BrowseEntry) 29 | 30 | // LookupType browses for service instances. 31 | func LookupType(ctx context.Context, service string, add AddFunc, rmv RmvFunc) (err error) { 32 | conn, err := newMDNSConn() 33 | if err != nil { 34 | return err 35 | } 36 | defer conn.close() 37 | 38 | return lookupType(ctx, service, conn, add, rmv) 39 | } 40 | 41 | // LookupTypeAtInterface browses for service instances at specific network interfaces. 42 | func LookupTypeAtInterfaces(ctx context.Context, service string, add AddFunc, rmv RmvFunc, ifaces ...string) (err error) { 43 | conn, err := newMDNSConn(ifaces...) 44 | if err != nil { 45 | return err 46 | } 47 | defer conn.close() 48 | 49 | return lookupType(ctx, service, conn, add, rmv, ifaces...) 50 | } 51 | 52 | // ServiceInstanceName returns the service instance name 53 | // in the form of ... 54 | // (Note the trailing dot.) 55 | func (e BrowseEntry) EscapedServiceInstanceName() string { 56 | return fmt.Sprintf("%s.%s.%s.", escape.Replace(e.Name), e.Type, e.Domain) 57 | } 58 | 59 | // ServiceInstanceName returns the same as `ServiceInstanceName()` 60 | // but removes any escape characters. 61 | func (e BrowseEntry) ServiceInstanceName() string { 62 | return fmt.Sprintf("%s.%s.%s.", e.Name, e.Type, e.Domain) 63 | } 64 | 65 | func lookupType(ctx context.Context, service string, conn MDNSConn, add AddFunc, rmv RmvFunc, ifaces ...string) (err error) { 66 | var cache = NewCache() 67 | 68 | m := new(dns.Msg) 69 | m.Question = []dns.Question{ 70 | dns.Question{ 71 | Name: service, 72 | Qtype: dns.TypePTR, 73 | Qclass: dns.ClassINET, 74 | }, 75 | } 76 | // TODO include known answers which current ttl is more than half of the correct ttl (see TFC6772 7.1: Known-Answer Supression) 77 | // m.Answer = ... 78 | // m.Authoritive = false // because our answers are *believes* 79 | 80 | readCtx, readCancel := context.WithCancel(ctx) 81 | defer readCancel() 82 | 83 | ch := conn.Read(readCtx) 84 | 85 | qs := make(chan *Query) 86 | go func() { 87 | for _, iface := range MulticastInterfaces(ifaces...) { 88 | iface := iface 89 | q := &Query{msg: m, iface: iface} 90 | qs <- q 91 | } 92 | }() 93 | 94 | es := []*BrowseEntry{} 95 | for { 96 | select { 97 | case q := <-qs: 98 | log.Debug.Printf("Send browsing query at %s\n%s\n", q.IfaceName(), q.msg) 99 | if err := conn.SendQuery(q); err != nil { 100 | log.Debug.Println("SendQuery:", err) 101 | } 102 | 103 | case req := <-ch: 104 | log.Debug.Printf("Receive message at %s\n%s\n", req.IfaceName(), req.msg) 105 | cache.UpdateFrom(req) 106 | for _, srv := range cache.Services() { 107 | if srv.ServiceName() != service { 108 | continue 109 | } 110 | 111 | for ifaceName, ips := range srv.ifaceIPs { 112 | var found = false 113 | for _, e := range es { 114 | if e.Name == srv.Name && e.IfaceName == ifaceName { 115 | found = true 116 | break 117 | } 118 | } 119 | if !found { 120 | e := BrowseEntry{ 121 | IPs: ips, 122 | Host: srv.Host, 123 | Port: srv.Port, 124 | IfaceName: ifaceName, 125 | Name: srv.Name, 126 | Type: srv.Type, 127 | Domain: srv.Domain, 128 | Text: srv.Text, 129 | } 130 | es = append(es, &e) 131 | add(e) 132 | } 133 | } 134 | } 135 | 136 | tmp := []*BrowseEntry{} 137 | for _, e := range es { 138 | var found = false 139 | for _, srv := range cache.Services() { 140 | if srv.ServiceInstanceName() == e.ServiceInstanceName() { 141 | found = true 142 | break 143 | } 144 | } 145 | 146 | if found { 147 | tmp = append(tmp, e) 148 | } else { 149 | // TODO 150 | rmv(*e) 151 | } 152 | } 153 | es = tmp 154 | case <-ctx.Done(): 155 | return ctx.Err() 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /browser_test.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestBrowse(t *testing.T) { 14 | testIface, _ := net.InterfaceByName("lo0") 15 | if testIface == nil { 16 | testIface, _ = net.InterfaceByName("lo") 17 | } 18 | if testIface == nil { 19 | t.Fatal("can not find the local interface") 20 | } 21 | 22 | localhost, err := os.Hostname() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | localhost = strings.TrimSuffix(strings.Replace(localhost, " ", "-", -1), ".local") // replace spaces with dashes and remove .local suffix 27 | for tName, hostValue := range map[string]string{ 28 | "regular host": "My-Computer", 29 | "empty host": "", 30 | "ip address": "192.168.0.1", 31 | } { 32 | t.Run(tName, func(t *testing.T) { 33 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 34 | defer cancel() 35 | cfg := Config{ 36 | Name: "My Service", 37 | Type: "_test._tcp", 38 | Host: hostValue, 39 | Port: 12334, 40 | Ifaces: []string{testIface.Name}, 41 | } 42 | srv, err := NewService(cfg) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | rs, err := NewResponder() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | go func() { 52 | _ = rs.Respond(ctx) 53 | }() 54 | 55 | _, err = rs.Add(srv) 56 | if err != nil { 57 | t.Fatal(err) 58 | 59 | } 60 | 61 | resultChan := make(chan BrowseEntry) 62 | defer close(resultChan) 63 | go func() { 64 | _ = LookupType(ctx, fmt.Sprintf("%s.local.", cfg.Type), func(entry BrowseEntry) { 65 | resultChan <- entry 66 | }, func(entry BrowseEntry) {}) 67 | }() 68 | 69 | select { 70 | case <-ctx.Done(): 71 | t.Fatal("timeout") 72 | case entry := <-resultChan: 73 | if entry.Name != cfg.Name { 74 | t.Fatalf("is=%v want=%v", entry.Name, cfg.Name) 75 | } 76 | if tName == "empty host" { 77 | if entry.Host != localhost { 78 | t.Fatalf("is=%v want=%v", entry.Host, localhost) 79 | } 80 | } else { 81 | if entry.Host != cfg.Host { 82 | t.Fatalf("is=%v want=%v", entry.Host, cfg.Host) 83 | } 84 | } 85 | if entry.Port != cfg.Port { 86 | t.Fatalf("is=%v want=%v", entry.Port, cfg.Port) 87 | } 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // Cache stores services in memory. 12 | type Cache struct { 13 | services map[string]*Service 14 | } 15 | 16 | // NewCache returns a new in-memory cache. 17 | func NewCache() *Cache { 18 | return &Cache{ 19 | services: make(map[string]*Service), 20 | } 21 | } 22 | 23 | // Services returns a list of stored services. 24 | func (c *Cache) Services() []*Service { 25 | tmp := []*Service{} 26 | for _, s := range c.services { 27 | tmp = append(tmp, s) 28 | } 29 | return tmp 30 | } 31 | 32 | // UpdateFrom updates the cache from resource records in msg. 33 | // TODO consider the cache-flush bit to make records as to be deleted in one second 34 | func (c *Cache) UpdateFrom(req *Request) (adds []*Service, rmvs []*Service) { 35 | answers := filterRecords(req, nil) 36 | sort.Sort(byType(answers)) 37 | 38 | for _, answer := range answers { 39 | switch rr := answer.(type) { 40 | case *dns.PTR: 41 | ttl := time.Duration(rr.Hdr.Ttl) * time.Second 42 | 43 | var entry *Service 44 | if e, ok := c.services[rr.Ptr]; !ok { 45 | if ttl == 0 { 46 | // Ignore new records with no ttl 47 | break 48 | } 49 | entry = newService(rr.Ptr) 50 | adds = append(adds, entry) 51 | c.services[entry.EscapedServiceInstanceName()] = entry 52 | } else { 53 | entry = e 54 | } 55 | 56 | entry.TTL = ttl 57 | entry.expiration = time.Now().Add(ttl) 58 | 59 | case *dns.SRV: 60 | ttl := time.Duration(rr.Hdr.Ttl) * time.Second 61 | var entry *Service 62 | if e, ok := c.services[rr.Hdr.Name]; !ok { 63 | if ttl == 0 { 64 | // Ignore new records with no ttl 65 | break 66 | } 67 | entry = newService(rr.Hdr.Name) 68 | adds = append(adds, entry) 69 | c.services[entry.EscapedServiceInstanceName()] = entry 70 | } else { 71 | entry = e 72 | } 73 | 74 | entry.SetHostname(rr.Target) 75 | entry.TTL = ttl 76 | entry.expiration = time.Now().Add(ttl) 77 | entry.Port = int(rr.Port) 78 | 79 | case *dns.A: 80 | for _, entry := range c.services { 81 | if entry.Hostname() == rr.Hdr.Name { 82 | entry.addIP(rr.A, req.iface) 83 | } 84 | } 85 | 86 | case *dns.AAAA: 87 | for _, entry := range c.services { 88 | if entry.Hostname() == rr.Hdr.Name { 89 | entry.addIP(rr.AAAA, req.iface) 90 | } 91 | } 92 | 93 | case *dns.TXT: 94 | if entry, ok := c.services[rr.Hdr.Name]; ok { 95 | text := make(map[string]string) 96 | for _, txt := range rr.Txt { 97 | elems := strings.SplitN(txt, "=", 2) 98 | if len(elems) == 2 { 99 | key := elems[0] 100 | value := elems[1] 101 | 102 | // Don't override existing keys 103 | // TODO make txt records case insensitive 104 | if _, ok := text[key]; !ok { 105 | text[key] = value 106 | } 107 | 108 | text[key] = value 109 | } 110 | } 111 | 112 | entry.Text = text 113 | entry.TTL = time.Duration(rr.Hdr.Ttl) * time.Second 114 | entry.expiration = time.Now().Add(entry.TTL) 115 | } 116 | default: 117 | // ignore 118 | } 119 | } 120 | 121 | // TODO remove outdated services regularly 122 | rmvs = c.removeExpired() 123 | 124 | return 125 | } 126 | 127 | func (c *Cache) removeExpired() []*Service { 128 | var outdated []*Service 129 | var services = c.services 130 | for key, srv := range services { 131 | if time.Now().After(srv.expiration) { 132 | outdated = append(outdated, srv) 133 | delete(c.services, key) 134 | } 135 | } 136 | 137 | return outdated 138 | } 139 | 140 | type byType []dns.RR 141 | 142 | func (a byType) Len() int { return len(a) } 143 | func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 144 | func (a byType) Less(i, j int) bool { 145 | // Sort in the following order 146 | // 1. SRV or PTR 147 | // 2. Anything else 148 | switch a[i].(type) { 149 | case *dns.SRV: 150 | return true 151 | case *dns.PTR: 152 | return true 153 | } 154 | 155 | return false 156 | } 157 | 158 | // filterRecords returns 159 | // - A and AAAA records for the same hostname as in defined by the service 160 | // - SRV records related to the same service instance name 161 | func filterRecords(req *Request, service *Service) []dns.RR { 162 | if req.iface != nil && service != nil && len(service.Ifaces) > 0 { 163 | if !service.IsVisibleAtInterface(req.iface.Name) { 164 | // Ignore records if the request coming from an ignored interface. 165 | return []dns.RR{} 166 | } 167 | } 168 | 169 | var all []dns.RR 170 | all = append(all, req.msg.Answer...) 171 | all = append(all, req.msg.Ns...) 172 | all = append(all, req.msg.Extra...) 173 | 174 | if service == nil { 175 | return all 176 | } 177 | 178 | var answers []dns.RR 179 | for _, answer := range all { 180 | switch rr := answer.(type) { 181 | case *dns.SRV: 182 | if rr.Target == service.Hostname() { 183 | // Ignore records coming from ourself 184 | continue 185 | } 186 | if rr.Hdr.Name != service.EscapedServiceInstanceName() { 187 | // Ignore records from other service instances 188 | continue 189 | } 190 | case *dns.A: 191 | if rr.Hdr.Name != service.Hostname() { 192 | // Ignore IPv4 address from other hosts 193 | continue 194 | } 195 | 196 | ip := rr.A.To4() 197 | if service.HasIPOnAnyInterface(ip) { 198 | // Ignore this record because we know that the service 199 | // has this ip address but on a different interface. 200 | continue 201 | } 202 | 203 | case *dns.AAAA: 204 | if rr.Hdr.Name != service.Hostname() { 205 | // Ignore IPv6 address from other hosts 206 | continue 207 | } 208 | 209 | ip := rr.AAAA.To16() 210 | if service.HasIPOnAnyInterface(ip) { 211 | // Ignore this record because we know that the service 212 | // has this ip address but on a different interface. 213 | continue 214 | } 215 | } 216 | answers = append(answers, answer) 217 | } 218 | 219 | return answers 220 | } 221 | -------------------------------------------------------------------------------- /cmd/bct/BonjourConformanceTest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brutella/dnssd/1fe6152bc887bc1b6efed7a374f1b64bded87b4d/cmd/bct/BonjourConformanceTest -------------------------------------------------------------------------------- /cmd/bct/ConformanceTestResults: -------------------------------------------------------------------------------- 1 | Bonjour Conformance Test Version 1.5.0(1500) 2 | Started Thu Jan 31 10:12:57 2019 3 | Completed Thu Jan 31 10:17:53 2019 4 | 5 | Link-Local Address Allocation 6 | ----------------------------- 7 | SKIPPED (test omitted by operator) 8 | 9 | Multicast DNS 10 | ----------------------------- 11 | PASSED: INITIAL PROBING 12 | WARNING: PROBING 13 | PASSED: PROBING: SIMULTANEOUS PROBE CONFLICT 14 | PASSED: PROBING: RATE LIMITING 15 | PASSED: PROBING: PROBE DENIALS 16 | PASSED: PROBING 17 | PASSED: WINNING SIMULTANEOUS PROBES - ANNOUNCEMENTS 18 | PASSED: WINNING SIMULTANEOUS PROBES: WINNING SIMULTANEOUS PROBES 19 | PASSED: WINNING SIMULTANEOUS PROBES 20 | PASSED: SRV PROBING/ANNOUNCEMENTS BASIC 21 | PASSED: SRV PROBING/ANNOUNCEMENTS 22 | PASSED: SUBSEQUENT CONFLICT - ANNOUNCEMENTS 23 | PASSED: SUBSEQUENT CONFLICT - A 24 | PASSED: SUBSEQUENT CONFLICT - ANNOUNCEMENTS 25 | PASSED: SUBSEQUENT CONFLICT - SRV 26 | PASSED: SUBSEQUENT CONFLICT 27 | PASSED: SIMPLE REPLY VERIFICATION 28 | PASSED: SHARED REPLY TIMING - UNIFORM RANDOM REPLY TIME DISTRIBUTION 29 | PASSED: SHARED REPLY TIMING 30 | PASSED: DUPLICATE SUPPRESSION 31 | PASSED: DISTRIBUTED DUPLICATE SUPPRESSION 32 | WARNING: MULTIPLE QUESTIONS - SHARED REPLY TIMING - UNIFORM RANDOM REPLY TIME DISTRIBUTION 33 | PASSED: MULTIPLE QUESTIONS - SHARED REPLY TIMING 34 | PASSED: MULTIPLE QUESTIONS - DUPLICATE SUPPRESSION 35 | PASSED: MULTIPLE QUESTIONS - DISTRIBUTED DUPLICATE SUPPRESSION 36 | PASSED: REPLY AGGREGATION 37 | PASSED: MANUAL NAME CHANGE - ANNOUNCEMENTS 38 | PASSED: MANUAL NAME CHANGE 39 | SKIPPED (omitted by operator): HOT-PLUGGING 40 | PASSED: NO DUPLICATE RECORDS IN PACKETS 41 | PASSED: REQUIRED ADDITIONAL RECORDS IN ANSWERS 42 | PASSED: LEGAL CHARACTERS IN ADDRESS RECORD NAMES 43 | PASSED: CACHE FLUSH BIT SET IN NON-SHARED RESPONSES 44 | PASSED: CACHE FLUSH BIT NOT SET IN PROPOSED ANSWER OF PROBES 45 | SKIPPED with 2 warning(s) and 1 skipped subtest(s). 46 | 47 | Network Interoperability 48 | ----------------------------- 49 | SKIPPED (test omitted by operator) 50 | 51 | ****************************************************************************** 52 | Sorry, you did not successfully pass the Bonjour Conformance test 53 | ****************************************************************************** 54 | -------------------------------------------------------------------------------- /cmd/bct/README.md: -------------------------------------------------------------------------------- 1 | # Bonjour Conformance Test 2 | 3 | The goal is to make `dnssd` fully compliant with [Bonjour](https://developer.apple.com/bonjour/) from Apple. We are using *Bonjour Conformance Test* (v1.5.0) to test our mDNS responder implementation. 4 | 5 | I'm using a MacBook Pro running macOSS 10.12 (or higher) as a test machine, which is connected to a router. 6 | The tested device is a Raspberry Pi 3 Model B also connected to the router. 7 | 8 | There is a test implementation of a mDNS responder in `_cmd/bct/main.go` which is compiled for the RPi with `GOOS=linux GOARCH=arm GOARM=7 go build -o bct main.go`. 9 | Run the executable `bct` on the RPi while running multicast DNS tests (withou hot plugging – see #9) on the test machine with `sudo ./BonjourConformanceTest -S -M h -DD -E `. 10 | 11 | The latest test results can be found in `ConformanceTestResults`. -------------------------------------------------------------------------------- /cmd/bct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "github.com/brutella/dnssd" 8 | "github.com/brutella/dnssd/log" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | if resp, err := dnssd.NewResponder(); err != nil { 16 | fmt.Println(err) 17 | } else { 18 | 19 | stop := make(chan os.Signal, 1) 20 | signal.Notify(stop, os.Interrupt) 21 | 22 | ctx, cancel := context.WithCancel(context.Background()) 23 | go func() { 24 | for { 25 | reader := bufio.NewReader(os.Stdin) 26 | fmt.Print("Enter name \nor\nexit\n>") 27 | name, _ := reader.ReadString('\n') 28 | name = strings.Trim(name, "\n") 29 | 30 | if name == "exit" { 31 | cancel() 32 | return 33 | } 34 | 35 | cfg := dnssd.Config{ 36 | Name: name, 37 | Type: "_asdf._tcp", 38 | Port: 12345, 39 | } 40 | srv, err := dnssd.NewService(cfg) 41 | if err != nil { 42 | log.Debug.Fatal(err) 43 | } 44 | log.Debug.Printf("%+v\n", srv) 45 | h, _ := resp.Add(srv) 46 | 47 | <-stop 48 | resp.Remove(h) 49 | } 50 | }() 51 | 52 | if err := resp.Respond(ctx); err != nil { 53 | fmt.Println(err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/browse/main.go: -------------------------------------------------------------------------------- 1 | // Command browse browses for specific dns-sd service types. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | 13 | "github.com/brutella/dnssd" 14 | "github.com/brutella/dnssd/log" 15 | ) 16 | 17 | var serviceFlag = flag.String("Type", "_asdf._tcp", "Service type") 18 | var domainFlag = flag.String("Domain", "local.", "Browsing domain") 19 | var verboseFlag = flag.Bool("Verbose", false, "Verbose logging") 20 | var timeFormat = "15:04:05.000" 21 | 22 | func main() { 23 | flag.Parse() 24 | 25 | if *verboseFlag { 26 | log.Debug.Enable() 27 | } 28 | 29 | ctx, cancel := context.WithCancel(context.Background()) 30 | defer cancel() 31 | 32 | service := fmt.Sprintf("%s.%s.", strings.Trim(*serviceFlag, "."), strings.Trim(*domainFlag, ".")) 33 | 34 | fmt.Printf("Browsing for %s\n", service) 35 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 36 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 37 | fmt.Printf("Timestamp A/R if Domain Service Type Service Name\n") 38 | 39 | addFn := func(e dnssd.BrowseEntry) { 40 | fmt.Printf("%s Add %s %s %s %s (%s)\n", time.Now().Format(timeFormat), e.IfaceName, e.Domain, e.Type, e.Name, e.IPs) 41 | } 42 | 43 | rmvFn := func(e dnssd.BrowseEntry) { 44 | fmt.Printf("%s Rmv %s %s %s %s\n", time.Now().Format(timeFormat), e.IfaceName, e.Domain, e.Type, e.Name) 45 | } 46 | 47 | if err := dnssd.LookupType(ctx, service, addFn, rmvFn); err != nil { 48 | fmt.Println(err) 49 | return 50 | } 51 | 52 | stop := make(chan os.Signal, 1) 53 | signal.Notify(stop, os.Interrupt) 54 | 55 | <-stop 56 | cancel() 57 | } 58 | -------------------------------------------------------------------------------- /cmd/debug/main.go: -------------------------------------------------------------------------------- 1 | // Command debug logs dns packets to the console. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "github.com/brutella/dnssd" 12 | ) 13 | 14 | var timeFormat = "15:04:05.000" 15 | 16 | func main() { 17 | fmt.Printf("Debugging…\n") 18 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 19 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 20 | 21 | fn := func(req *dnssd.Request) { 22 | fmt.Println("-------------------------------------------") 23 | fmt.Printf("%s\n%v\n", time.Now().Format(timeFormat), req) 24 | } 25 | 26 | ctx, cancel := context.WithCancel(context.Background()) 27 | defer cancel() 28 | 29 | if rsp, err := dnssd.NewResponder(); err != nil { 30 | fmt.Println(err) 31 | } else { 32 | rsp.Debug(ctx, fn) 33 | 34 | stop := make(chan os.Signal, 1) 35 | signal.Notify(stop, os.Interrupt) 36 | <-stop 37 | cancel() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/dnssd/dnssd.go: -------------------------------------------------------------------------------- 1 | // dnssd is a utilty to register and browser DNS-SD services. 2 | package main 3 | 4 | import ( 5 | "github.com/brutella/dnssd" 6 | "github.com/brutella/dnssd/log" 7 | 8 | "context" 9 | "flag" 10 | "fmt" 11 | "net" 12 | "os" 13 | "os/signal" 14 | "path/filepath" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | var nameFlag = flag.String("Name", "", "Service Name") 20 | var typeFlag = flag.String("Type", "", "Service type") 21 | var domainFlag = flag.String("Domain", "local", "Service Domain") 22 | var hostFlag = flag.String("Host", "", "Hostname") 23 | var ipFlag = flag.String("IP", "", "") 24 | var portFlag = flag.Int("Port", 0, "") 25 | var interfaceFlag = flag.String("Interface", "", "") 26 | var timeFormat = "15:04:05.000" 27 | var verboseFlag = flag.Bool("Verbose", false, "Verbose logging") 28 | 29 | // Name of the invoked executable. 30 | var name = filepath.Base(os.Args[0]) 31 | 32 | func printUsage() { 33 | log.Info.Println("A DNS-SD utilty to register, browse and resolve Bonjour services.\n\n" + 34 | "Usage:\n" + 35 | " " + name + " register -Name -Type -Port [-Domain -Interface -Host -IP ]\n" + 36 | " " + name + " browse -Type [-Domain -Interface ]\n" + 37 | " " + name + " resolve -Name -Type [-Domain -Interface ]\n") 38 | } 39 | 40 | func resolve(typee, instance string) { 41 | ifaces := parseInterfaceFlag() 42 | ifaceDesc := "all interfaces" 43 | if len(ifaces) > 0 { 44 | ifaceDesc = strings.Join(ifaces, ", ") 45 | } 46 | 47 | fmt.Printf("Lookup %s at %s\n", instance, ifaceDesc) 48 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 49 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 50 | 51 | addFn := func(e dnssd.BrowseEntry) { 52 | if e.ServiceInstanceName() == instance { 53 | text := "" 54 | for key, value := range e.Text { 55 | text += fmt.Sprintf("%s=%s", key, value) 56 | } 57 | fmt.Printf("%s %s can be reached at %s.%s.:%d %v\n", time.Now().Format(timeFormat), e.ServiceInstanceName(), e.Host, e.Domain, e.Port, text) 58 | } 59 | } 60 | 61 | ctx, cancel := context.WithCancel(context.Background()) 62 | defer cancel() 63 | 64 | if err := dnssd.LookupTypeAtInterfaces(ctx, typee, addFn, func(dnssd.BrowseEntry) {}, ifaces...); err != nil { 65 | fmt.Println(err) 66 | return 67 | } 68 | 69 | stop := make(chan os.Signal, 1) 70 | signal.Notify(stop, os.Interrupt) 71 | 72 | <-stop 73 | cancel() 74 | } 75 | 76 | func register(instance string) { 77 | if *portFlag == 0 { 78 | log.Info.Println("invalid port", *portFlag) 79 | printUsage() 80 | return 81 | } 82 | 83 | var ips []net.IP 84 | if *ipFlag != "" { 85 | ip := net.ParseIP(*ipFlag) 86 | if ip == nil { 87 | log.Info.Println("invalid ip", *ipFlag) 88 | printUsage() 89 | return 90 | } 91 | ips = []net.IP{ip} 92 | } 93 | 94 | fmt.Printf("Registering Service %s port %d\n", instance, *portFlag) 95 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 96 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 97 | 98 | ctx, cancel := context.WithCancel(context.Background()) 99 | defer cancel() 100 | 101 | if resp, err := dnssd.NewResponder(); err != nil { 102 | fmt.Println(err) 103 | } else { 104 | cfg := dnssd.Config{ 105 | Name: *nameFlag, 106 | Type: *typeFlag, 107 | Domain: *domainFlag, 108 | Port: *portFlag, 109 | Ifaces: parseInterfaceFlag(), 110 | IPs: ips, 111 | Host: *hostFlag, 112 | } 113 | srv, err := dnssd.NewService(cfg) 114 | if err != nil { 115 | log.Info.Fatal(err) 116 | } 117 | 118 | go func() { 119 | stop := make(chan os.Signal, 1) 120 | signal.Notify(stop, os.Interrupt) 121 | 122 | <-stop 123 | cancel() 124 | }() 125 | 126 | go func() { 127 | time.Sleep(1 * time.Second) 128 | handle, err := resp.Add(srv) 129 | if err != nil { 130 | fmt.Println(err) 131 | } else { 132 | fmt.Printf("%s Got a reply for service %s: Name now registered and active\n", time.Now().Format(timeFormat), handle.Service().ServiceInstanceName()) 133 | } 134 | }() 135 | err = resp.Respond(ctx) 136 | 137 | if err != nil { 138 | fmt.Println(err) 139 | } 140 | } 141 | } 142 | 143 | func parseInterfaceFlag() []string { 144 | ifaces := []string{} 145 | if len(*interfaceFlag) > 0 { 146 | for _, iface := range strings.Split(*interfaceFlag, ",") { 147 | trimmed := strings.TrimSpace(iface) 148 | if len(trimmed) == 0 { 149 | continue 150 | } 151 | ifaces = append(ifaces, trimmed) 152 | } 153 | } 154 | 155 | return ifaces 156 | } 157 | 158 | func browse(typee string) { 159 | ctx, cancel := context.WithCancel(context.Background()) 160 | defer cancel() 161 | 162 | ifaces := parseInterfaceFlag() 163 | ifaceDesc := "all interfaces" 164 | if len(ifaces) > 0 { 165 | ifaceDesc = strings.Join(ifaces, ", ") 166 | } 167 | 168 | fmt.Printf("Browsing for %s at %s\n", typee, ifaceDesc) 169 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 170 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 171 | fmt.Printf("Timestamp A/R if Domain Service Type Instance Name\n") 172 | 173 | addFn := func(e dnssd.BrowseEntry) { 174 | fmt.Printf("%s Add %s %s %s %s (%s)\n", time.Now().Format(timeFormat), e.IfaceName, e.Domain, e.Type, e.Name, e.IPs) 175 | } 176 | 177 | rmvFn := func(e dnssd.BrowseEntry) { 178 | fmt.Printf("%s Rmv %s %s %s %s\n", time.Now().Format(timeFormat), e.IfaceName, e.Domain, e.Type, e.Name) 179 | } 180 | 181 | if err := dnssd.LookupTypeAtInterfaces(ctx, typee, addFn, rmvFn, ifaces...); err != nil { 182 | fmt.Println(err) 183 | return 184 | } 185 | 186 | stop := make(chan os.Signal, 1) 187 | signal.Notify(stop, os.Interrupt) 188 | 189 | <-stop 190 | cancel() 191 | } 192 | 193 | func main() { 194 | args := os.Args[1:] 195 | if len(args) == 0 { 196 | printUsage() 197 | return 198 | } 199 | 200 | // The first argument is the command. 201 | cmd := args[0] 202 | 203 | // Use the remaining arguments as flags. 204 | flag.CommandLine.Parse(os.Args[2:]) 205 | 206 | if *typeFlag == "" { 207 | printUsage() 208 | return 209 | } 210 | 211 | if *verboseFlag { 212 | log.Debug.Enable() 213 | } 214 | 215 | typee := fmt.Sprintf("%s.%s.", strings.Trim(*typeFlag, "."), strings.Trim(*domainFlag, ".")) 216 | instance := fmt.Sprintf("%s.%s.%s.", strings.Trim(*nameFlag, "."), strings.Trim(*typeFlag, "."), strings.Trim(*domainFlag, ".")) 217 | 218 | switch cmd { 219 | case "register": 220 | if *nameFlag == "" { 221 | printUsage() 222 | return 223 | } 224 | register(instance) 225 | case "browse": 226 | browse(typee) 227 | case "resolve": 228 | if *nameFlag == "" { 229 | printUsage() 230 | return 231 | } 232 | resolve(typee, instance) 233 | default: 234 | printUsage() 235 | return 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /cmd/filter-ifaces/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | slog "log" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "github.com/brutella/dnssd" 12 | ) 13 | 14 | func main() { 15 | // log.Debug.Enable() 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | go startDNSSDServer(ctx) 20 | 21 | time.Sleep(time.Second) 22 | go queryDNSSD(ctx) 23 | 24 | go func() { 25 | stop := make(chan os.Signal, 1) 26 | signal.Notify(stop, os.Interrupt) 27 | 28 | <-stop 29 | cancel() 30 | }() 31 | 32 | <-ctx.Done() 33 | } 34 | 35 | func queryDNSSD(ctx context.Context) { 36 | service := "_service_type._tcp.local." 37 | 38 | slog.Printf("Lookup %s\n", service) 39 | 40 | addFn := func(e dnssd.BrowseEntry) { 41 | slog.Printf( 42 | "%s can be reached at %s %v\n", 43 | e.ServiceInstanceName(), 44 | e.IPs, 45 | e.Text) 46 | } 47 | 48 | if err := dnssd.LookupType(ctx, service, addFn, func(dnssd.BrowseEntry) {}); err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | } 53 | 54 | func startDNSSDServer(ctx context.Context) { 55 | txtRecord := map[string]string{ 56 | "txtvers": "1", 57 | "data": "some-data", 58 | } 59 | config := dnssd.Config{ 60 | Name: "my_service", 61 | Type: "_service_type._tcp", 62 | Domain: "local", 63 | Port: 1337, 64 | Text: txtRecord, 65 | Ifaces: []string{"en0"}, 66 | // IPs: []net.IP{net.ParseIP("192.168.228.92")}, 67 | } 68 | 69 | service, err := dnssd.NewService(config) 70 | if err != nil { 71 | slog.Fatal(err) 72 | } 73 | 74 | responder, err := dnssd.NewResponder() 75 | if err != nil { 76 | slog.Fatal(err) 77 | } 78 | 79 | _, err = responder.Add(service) 80 | if err != nil { 81 | slog.Fatal(err) 82 | } 83 | 84 | slog.Println("Starting dnssd server") 85 | err = responder.Respond(ctx) 86 | if err != nil { 87 | slog.Fatal(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cmd/register/main.go: -------------------------------------------------------------------------------- 1 | // Command register registers a dns-sd service instance. 2 | package main 3 | 4 | import ( 5 | "github.com/brutella/dnssd" 6 | "github.com/brutella/dnssd/log" 7 | 8 | "context" 9 | "flag" 10 | "fmt" 11 | slog "log" 12 | "os" 13 | "os/signal" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var instanceFlag = flag.String("Name", "Service", "Service name") 19 | var serviceFlag = flag.String("Type", "_asdf._tcp", "Service type") 20 | var domainFlag = flag.String("Domain", "local", "domain") 21 | var portFlag = flag.Int("Port", 12345, "Port") 22 | var verboseFlag = flag.Bool("Verbose", false, "Verbose logging") 23 | var interfaceFlag = flag.String("Interface", "", "Network interface name") 24 | var timeFormat = "15:04:05.000" 25 | 26 | func main() { 27 | flag.Parse() 28 | if len(*instanceFlag) == 0 || len(*serviceFlag) == 0 || len(*domainFlag) == 0 { 29 | flag.Usage() 30 | return 31 | } 32 | 33 | if *verboseFlag { 34 | log.Debug.Enable() 35 | } 36 | 37 | instance := fmt.Sprintf("%s.%s.%s.", strings.Trim(*instanceFlag, "."), strings.Trim(*serviceFlag, "."), strings.Trim(*domainFlag, ".")) 38 | 39 | fmt.Printf("Registering Service %s port %d\n", instance, *portFlag) 40 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 41 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 42 | 43 | ctx, cancel := context.WithCancel(context.Background()) 44 | defer cancel() 45 | 46 | if resp, err := dnssd.NewResponder(); err != nil { 47 | fmt.Println(err) 48 | } else { 49 | ifaces := []string{} 50 | if len(*interfaceFlag) > 0 { 51 | ifaces = append(ifaces, *interfaceFlag) 52 | } 53 | 54 | cfg := dnssd.Config{ 55 | Name: *instanceFlag, 56 | Type: *serviceFlag, 57 | Domain: *domainFlag, 58 | Port: *portFlag, 59 | Ifaces: ifaces, 60 | } 61 | srv, err := dnssd.NewService(cfg) 62 | if err != nil { 63 | slog.Fatal(err) 64 | } 65 | 66 | go func() { 67 | stop := make(chan os.Signal, 1) 68 | signal.Notify(stop, os.Interrupt) 69 | 70 | <-stop 71 | cancel() 72 | }() 73 | 74 | go func() { 75 | time.Sleep(1 * time.Second) 76 | handle, err := resp.Add(srv) 77 | if err != nil { 78 | fmt.Println(err) 79 | } else { 80 | fmt.Printf("%s Got a reply for service %s: Name now registered and active\n", time.Now().Format(timeFormat), handle.Service().ServiceInstanceName()) 81 | } 82 | }() 83 | err = resp.Respond(ctx) 84 | 85 | if err != nil { 86 | fmt.Println(err) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cmd/resolve/main.go: -------------------------------------------------------------------------------- 1 | // Command resolve resolves a dns-sd service instance. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | 13 | "github.com/brutella/dnssd" 14 | ) 15 | 16 | var instanceFlag = flag.String("Name", "Service", "Service Name") 17 | var serviceFlag = flag.String("Type", "_asdf._tcp", "Service type") 18 | var domainFlag = flag.String("Domain", "local", "Browsing domain") 19 | 20 | var timeFormat = "15:04:05.000" 21 | 22 | func main() { 23 | flag.Parse() 24 | if len(*instanceFlag) == 0 || len(*serviceFlag) == 0 || len(*domainFlag) == 0 { 25 | flag.Usage() 26 | return 27 | } 28 | service := fmt.Sprintf("%s.%s.", strings.Trim(*serviceFlag, "."), strings.Trim(*domainFlag, ".")) 29 | instance := fmt.Sprintf("%s.%s.%s.", strings.Trim(*instanceFlag, "."), strings.Trim(*serviceFlag, "."), strings.Trim(*domainFlag, ".")) 30 | 31 | fmt.Printf("Lookup %s\n", instance) 32 | fmt.Printf("DATE: –––%s–––\n", time.Now().Format("Mon Jan 2 2006")) 33 | fmt.Printf("%s ...STARTING...\n", time.Now().Format(timeFormat)) 34 | 35 | addFn := func(e dnssd.BrowseEntry) { 36 | if e.ServiceInstanceName() == instance { 37 | text := "" 38 | for key, value := range e.Text { 39 | text += fmt.Sprintf("%s=%s", key, value) 40 | } 41 | fmt.Printf("%s %s can be reached at %s %v\n", time.Now().Format(timeFormat), e.ServiceInstanceName(), e.IPs, text) 42 | } 43 | } 44 | 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | defer cancel() 47 | 48 | if err := dnssd.LookupType(ctx, service, addFn, func(dnssd.BrowseEntry) {}); err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | 53 | stop := make(chan os.Signal, 1) 54 | signal.Notify(stop, os.Interrupt) 55 | 56 | <-stop 57 | cancel() 58 | } 59 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "reflect" 7 | "sort" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | // PTR returns the PTR record for the service. 13 | func PTR(srv Service) *dns.PTR { 14 | return &dns.PTR{ 15 | Hdr: dns.RR_Header{ 16 | Name: srv.ServiceName(), 17 | Rrtype: dns.TypePTR, 18 | Class: dns.ClassINET, 19 | Ttl: TTLDefault, 20 | }, 21 | Ptr: srv.EscapedServiceInstanceName(), 22 | } 23 | } 24 | 25 | func DNSSDServicesPTR(srv Service) *dns.PTR { 26 | return &dns.PTR{ 27 | Hdr: dns.RR_Header{ 28 | Name: srv.ServicesMetaQueryName(), 29 | Rrtype: dns.TypePTR, 30 | Class: dns.ClassINET, 31 | Ttl: TTLDefault, 32 | }, 33 | Ptr: srv.ServiceName(), 34 | } 35 | } 36 | 37 | // SRV returns the SRV record for the service. 38 | func SRV(srv Service) *dns.SRV { 39 | return &dns.SRV{ 40 | Hdr: dns.RR_Header{ 41 | Name: srv.EscapedServiceInstanceName(), 42 | Rrtype: dns.TypeSRV, 43 | Class: dns.ClassINET, 44 | Ttl: TTLHostname, 45 | }, 46 | Priority: 0, 47 | Weight: 0, 48 | Port: uint16(srv.Port), 49 | Target: srv.Hostname(), 50 | } 51 | } 52 | 53 | // TXT returns the TXT record for the service. 54 | func TXT(srv Service) *dns.TXT { 55 | keys := []string{} 56 | for key := range srv.Text { 57 | keys = append(keys, key) 58 | } 59 | sort.Strings(keys) 60 | 61 | txts := []string{} 62 | for _, k := range keys { 63 | txts = append(txts, fmt.Sprintf("%s=%s", k, srv.Text[k])) 64 | } 65 | 66 | // An empty TXT record containing zero strings is not allowed. (RFC6763 6.1) 67 | if len(txts) == 0 { 68 | txts = []string{""} 69 | } 70 | 71 | return &dns.TXT{ 72 | Hdr: dns.RR_Header{ 73 | Name: srv.EscapedServiceInstanceName(), 74 | Rrtype: dns.TypeTXT, 75 | Class: dns.ClassINET, 76 | Ttl: TTLDefault, 77 | }, 78 | Txt: txts, 79 | } 80 | } 81 | 82 | // NSEC returns the NSEC record for the service. 83 | func NSEC(rr dns.RR, srv Service, iface *net.Interface) *dns.NSEC { 84 | if iface != nil && !srv.IsVisibleAtInterface(iface.Name) { 85 | return nil 86 | } 87 | 88 | switch r := rr.(type) { 89 | case *dns.PTR: 90 | return &dns.NSEC{ 91 | Hdr: dns.RR_Header{ 92 | Name: r.Ptr, 93 | Rrtype: dns.TypeNSEC, 94 | Class: dns.ClassINET, 95 | Ttl: TTLDefault, 96 | }, 97 | NextDomain: r.Ptr, 98 | TypeBitMap: []uint16{dns.TypeTXT, dns.TypeSRV}, 99 | } 100 | case *dns.SRV: 101 | types := []uint16{} 102 | ips := srv.IPsAtInterface(iface) 103 | if includesIPv4(ips) { 104 | types = append(types, dns.TypeA) 105 | } 106 | if includesIPv6(ips) { 107 | types = append(types, dns.TypeAAAA) 108 | } 109 | 110 | if len(types) > 0 { 111 | return &dns.NSEC{ 112 | Hdr: dns.RR_Header{ 113 | Name: r.Target, 114 | Rrtype: dns.TypeNSEC, 115 | Class: dns.ClassINET, 116 | Ttl: TTLDefault, 117 | }, 118 | NextDomain: r.Target, 119 | TypeBitMap: types, 120 | } 121 | } 122 | default: 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // A returns the A records (IPv4 addresses) for the service. 129 | func A(srv Service, iface *net.Interface) []*dns.A { 130 | if iface == nil { 131 | return []*dns.A{} 132 | } 133 | 134 | if !srv.IsVisibleAtInterface(iface.Name) { 135 | return []*dns.A{} 136 | } 137 | 138 | ips := srv.IPsAtInterface(iface) 139 | 140 | var as []*dns.A 141 | for _, ip := range ips { 142 | if ip.To4() != nil { 143 | a := &dns.A{ 144 | Hdr: dns.RR_Header{ 145 | Name: srv.Hostname(), 146 | Rrtype: dns.TypeA, 147 | Class: dns.ClassINET, 148 | Ttl: TTLHostname, 149 | }, 150 | A: ip, 151 | } 152 | as = append(as, a) 153 | } 154 | } 155 | 156 | return as 157 | } 158 | 159 | // AAAA returns the AAAA records (IPv6 addresses) of the service. 160 | func AAAA(srv Service, iface *net.Interface) []*dns.AAAA { 161 | if iface == nil { 162 | return []*dns.AAAA{} 163 | } 164 | 165 | if !srv.IsVisibleAtInterface(iface.Name) { 166 | return []*dns.AAAA{} 167 | } 168 | 169 | ips := srv.IPsAtInterface(iface) 170 | 171 | var aaaas []*dns.AAAA 172 | for _, ip := range ips { 173 | if ip.To4() == nil && ip.To16() != nil { 174 | aaaa := &dns.AAAA{ 175 | Hdr: dns.RR_Header{ 176 | Name: srv.Hostname(), 177 | Rrtype: dns.TypeAAAA, 178 | Class: dns.ClassINET, 179 | Ttl: TTLHostname, 180 | }, 181 | AAAA: ip, 182 | } 183 | aaaas = append(aaaas, aaaa) 184 | } 185 | } 186 | 187 | return aaaas 188 | } 189 | 190 | func splitRecords(records []dns.RR) (as []*dns.A, aaaas []*dns.AAAA, srvs []*dns.SRV) { 191 | for _, record := range records { 192 | switch rr := record.(type) { 193 | case *dns.A: 194 | if rr.A.To4() != nil { 195 | as = append(as, rr) 196 | } 197 | 198 | case *dns.AAAA: 199 | if rr.AAAA.To16() != nil { 200 | aaaas = append(aaaas, rr) 201 | } 202 | case *dns.SRV: 203 | srvs = append(srvs, rr) 204 | } 205 | } 206 | return 207 | } 208 | 209 | // Returns true if ips contains IPv4 addresses. 210 | func includesIPv4(ips []net.IP) bool { 211 | for _, ip := range ips { 212 | if ip.To4() != nil { 213 | return true 214 | } 215 | } 216 | 217 | return false 218 | } 219 | 220 | // Returns true if ips contains IPv6 addresses. 221 | func includesIPv6(ips []net.IP) bool { 222 | for _, ip := range ips { 223 | if ip.To4() == nil && ip.To16() != nil { 224 | return true 225 | } 226 | } 227 | 228 | return false 229 | } 230 | 231 | // Removes this from that. 232 | func remove(this []dns.RR, that []dns.RR) []dns.RR { 233 | var result []dns.RR 234 | for _, thatRr := range that { 235 | isUnknown := true 236 | for _, thisRr := range this { 237 | switch a := thisRr.(type) { 238 | case *dns.PTR: 239 | if ptr, ok := thatRr.(*dns.PTR); ok { 240 | if a.Ptr == ptr.Ptr && a.Hdr.Name == ptr.Hdr.Name && a.Hdr.Ttl > ptr.Hdr.Ttl/2 { 241 | isUnknown = false 242 | } 243 | } 244 | case *dns.SRV: 245 | if srv, ok := thatRr.(*dns.SRV); ok { 246 | if a.Target == srv.Target && a.Port == srv.Port && a.Hdr.Name == srv.Hdr.Name && a.Hdr.Ttl > srv.Hdr.Ttl/2 { 247 | isUnknown = false 248 | } 249 | } 250 | case *dns.TXT: 251 | if txt, ok := thatRr.(*dns.TXT); ok { 252 | if reflect.DeepEqual(a.Txt, txt.Txt) && a.Hdr.Ttl > txt.Hdr.Ttl/2 { 253 | isUnknown = false 254 | } 255 | } 256 | } 257 | } 258 | 259 | if isUnknown { 260 | result = append(result, thatRr) 261 | } 262 | } 263 | 264 | return result 265 | } 266 | 267 | // mergeMsgs merges the records in msgs into one message. 268 | func mergeMsgs(msgs []*dns.Msg) *dns.Msg { 269 | resp := new(dns.Msg) 270 | resp.Answer = []dns.RR{} 271 | resp.Ns = []dns.RR{} 272 | resp.Extra = []dns.RR{} 273 | resp.Question = []dns.Question{} 274 | 275 | for _, msg := range msgs { 276 | if msg.Answer != nil { 277 | resp.Answer = append(resp.Answer, remove(resp.Answer, msg.Answer)...) 278 | } 279 | if msg.Ns != nil { 280 | resp.Ns = append(resp.Ns, remove(resp.Ns, msg.Ns)...) 281 | } 282 | if msg.Extra != nil { 283 | resp.Extra = append(resp.Extra, remove(resp.Extra, msg.Extra)...) 284 | } 285 | 286 | if msg.Question != nil { 287 | resp.Question = append(resp.Question, msg.Question...) 288 | } 289 | } 290 | 291 | return resp 292 | } 293 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brutella/dnssd 2 | 3 | require ( 4 | github.com/miekg/dns v1.1.61 5 | github.com/vishvananda/netlink v1.2.1-beta.2 6 | golang.org/x/net v0.26.0 7 | ) 8 | 9 | require ( 10 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect 11 | golang.org/x/mod v0.18.0 // indirect 12 | golang.org/x/sync v0.7.0 // indirect 13 | golang.org/x/sys v0.21.0 // indirect 14 | golang.org/x/tools v0.22.0 // indirect 15 | ) 16 | 17 | go 1.20 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= 2 | github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= 3 | github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= 4 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 5 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= 6 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 7 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 8 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 9 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 10 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 11 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 12 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 13 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 16 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 18 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 19 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var ( 10 | // Debug generates debug lines of output with a "DEBUG" prefix. 11 | // By default the lines are written to /dev/null. 12 | Debug = &Logger{log.New(io.Discard, "DEBUG ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile)} 13 | 14 | // Info generates debug lines of output with a "INFO" prefix. 15 | // By default the lines are written to stdout. 16 | Info = &Logger{log.New(os.Stdout, "INFO ", log.LstdFlags|log.Lshortfile)} 17 | ) 18 | 19 | // Logger is a wrapper for log.Logger and provides 20 | // methods to enable and disable logging. 21 | type Logger struct { 22 | *log.Logger 23 | } 24 | 25 | // Disable sets the logging output to /dev/null. 26 | func (l *Logger) Disable() { 27 | l.SetOutput(io.Discard) 28 | } 29 | 30 | // Enable sets the logging output to stdout. 31 | func (l *Logger) Enable() { 32 | l.SetOutput(os.Stdout) 33 | } 34 | -------------------------------------------------------------------------------- /mdns.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | "github.com/brutella/dnssd/log" 10 | "github.com/miekg/dns" 11 | "golang.org/x/net/ipv4" 12 | "golang.org/x/net/ipv6" 13 | ) 14 | 15 | var ( 16 | // IPv4LinkLocalMulticast is the IPv4 link-local multicast address. 17 | IPv4LinkLocalMulticast = net.ParseIP("224.0.0.251") 18 | // IPv6LinkLocalMulticast is the IPv6 link-local multicast address. 19 | IPv6LinkLocalMulticast = net.ParseIP("ff02::fb") 20 | 21 | // AddrIPv4LinkLocalMulticast is the IPv4 link-local multicast UDP address. 22 | AddrIPv4LinkLocalMulticast = &net.UDPAddr{ 23 | IP: IPv4LinkLocalMulticast, 24 | Port: 5353, 25 | } 26 | 27 | // AddrIPv6LinkLocalMulticast is the IPv5 link-local multicast UDP address. 28 | AddrIPv6LinkLocalMulticast = &net.UDPAddr{ 29 | IP: IPv6LinkLocalMulticast, 30 | Port: 5353, 31 | } 32 | 33 | // TTLDefault is the default time-to-live for mDNS resource records. 34 | TTLDefault uint32 = 75 * 6 35 | 36 | // TTLHostname is the default time-to-livefor mDNS hostname records. 37 | TTLHostname uint32 = 120 38 | ) 39 | 40 | // Query is a mDNS query 41 | type Query struct { 42 | msg *dns.Msg // The query message 43 | iface *net.Interface // The network interface to which the message is sent 44 | } 45 | 46 | // IfaceName returns the name of the network interface where the request was received. 47 | // If the network interface is unknown, the string "?" is returned. 48 | func (q Query) IfaceName() string { 49 | if q.iface != nil { 50 | return q.iface.Name 51 | } 52 | 53 | return "?" 54 | } 55 | 56 | // Response is a mDNS response 57 | type Response struct { 58 | msg *dns.Msg // The response message 59 | addr *net.UDPAddr // Is nil for multicast response 60 | iface *net.Interface // The network interface to which the message is sent 61 | } 62 | 63 | // Request represents an incoming mDNS message 64 | type Request struct { 65 | msg *dns.Msg // The message 66 | from *net.UDPAddr // The source addr of the message 67 | iface *net.Interface // The network interface from which the message was received 68 | } 69 | 70 | func (r Request) String() string { 71 | return fmt.Sprintf("%s@%s\n%v", r.from.IP, r.IfaceName(), r.msg) 72 | } 73 | 74 | // Raw returns the raw DNS maessage. 75 | func (r Request) Raw() *dns.Msg { 76 | return r.msg 77 | } 78 | 79 | // From returns the sender address. 80 | func (r Request) From() *net.UDPAddr { 81 | return r.from 82 | } 83 | 84 | // IfaceName returns the name of the network interface where the request was received. 85 | // If the network interface is unknown, the string "?" is returned. 86 | func (r Request) IfaceName() string { 87 | if r.iface != nil { 88 | return r.iface.Name 89 | } 90 | 91 | return "?" 92 | } 93 | 94 | // IsLegacyUnicast returns `true` if the request came from a non-5353 port and thus, the resolver is a simple resolver by https://datatracker.ietf.org/doc/html/rfc6762#section-6.7). 95 | // For legacy unicast requests, the response needs to look like a normal unicast DNS response. 96 | func isLegacyUnicastSource(addr *net.UDPAddr) bool { 97 | return addr != nil && addr.Port != 5353 98 | } 99 | 100 | // MDNSConn represents a mDNS connection. It encapsulates an IPv4 and IPv6 UDP connection. 101 | type MDNSConn interface { 102 | // SendQuery sends a mDNS query. 103 | SendQuery(q *Query) error 104 | 105 | // SendResponse sends a mDNS response 106 | SendResponse(resp *Response) error 107 | 108 | // Read returns a channel which receives mDNS messages 109 | Read(ctx context.Context) <-chan *Request 110 | 111 | // Clears the connection buffer 112 | Drain(ctx context.Context) 113 | 114 | // Close closes the connection 115 | Close() 116 | } 117 | 118 | type mdnsConn struct { 119 | ipv4 *ipv4.PacketConn 120 | ipv6 *ipv6.PacketConn 121 | udpConn4 *net.UDPConn 122 | udpConn6 *net.UDPConn 123 | ch chan *Request 124 | } 125 | 126 | // NewMDNSConn returns a new mdns connection. 127 | func NewMDNSConn() (MDNSConn, error) { 128 | return newMDNSConn() 129 | } 130 | 131 | // SendQuery sends a query. 132 | func (c *mdnsConn) SendQuery(q *Query) error { 133 | return c.sendQuery(q.msg, q.iface) 134 | } 135 | 136 | // SendResponse sends a response. 137 | // The message is sent as unicast, if an receiver address is specified in the response. 138 | func (c *mdnsConn) SendResponse(resp *Response) error { 139 | if resp.addr != nil { 140 | return c.sendResponseTo(resp.msg, resp.iface, resp.addr) 141 | } 142 | 143 | return c.sendResponse(resp.msg, resp.iface) 144 | } 145 | 146 | // Read returns a channel, which receives mDNS requests. 147 | func (c *mdnsConn) Read(ctx context.Context) <-chan *Request { 148 | return c.read(ctx) 149 | } 150 | 151 | // Drain drains the incoming requests channel. 152 | func (c *mdnsConn) Drain(ctx context.Context) { 153 | log.Debug.Println("Draining connection") 154 | for { 155 | select { 156 | case req := <-c.Read(ctx): 157 | log.Debug.Println("Ignoring msg from", req.from.IP) 158 | default: 159 | return 160 | } 161 | } 162 | } 163 | 164 | // Close closes the mDNS connection. 165 | func (c *mdnsConn) Close() { 166 | c.close() 167 | } 168 | 169 | func newMDNSConn(ifs ...string) (*mdnsConn, error) { 170 | var errs []error 171 | var connIPv4 *ipv4.PacketConn 172 | var connIPv6 *ipv6.PacketConn 173 | 174 | conn4, err := net.ListenUDP("udp4", AddrIPv4LinkLocalMulticast) 175 | if err != nil { 176 | errs = append(errs, err) 177 | } 178 | 179 | connIPv4 = ipv4.NewPacketConn(conn4) 180 | if err := connIPv4.SetControlMessage(ipv4.FlagInterface, true); err != nil { 181 | log.Debug.Printf("IPv4 interface socket opt: %v", err) 182 | } 183 | // Enable multicast loopback to receive all sent data 184 | if err := connIPv4.SetMulticastLoopback(true); err != nil { 185 | log.Debug.Println("IPv4 set multicast loopback:", err) 186 | } 187 | // Set TTL to 255 (rfc6762) 188 | if err := connIPv4.SetTTL(255); err != nil { 189 | log.Debug.Println("IPv4 set TTL:", err) 190 | } 191 | if err := connIPv4.SetMulticastTTL(255); err != nil { 192 | log.Debug.Println("IPv4 set multicast TTL:", err) 193 | } 194 | 195 | for _, iface := range MulticastInterfaces(ifs...) { 196 | if err := connIPv4.JoinGroup(iface, &net.UDPAddr{IP: IPv4LinkLocalMulticast}); err != nil { 197 | log.Debug.Printf("Failed joining IPv4 %v: %v", iface.Name, err) 198 | } else { 199 | log.Debug.Printf("Joined IPv4 %v", iface.Name) 200 | } 201 | } 202 | 203 | conn6, err := net.ListenUDP("udp6", AddrIPv6LinkLocalMulticast) 204 | if err != nil { 205 | errs = append(errs, err) 206 | } 207 | connIPv6 = ipv6.NewPacketConn(conn6) 208 | if err := connIPv6.SetControlMessage(ipv6.FlagInterface, true); err != nil { 209 | log.Debug.Printf("IPv6 interface socket opt: %v", err) 210 | } 211 | // Enable multicast loopback to receive all sent data 212 | if err := connIPv6.SetMulticastLoopback(true); err != nil { 213 | log.Debug.Println("IPv6 set multicast loopback:", err) 214 | } 215 | // Set TTL to 255 (rfc6762) 216 | if err := connIPv6.SetHopLimit(255); err != nil { 217 | log.Debug.Println("IPv4 set TTL:", err) 218 | } 219 | if err := connIPv6.SetMulticastHopLimit(255); err != nil { 220 | log.Debug.Println("IPv4 set multicast TTL:", err) 221 | } 222 | for _, iface := range MulticastInterfaces(ifs...) { 223 | if err := connIPv6.JoinGroup(iface, &net.UDPAddr{IP: IPv6LinkLocalMulticast}); err != nil { 224 | log.Debug.Printf("Failed joining IPv6 %v: %v", iface.Name, err) 225 | } else { 226 | log.Debug.Printf("Joined IPv6 %v", iface.Name) 227 | } 228 | } 229 | 230 | if err := first(errs...); connIPv4 == nil && connIPv6 == nil { 231 | return nil, fmt.Errorf("Failed setting up UDP server: %v", err) 232 | } 233 | 234 | return &mdnsConn{ 235 | ipv4: connIPv4, 236 | ipv6: connIPv6, 237 | udpConn4: conn4, 238 | udpConn6: conn6, 239 | ch: make(chan *Request), 240 | }, nil 241 | } 242 | 243 | func (c *mdnsConn) close() { 244 | if c.ipv4 != nil { 245 | c.ipv4.Close() 246 | } 247 | 248 | if c.ipv6 != nil { 249 | c.ipv6.Close() 250 | } 251 | 252 | if c.udpConn4 != nil { 253 | c.udpConn4.Close() 254 | } 255 | 256 | if c.udpConn6 != nil { 257 | c.udpConn6.Close() 258 | } 259 | } 260 | 261 | func (c *mdnsConn) read(ctx context.Context) <-chan *Request { 262 | c.readInto(ctx, c.ch) 263 | return c.ch 264 | } 265 | 266 | func (c *mdnsConn) readInto(ctx context.Context, ch chan *Request) { 267 | 268 | isDone := func(ctx context.Context) bool { 269 | return ctx.Err() != nil 270 | } 271 | 272 | if c.ipv4 != nil { 273 | go func() { 274 | buf := make([]byte, 65536) 275 | for { 276 | if isDone(ctx) { 277 | return 278 | } 279 | 280 | n, cm, from, err := c.ipv4.ReadFrom(buf) 281 | if err != nil { 282 | continue 283 | } 284 | 285 | udpAddr, ok := from.(*net.UDPAddr) 286 | if !ok { 287 | log.Info.Println("dnssd: invalid source address") 288 | continue 289 | } 290 | 291 | var iface *net.Interface 292 | if cm != nil { 293 | iface, err = net.InterfaceByIndex(cm.IfIndex) 294 | if err != nil { 295 | continue 296 | } 297 | } else { 298 | //On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented. 299 | //ref https://pkg.go.dev/golang.org/x/net/ipv4#pkg-note-BUG 300 | iface, err = getInterfaceByIp(udpAddr.IP) 301 | if err != nil { 302 | continue 303 | } 304 | } 305 | 306 | if n > 0 { 307 | m := new(dns.Msg) 308 | if err := m.Unpack(buf); err == nil && !shouldIgnore(m) { 309 | ch <- &Request{m, udpAddr, iface} 310 | } 311 | } 312 | } 313 | }() 314 | } 315 | 316 | if c.ipv6 != nil { 317 | go func() { 318 | buf := make([]byte, 65536) 319 | for { 320 | if isDone(ctx) { 321 | return 322 | } 323 | 324 | n, cm, from, err := c.ipv6.ReadFrom(buf) 325 | if err != nil { 326 | continue 327 | } 328 | 329 | udpAddr, ok := from.(*net.UDPAddr) 330 | if !ok { 331 | log.Info.Println("dnssd: invalid source address") 332 | continue 333 | } 334 | 335 | var iface *net.Interface 336 | if cm != nil { 337 | iface, err = net.InterfaceByIndex(cm.IfIndex) 338 | if err != nil { 339 | continue 340 | } 341 | } else { 342 | //On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented. 343 | //ref https://pkg.go.dev/golang.org/x/net/ipv6#pkg-note-BUG 344 | //The zone specifies the scope of the literal IPv6 address as defined in RFC 4007. 345 | iface, err = net.InterfaceByName(udpAddr.Zone) 346 | if err != nil { 347 | continue 348 | } 349 | } 350 | 351 | if n > 0 { 352 | m := new(dns.Msg) 353 | if err := m.Unpack(buf); err == nil && !shouldIgnore(m) { 354 | ch <- &Request{m, udpAddr, iface} 355 | } 356 | } 357 | } 358 | }() 359 | } 360 | } 361 | 362 | func (c *mdnsConn) sendQuery(m *dns.Msg, iface *net.Interface) error { 363 | sanitizeQuery(m) 364 | 365 | return c.writeMsg(m, iface) 366 | } 367 | 368 | func (c *mdnsConn) sendResponse(m *dns.Msg, iface *net.Interface) error { 369 | sanitizeResponse(m) 370 | 371 | return c.writeMsg(m, iface) 372 | } 373 | 374 | func (c *mdnsConn) sendResponseTo(m *dns.Msg, iface *net.Interface, addr *net.UDPAddr) error { 375 | // Don't sanitize legacy unicast responses. 376 | if !isLegacyUnicastSource(addr) { 377 | sanitizeResponse(m) 378 | } 379 | 380 | return c.writeMsgTo(m, iface, addr) 381 | } 382 | 383 | func (c *mdnsConn) writeMsg(m *dns.Msg, iface *net.Interface) error { 384 | var err error 385 | if c.ipv4 != nil { 386 | err = c.writeMsgTo(m, iface, AddrIPv4LinkLocalMulticast) 387 | } 388 | 389 | if c.ipv6 != nil { 390 | err = c.writeMsgTo(m, iface, AddrIPv6LinkLocalMulticast) 391 | } 392 | 393 | return err 394 | } 395 | 396 | func (c *mdnsConn) writeMsgTo(m *dns.Msg, iface *net.Interface, addr *net.UDPAddr) error { 397 | // Don't sanitize legacy unicast responses. 398 | if !isLegacyUnicastSource(addr) { 399 | sanitizeMsg(m) 400 | } 401 | 402 | if c.ipv4 != nil && addr.IP.To4() != nil { 403 | if out, err := m.Pack(); err == nil { 404 | var ctrl *ipv4.ControlMessage 405 | if iface != nil { 406 | ctrl = &ipv4.ControlMessage{ 407 | IfIndex: iface.Index, 408 | } 409 | } 410 | c.ipv4.PacketConn.SetWriteDeadline(time.Now().Add(time.Second)) 411 | if _, err = c.ipv4.WriteTo(out, ctrl, addr); err != nil { 412 | return err 413 | } 414 | } 415 | } 416 | 417 | if c.ipv6 != nil && addr.IP.To4() == nil { 418 | if out, err := m.Pack(); err == nil { 419 | var ctrl *ipv6.ControlMessage 420 | if iface != nil { 421 | ctrl = &ipv6.ControlMessage{ 422 | IfIndex: iface.Index, 423 | } 424 | } 425 | c.ipv6.PacketConn.SetWriteDeadline(time.Now().Add(time.Second)) 426 | if _, err = c.ipv6.WriteTo(out, ctrl, addr); err != nil { 427 | return err 428 | } 429 | } 430 | } 431 | 432 | return nil 433 | } 434 | 435 | func shouldIgnore(m *dns.Msg) bool { 436 | if m.Opcode != 0 { 437 | return true 438 | } 439 | 440 | if m.Rcode != 0 { 441 | return true 442 | } 443 | 444 | return false 445 | } 446 | 447 | func sanitizeResponse(m *dns.Msg) { 448 | if m.Question != nil && len(m.Question) > 0 { 449 | log.Info.Println("dnssd: Multicast DNS responses MUST NOT contain any questions in the Question Section. (RFC6762 6)") 450 | m.Question = nil 451 | } 452 | 453 | if !m.Response { 454 | log.Info.Println("dnssd: In response messages the QR bit MUST be one (RFC6762 18.2)") 455 | m.Response = true 456 | } 457 | 458 | if !m.Authoritative { 459 | log.Info.Println("dnssd: AA Bit bit MUST be set to one in response messages (RFC6762 18.4)") 460 | m.Authoritative = true 461 | } 462 | 463 | if m.Truncated { 464 | log.Info.Println("dnssd: In multicast response messages, the TC bit MUST be zero on transmission. (RFC6762 18.5)") 465 | m.Truncated = false 466 | } 467 | } 468 | 469 | func sanitizeQuery(m *dns.Msg) { 470 | if m.Response { 471 | log.Info.Println("dnssd: In query messages the QR bit MUST be zero (RFC6762 18.2)") 472 | m.Response = false 473 | } 474 | 475 | if m.Authoritative { 476 | log.Info.Println("dnssd: AA Bit MUST be zero in query messages (RFC6762 18.4)") 477 | m.Authoritative = false 478 | } 479 | } 480 | 481 | func sanitizeMsg(m *dns.Msg) { 482 | if m.Opcode != 0 { 483 | log.Info.Println("dnssd: In both multicast query and multicast response messages, the OPCODE MUST be zero on transmission (RFC6762 18.3)") 484 | m.Opcode = 0 485 | } 486 | 487 | if m.RecursionDesired { 488 | log.Info.Println("dnssd: In both multicast query and multicast response messages, the Recursion Available bit MUST be zero on transmission. (RFC6762 18.7)") 489 | m.RecursionDesired = false 490 | } 491 | 492 | if m.Zero { 493 | log.Info.Println("dnssd: In both query and response messages, the Zero bit MUST be zero on transmission (RFC6762 18.8)") 494 | m.Zero = false 495 | } 496 | 497 | if m.AuthenticatedData { 498 | log.Info.Println("dnssd: In both multicast query and multicast response messages, the Authentic Data bit MUST be zero on transmission (RFC6762 18.9)") 499 | m.AuthenticatedData = false 500 | } 501 | 502 | if m.CheckingDisabled { 503 | log.Info.Println("dnssd: In both multicast query and multicast response messages, the Checking Disabled bit MUST be zero on transmission (RFC6762 18.10)") 504 | m.CheckingDisabled = false 505 | } 506 | 507 | if m.Rcode != 0 { 508 | log.Info.Println("dnssd: In both multicast query and multicast response messages, the Response Code MUST be zero on transmission. (RFC6762 18.11)") 509 | m.Rcode = 0 510 | } 511 | } 512 | 513 | func first(errs ...error) error { 514 | for _, err := range errs { 515 | if err != nil { 516 | return err 517 | } 518 | } 519 | 520 | return nil 521 | } 522 | 523 | // Sets the Top Bit of rrclass for all answer records (except PTR) to trigger a cache flush in the receivers. 524 | func setAnswerCacheFlushBit(msg *dns.Msg) { 525 | // From RFC6762 526 | // The most significant bit of the rrclass for a record in the Answer 527 | // Section of a response message is the Multicast DNS cache-flush bit 528 | // and is discussed in more detail below in Section 10.2, "Announcements 529 | // to Flush Outdated Cache Entries". 530 | for _, a := range msg.Answer { 531 | switch a.(type) { 532 | case *dns.PTR: 533 | continue 534 | default: 535 | a.Header().Class |= (1 << 15) 536 | } 537 | } 538 | } 539 | 540 | // Sets the Top Bit of class to indicate the unicast responses are preferred for this question. 541 | func setQuestionUnicast(q *dns.Question) { 542 | q.Qclass |= (1 << 15) 543 | } 544 | 545 | // Returns true if q requires unicast responses. 546 | func isUnicastQuestion(q dns.Question) bool { 547 | // From RFC6762 548 | // 18.12. Repurposing of Top Bit of qclass in Question Section 549 | // 550 | // In the Question Section of a Multicast DNS query, the top bit of the 551 | // qclass field is used to indicate that unicast responses are preferred 552 | // for this particular question. (See Section 5.4.) 553 | return q.Qclass&(1<<15) != 0 554 | } 555 | 556 | func getInterfaceByIp(ip net.IP) (*net.Interface, error) { 557 | interfaces, err := net.Interfaces() 558 | if err != nil { 559 | return nil, err 560 | } 561 | 562 | for _, iface := range interfaces { 563 | // check interface running flag 564 | if iface.Flags&net.FlagRunning != 0 { 565 | addrs, _ := iface.Addrs() 566 | for _, addr := range addrs { 567 | if ipnet, ok := addr.(*net.IPNet); ok && ipnet.Contains(ip) { 568 | return &iface, nil 569 | } 570 | } 571 | } 572 | } 573 | return nil, fmt.Errorf("could not find interface by %v", ip) 574 | } 575 | -------------------------------------------------------------------------------- /netlink_linux.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/brutella/dnssd/log" 8 | "github.com/vishvananda/netlink" 9 | ) 10 | 11 | // linkSubscribe subscribes to network interface updates (Ethernet cable is plugged in) via the netlink API. 12 | // For simplicity reasons, all network interfaces are re-announced, when network interfaces change. 13 | func (r *responder) linkSubscribe(ctx context.Context) { 14 | done := make(chan struct{}) 15 | defer close(done) 16 | 17 | ch := make(chan netlink.LinkUpdate, 1) 18 | if err := netlink.LinkSubscribe(ch, done); err != nil { 19 | return 20 | } 21 | 22 | log.Debug.Println("waiting for link updates...") 23 | 24 | for { 25 | select { 26 | case update := <-ch: 27 | iface, err := net.InterfaceByIndex(int(update.Index)) 28 | if err != nil { 29 | log.Info.Println(err) 30 | continue 31 | } 32 | 33 | if isInterfaceUpAndRunning(iface) { 34 | log.Debug.Printf("interface %s is up", iface.Name) 35 | 36 | addrs, err := iface.Addrs() 37 | if err == nil { 38 | log.Debug.Printf("addrs %+v", addrs) 39 | } 40 | } else { 41 | log.Debug.Printf("interface %s is down", iface.Name) 42 | } 43 | 44 | log.Debug.Println("announcing services after link update") 45 | r.mutex.Lock() 46 | r.announce(services(r.managed)) 47 | r.mutex.Unlock() 48 | case <-ctx.Done(): 49 | return 50 | } 51 | } 52 | } 53 | 54 | func isInterfaceUpAndRunning(iface *net.Interface) bool { 55 | return iface.Flags&net.FlagUp == net.FlagUp && iface.Flags&net.FlagRunning == net.FlagRunning 56 | } 57 | -------------------------------------------------------------------------------- /netlink_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package dnssd 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/brutella/dnssd/log" 9 | ) 10 | 11 | func (r *responder) linkSubscribe(context.Context) { 12 | log.Info.Println("dnssd: unable to wait for link updates") 13 | } 14 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "net" 7 | "sort" 8 | "strings" 9 | "time" 10 | 11 | "github.com/brutella/dnssd/log" 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | // ProbeService probes for the hostname and service instance name of srv. 16 | // If err == nil, the returned service is verified to be unique on the local network. 17 | func ProbeService(ctx context.Context, srv Service) (Service, error) { 18 | conn, err := newMDNSConn(srv.Ifaces...) 19 | 20 | if err != nil { 21 | return srv, err 22 | } 23 | 24 | defer conn.close() 25 | 26 | // After one minute of probing, if the Multicast DNS responder has been 27 | // unable to find any unused name, it should log an error (RFC6762 9) 28 | probeCtx, cancel := context.WithTimeout(ctx, 60*time.Second) 29 | defer cancel() 30 | 31 | // When ready to send its Multicast DNS probe packet(s) the host should 32 | // first wait for a short random delay time, uniformly distributed in 33 | // the range 0-250 ms. (RFC6762 8.1) 34 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 35 | delay := time.Duration(r.Intn(250)) * time.Millisecond 36 | log.Debug.Println("Probing delay", delay) 37 | time.Sleep(delay) 38 | 39 | return probeService(probeCtx, conn, srv, 250*time.Millisecond, false) 40 | } 41 | 42 | func ReprobeService(ctx context.Context, srv Service) (Service, error) { 43 | conn, err := newMDNSConn(srv.Ifaces...) 44 | 45 | if err != nil { 46 | return srv, err 47 | } 48 | 49 | defer conn.close() 50 | return probeService(ctx, conn, srv, 250*time.Millisecond, true) 51 | } 52 | 53 | func probeService(ctx context.Context, conn MDNSConn, srv Service, delay time.Duration, probeOnce bool) (s Service, e error) { 54 | candidate := srv.Copy() 55 | prevConflict := probeConflict{} 56 | 57 | // Keep track of the number of conflicts 58 | numHostConflicts := 0 59 | numNameConflicts := 0 60 | 61 | for i := 1; i <= 100; i++ { 62 | conflict, err := probe(ctx, conn, *candidate) 63 | if err != nil { 64 | e = err 65 | return 66 | } 67 | 68 | if conflict.hasNone() { 69 | s = *candidate 70 | return 71 | } 72 | 73 | candidate = candidate.Copy() 74 | 75 | if conflict.hostname && (prevConflict.hostname || probeOnce) { 76 | numHostConflicts++ 77 | candidate.Host = incrementHostname(candidate.Host, numHostConflicts+1) 78 | conflict.hostname = false 79 | } 80 | 81 | if conflict.serviceName && (prevConflict.serviceName || probeOnce) { 82 | numNameConflicts++ 83 | candidate.Name = incrementServiceName(candidate.Name, numNameConflicts+1) 84 | conflict.serviceName = false 85 | } 86 | 87 | prevConflict = conflict 88 | 89 | if conflict.hasAny() { 90 | // If the host finds that its own data is lexicographically earlier, 91 | // then it defers to the winning host by waiting one second, 92 | // and then begins probing for this record again. (RFC6762 8.2) 93 | log.Debug.Println("Increase wait time after receiving conflicting data") 94 | delay = 1 * time.Second 95 | } 96 | 97 | log.Debug.Println("Probing wait", delay) 98 | time.Sleep(delay) 99 | } 100 | 101 | return 102 | } 103 | 104 | func probe(ctx context.Context, conn MDNSConn, service Service) (conflict probeConflict, err error) { 105 | var queries []*Query 106 | for _, iface := range service.Interfaces() { 107 | queries = append(queries, probeQuery(service, iface)) 108 | } 109 | 110 | readCtx, readCancel := context.WithCancel(ctx) 111 | defer readCancel() 112 | 113 | // Multicast DNS responses received *before* the first probe packet is sent 114 | // MUST be silently ignored. (RFC6762 8.1) 115 | conn.Drain(readCtx) 116 | ch := conn.Read(readCtx) 117 | 118 | queryTime := time.After(1 * time.Millisecond) 119 | queriesCount := 1 120 | 121 | for { 122 | select { 123 | case rsp := <-ch: 124 | 125 | if rsp.iface == nil { 126 | continue 127 | } 128 | 129 | reqAs, reqAAAAs, reqSRVs := splitRecords(filterRecords(rsp, &service)) 130 | 131 | as := A(service, rsp.iface) 132 | aaaas := AAAA(service, rsp.iface) 133 | 134 | if len(reqAs) > 0 && len(as) > 0 && areDenyingAs(reqAs, as) { 135 | log.Debug.Printf("%v:%d@%s denies A\n", rsp.from.IP, rsp.from.Port, rsp.IfaceName()) 136 | log.Debug.Println(reqAs) 137 | log.Debug.Println(as) 138 | conflict.hostname = true 139 | } 140 | 141 | if len(reqAAAAs) > 0 && len(aaaas) > 0 && areDenyingAAAAs(reqAAAAs, aaaas) { 142 | log.Debug.Printf("%v:%d@%s denies AAAA\n", rsp.from.IP, rsp.from.Port, rsp.IfaceName()) 143 | log.Debug.Println(reqAAAAs) 144 | log.Debug.Println(aaaas) 145 | conflict.hostname = true 146 | } 147 | 148 | // If the service instance name is already taken from another host, 149 | // we have a service instance name conflict 150 | conflict.serviceName = len(reqSRVs) > 0 151 | 152 | case <-ctx.Done(): 153 | err = ctx.Err() 154 | return 155 | 156 | case <-queryTime: 157 | // Stop on conflict 158 | if conflict.hasAny() { 159 | return conflict, err 160 | } 161 | 162 | // Stop after 3 probe queries 163 | if queriesCount > 3 { 164 | return 165 | } 166 | 167 | queriesCount++ 168 | for _, q := range queries { 169 | log.Debug.Println("Sending probe", q.iface.Name, q.msg) 170 | if err := conn.SendQuery(q); err != nil { 171 | log.Debug.Println("Sending probe err:", err) 172 | } 173 | } 174 | 175 | delay := 250 * time.Millisecond 176 | log.Debug.Println("Waiting for conflicting data", delay) 177 | queryTime = time.After(delay) 178 | } 179 | } 180 | } 181 | 182 | func probeQuery(service Service, iface *net.Interface) *Query { 183 | msg := new(dns.Msg) 184 | 185 | instanceQ := dns.Question{ 186 | Name: service.EscapedServiceInstanceName(), 187 | Qtype: dns.TypeANY, 188 | Qclass: dns.ClassINET, 189 | } 190 | 191 | hostQ := dns.Question{ 192 | Name: service.Hostname(), 193 | Qtype: dns.TypeANY, 194 | Qclass: dns.ClassINET, 195 | } 196 | 197 | setQuestionUnicast(&instanceQ) 198 | setQuestionUnicast(&hostQ) 199 | 200 | msg.Question = []dns.Question{instanceQ, hostQ} 201 | 202 | srv := SRV(service) 203 | as := A(service, iface) 204 | aaaas := AAAA(service, iface) 205 | 206 | var authority = []dns.RR{srv} 207 | for _, a := range as { 208 | authority = append(authority, a) 209 | } 210 | for _, aaaa := range aaaas { 211 | authority = append(authority, aaaa) 212 | } 213 | msg.Ns = authority 214 | 215 | return &Query{msg: msg, iface: iface} 216 | } 217 | 218 | type probeConflict struct { 219 | hostname bool 220 | serviceName bool 221 | } 222 | 223 | func (pr probeConflict) hasNone() bool { 224 | return !pr.hostname && !pr.serviceName 225 | } 226 | 227 | func (pr probeConflict) hasAny() bool { 228 | return pr.hostname || pr.serviceName 229 | } 230 | 231 | func isDenyingA(this *dns.A, that *dns.A) bool { 232 | if strings.EqualFold(this.Hdr.Name, that.Hdr.Name) { 233 | log.Debug.Println("Same hosts") 234 | 235 | if !isValidRR(this) { 236 | log.Debug.Println("Invalid record produces conflict") 237 | return true 238 | } 239 | 240 | switch compareIP(this.A.To4(), that.A.To4()) { 241 | case -1: 242 | log.Debug.Println("Lexicographical earlier") 243 | case 1: 244 | log.Debug.Println("Lexicographical later") 245 | return true 246 | default: 247 | log.Debug.Println("No conflict") 248 | } 249 | } 250 | 251 | return false 252 | } 253 | 254 | // isDenyingAAAA returns true if this denies that. 255 | func isDenyingAAAA(this *dns.AAAA, that *dns.AAAA) bool { 256 | if strings.EqualFold(this.Hdr.Name, that.Hdr.Name) { 257 | log.Debug.Println("Same hosts") 258 | if !isValidRR(this) { 259 | log.Debug.Println("Invalid record produces conflict") 260 | return true 261 | } 262 | 263 | switch compareIP(this.AAAA.To16(), that.AAAA.To16()) { 264 | case -1: 265 | log.Debug.Println("Lexicographical earlier") 266 | case 1: 267 | log.Debug.Println("Lexicographical later") 268 | return true 269 | default: 270 | log.Debug.Println("No conflict") 271 | } 272 | } 273 | 274 | return false 275 | } 276 | 277 | // areDenyingAs returns true if this and that are denying each other. 278 | func areDenyingAs(this []*dns.A, that []*dns.A) bool { 279 | if len(this) != len(that) { 280 | log.Debug.Printf("A: different number of records is a conflict (%d != %d)\n", len(this), len(that)) 281 | return true 282 | } 283 | 284 | sort.Sort(byAIP(this)) 285 | sort.Sort(byAIP(that)) 286 | 287 | for i, ti := range this { 288 | ta := that[i] 289 | if isDenyingA(ti, ta) { 290 | return true 291 | } 292 | } 293 | 294 | log.Debug.Println("A: same records are no conflict") 295 | return false 296 | } 297 | 298 | func areDenyingAAAAs(this []*dns.AAAA, that []*dns.AAAA) bool { 299 | if len(this) != len(that) { 300 | log.Debug.Printf("AAAA: different number of records is a conflict (%d != %d)\n", len(this), len(that)) 301 | return true 302 | } 303 | 304 | sort.Sort(byAAAAIP(this)) 305 | sort.Sort(byAAAAIP(that)) 306 | 307 | for i, ti := range this { 308 | ta := that[i] 309 | if isDenyingAAAA(ti, ta) { 310 | return true 311 | } 312 | } 313 | 314 | log.Debug.Println("AAAA: same records are no conflict") 315 | return false 316 | } 317 | 318 | type byAIP []*dns.A 319 | 320 | func (a byAIP) Len() int { return len(a) } 321 | func (a byAIP) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 322 | func (a byAIP) Less(i, j int) bool { 323 | return strings.Compare(a[i].A.To4().String(), a[j].A.To4().String()) == -1 324 | } 325 | 326 | type byAAAAIP []*dns.AAAA 327 | 328 | func (a byAAAAIP) Len() int { return len(a) } 329 | func (a byAAAAIP) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 330 | func (a byAAAAIP) Less(i, j int) bool { 331 | return strings.Compare(a[i].AAAA.To16().String(), a[j].AAAA.To16().String()) == -1 332 | } 333 | 334 | // isDenyingSRV returns true if this denies that. 335 | func isDenyingSRV(this *dns.SRV, that *dns.SRV) bool { 336 | if strings.EqualFold(this.Hdr.Name, that.Hdr.Name) { 337 | log.Debug.Println("Same SRV") 338 | if !isValidRR(this) { 339 | log.Debug.Println("Invalid record produces conflict") 340 | return true 341 | } 342 | 343 | switch compareSRV(this, that) { 344 | case -1: 345 | log.Debug.Println("Lexicographical earlier") 346 | case 1: 347 | log.Debug.Println("Lexicographical later") 348 | return true 349 | default: 350 | log.Debug.Println("No conflict") 351 | } 352 | } 353 | 354 | return false 355 | } 356 | 357 | func isValidRR(rr dns.RR) bool { 358 | switch r := rr.(type) { 359 | case *dns.A: 360 | return !net.IPv4zero.Equal(r.A) 361 | case *dns.AAAA: 362 | return !net.IPv6zero.Equal(r.AAAA) 363 | case *dns.SRV: 364 | return len(r.Target) > 0 && r.Port != 0 365 | default: 366 | } 367 | 368 | return true 369 | } 370 | 371 | func compareIP(this net.IP, that net.IP) int { 372 | count := len(this) 373 | if count > len(that) { 374 | count = len(that) 375 | } 376 | 377 | for i := 0; i < count; i++ { 378 | if this[i] < that[i] { 379 | return -1 380 | } else if this[i] > that[i] { 381 | return 1 382 | } 383 | } 384 | 385 | if len(this) < len(that) { 386 | return -1 387 | } else if len(this) > len(that) { 388 | return 1 389 | } 390 | return 0 391 | } 392 | 393 | func compareSRV(this *dns.SRV, that *dns.SRV) int { 394 | if this.Priority < that.Priority { 395 | return -1 396 | } else if this.Priority > that.Priority { 397 | return 1 398 | } 399 | 400 | if this.Weight < that.Weight { 401 | return -1 402 | } else if this.Weight > that.Weight { 403 | return 1 404 | } 405 | 406 | if this.Port < that.Port { 407 | return -1 408 | } else if this.Port > that.Port { 409 | return 1 410 | } 411 | 412 | return strings.Compare(this.Target, that.Target) 413 | } 414 | -------------------------------------------------------------------------------- /probe_test.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | 6 | "context" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var testAddr = net.UDPAddr{ 13 | IP: net.IP{}, 14 | Port: 1234, 15 | Zone: "", 16 | } 17 | 18 | var testIface = &net.Interface{ 19 | Index: 0, 20 | MTU: 0, 21 | Name: "lo0", 22 | HardwareAddr: []byte{}, 23 | Flags: net.FlagUp, 24 | } 25 | 26 | type testConn struct { 27 | read chan *Request 28 | 29 | in chan *dns.Msg 30 | out chan *dns.Msg 31 | } 32 | 33 | func newTestConn() *testConn { 34 | c := &testConn{ 35 | read: make(chan *Request), 36 | in: make(chan *dns.Msg), 37 | out: make(chan *dns.Msg), 38 | } 39 | 40 | return c 41 | } 42 | 43 | func (c *testConn) SendQuery(q *Query) error { 44 | go func() { 45 | c.out <- q.msg 46 | }() 47 | return nil 48 | } 49 | 50 | func (c *testConn) SendResponse(resp *Response) error { 51 | go func() { 52 | c.out <- resp.msg 53 | }() 54 | 55 | return nil 56 | } 57 | 58 | func (c *testConn) Read(ctx context.Context) <-chan *Request { 59 | go c.start(ctx) 60 | return c.read 61 | } 62 | 63 | func (c *testConn) Drain(ctx context.Context) {} 64 | 65 | func (c *testConn) Close() {} 66 | 67 | func (c *testConn) start(ctx context.Context) { 68 | for { 69 | select { 70 | case msg := <-c.in: 71 | req := &Request{msg: msg, from: &testAddr, iface: testIface} 72 | c.read <- req 73 | case <-ctx.Done(): 74 | return 75 | } 76 | } 77 | } 78 | 79 | // TestProbing tests probing by using 2 services with the same 80 | // service instance name and host name.Once the first services 81 | // is announced, the probing for the second service should give 82 | func TestProbing(t *testing.T) { 83 | // log.Debug.Enable() 84 | testIface, _ = net.InterfaceByName("lo0") 85 | if testIface == nil { 86 | testIface, _ = net.InterfaceByName("lo") 87 | } 88 | if testIface == nil { 89 | t.Fatal("can not find the local interface") 90 | } 91 | 92 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 93 | 94 | conn := newTestConn() 95 | otherConn := newTestConn() 96 | conn.in = otherConn.out 97 | conn.out = otherConn.in 98 | 99 | cfg := Config{ 100 | Name: "My Service", 101 | Type: "_hap._tcp", 102 | Host: "My Computer", 103 | Port: 12334, 104 | Ifaces: []string{testIface.Name}, 105 | } 106 | srv, err := NewService(cfg) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | srv.ifaceIPs = map[string][]net.IP{ 111 | testIface.Name: []net.IP{net.IP{192, 168, 0, 122}}, 112 | } 113 | 114 | r := newResponder(otherConn) 115 | go func() { 116 | rcfg := cfg.Copy() 117 | rsrv, err := NewService(rcfg) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | rsrv.ifaceIPs = map[string][]net.IP{ 122 | testIface.Name: []net.IP{net.IP{192, 168, 0, 123}}, 123 | } 124 | 125 | rctx, rcancel := context.WithCancel(ctx) 126 | defer rcancel() 127 | 128 | r.addManaged(rsrv) 129 | r.Respond(rctx) 130 | }() 131 | 132 | resolved, err := probeService(ctx, conn, srv, 500*time.Millisecond, true) 133 | 134 | if x := err; x != nil { 135 | t.Fatal(x) 136 | } 137 | 138 | if is, want := resolved.Host, "My-Computer-2"; is != want { 139 | t.Fatalf("is=%v want=%v", is, want) 140 | } 141 | 142 | if is, want := resolved.Name, "My Service (2)"; is != want { 143 | t.Fatalf("is=%v want=%v", is, want) 144 | } 145 | 146 | cancel() 147 | } 148 | 149 | func TestIsLexicographicLater(t *testing.T) { 150 | this := &dns.A{ 151 | Hdr: dns.RR_Header{ 152 | Name: "MyPrinter.local.", 153 | Rrtype: dns.TypeA, 154 | Class: dns.ClassINET, 155 | Ttl: TTLHostname, 156 | }, 157 | A: net.ParseIP("169.254.99.200"), 158 | } 159 | 160 | that := &dns.A{ 161 | Hdr: dns.RR_Header{ 162 | Name: "MyPrinter.local.", 163 | Rrtype: dns.TypeA, 164 | Class: dns.ClassINET, 165 | Ttl: TTLHostname, 166 | }, 167 | A: net.ParseIP("169.254.200.50"), 168 | } 169 | 170 | if is, want := compareIP(this.A.To4(), that.A.To4()), -1; is != want { 171 | t.Fatalf("is=%v want=%v", is, want) 172 | } 173 | 174 | if is, want := compareIP(that.A.To4(), this.A.To4()), 1; is != want { 175 | t.Fatalf("is=%v want=%v", is, want) 176 | } 177 | } 178 | 179 | func TestDenyingAs(t *testing.T) { 180 | tests := []struct { 181 | This []*dns.A 182 | That []*dns.A 183 | Result bool 184 | }{ 185 | { 186 | This: []*dns.A{ 187 | &dns.A{ 188 | Hdr: dns.RR_Header{ 189 | Name: "MyPrinter.local.", 190 | Rrtype: dns.TypeA, 191 | Class: dns.ClassINET, 192 | Ttl: TTLHostname, 193 | }, 194 | A: net.ParseIP("169.254.99.200"), 195 | }, 196 | }, 197 | That: []*dns.A{ 198 | &dns.A{ 199 | Hdr: dns.RR_Header{ 200 | Name: "MyPrinter.local.", 201 | Rrtype: dns.TypeA, 202 | Class: dns.ClassINET, 203 | Ttl: TTLHostname, 204 | }, 205 | A: net.ParseIP("169.254.99.200"), 206 | }, 207 | }, 208 | Result: false, 209 | }, 210 | { 211 | This: []*dns.A{ 212 | &dns.A{ 213 | Hdr: dns.RR_Header{ 214 | Name: "MyPrinter.local.", 215 | Rrtype: dns.TypeA, 216 | Class: dns.ClassINET, 217 | Ttl: TTLHostname, 218 | }, 219 | A: net.ParseIP("169.254.99.200"), 220 | }, 221 | }, 222 | That: []*dns.A{}, 223 | Result: true, 224 | }, 225 | { 226 | This: []*dns.A{}, 227 | That: []*dns.A{ 228 | &dns.A{ 229 | Hdr: dns.RR_Header{ 230 | Name: "MyPrinter.local.", 231 | Rrtype: dns.TypeA, 232 | Class: dns.ClassINET, 233 | Ttl: TTLHostname, 234 | }, 235 | A: net.ParseIP("169.254.99.200"), 236 | }, 237 | }, 238 | Result: true, 239 | }, 240 | { 241 | This: []*dns.A{}, 242 | That: []*dns.A{}, 243 | Result: false, 244 | }, 245 | } 246 | 247 | for _, test := range tests { 248 | if is, want := areDenyingAs(test.This, test.That), test.Result; is != want { 249 | t.Fatalf("%v != %v is=%v want=%v", test.This, test.That, is, want) 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /resolve.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/brutella/dnssd/log" 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // LookupInstance resolves a service by its service instance name. 11 | func LookupInstance(ctx context.Context, instance string) (Service, error) { 12 | var srv Service 13 | 14 | conn, err := NewMDNSConn() 15 | if err != nil { 16 | return srv, err 17 | } 18 | 19 | return lookupInstance(ctx, instance, conn) 20 | } 21 | 22 | func lookupInstance(ctx context.Context, instance string, conn MDNSConn) (srv Service, err error) { 23 | var cache = NewCache() 24 | 25 | m := new(dns.Msg) 26 | 27 | srvQ := dns.Question{ 28 | Name: instance, 29 | Qtype: dns.TypeSRV, 30 | Qclass: dns.ClassINET, 31 | } 32 | txtQ := dns.Question{ 33 | Name: instance, 34 | Qtype: dns.TypeTXT, 35 | Qclass: dns.ClassINET, 36 | } 37 | setQuestionUnicast(&srvQ) 38 | setQuestionUnicast(&txtQ) 39 | 40 | m.Question = []dns.Question{srvQ, txtQ} 41 | 42 | readCtx, readCancel := context.WithCancel(ctx) 43 | defer readCancel() 44 | 45 | ch := conn.Read(readCtx) 46 | 47 | qs := make(chan *Query) 48 | go func() { 49 | for _, iface := range MulticastInterfaces() { 50 | iface := iface 51 | q := &Query{msg: m, iface: iface} 52 | qs <- q 53 | } 54 | }() 55 | 56 | for { 57 | select { 58 | case q := <-qs: 59 | if err := conn.SendQuery(q); err != nil { 60 | log.Info.Println("dnssd:", err) 61 | } 62 | case req := <-ch: 63 | cache.UpdateFrom(req) 64 | if s, ok := cache.services[instance]; ok { 65 | srv = *s 66 | return 67 | } 68 | case <-ctx.Done(): 69 | err = ctx.Err() 70 | return 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /responder.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/brutella/dnssd/log" 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | type ReadFunc func(*Request) 17 | 18 | // Responder represents a mDNS responder. 19 | type Responder interface { 20 | // Add adds a service to the responder. 21 | // Use the returned service handle to update service properties. 22 | Add(srv Service) (ServiceHandle, error) 23 | 24 | // Remove removes the service associated with the service handle from the responder. 25 | Remove(srv ServiceHandle) 26 | 27 | // Respond makes the receiver announcing and managing services. 28 | Respond(ctx context.Context) error 29 | 30 | // Debug calls a function for every dns request the responder receives. 31 | Debug(ctx context.Context, fn ReadFunc) 32 | } 33 | 34 | type responder struct { 35 | isRunning bool 36 | 37 | conn MDNSConn 38 | unmanaged []*serviceHandle 39 | managed []*serviceHandle 40 | 41 | mutex *sync.Mutex 42 | truncated *Request 43 | random *rand.Rand 44 | upIfaces []string 45 | } 46 | 47 | // NewResponder returns a new mDNS responder. 48 | func NewResponder() (Responder, error) { 49 | conn, err := newMDNSConn() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return newResponder(conn), nil 55 | } 56 | 57 | func newResponder(conn MDNSConn) *responder { 58 | return &responder{ 59 | isRunning: false, 60 | conn: conn, 61 | unmanaged: []*serviceHandle{}, 62 | managed: []*serviceHandle{}, 63 | mutex: &sync.Mutex{}, 64 | random: rand.New(rand.NewSource(time.Now().UnixNano())), 65 | upIfaces: []string{}, 66 | } 67 | } 68 | 69 | func (r *responder) Remove(h ServiceHandle) { 70 | r.mutex.Lock() 71 | defer r.mutex.Unlock() 72 | for i, s := range r.managed { 73 | if h == s { 74 | handle := h.(*serviceHandle) 75 | r.unannounce([]*Service{handle.service}) 76 | r.managed = append(r.managed[:i], r.managed[i+1:]...) 77 | return 78 | } 79 | } 80 | } 81 | 82 | func (r *responder) Add(srv Service) (ServiceHandle, error) { 83 | r.mutex.Lock() 84 | defer r.mutex.Unlock() 85 | 86 | if r.isRunning { 87 | ctx, cancel := context.WithCancel(context.TODO()) 88 | defer cancel() 89 | 90 | srv, err := r.register(ctx, srv) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return r.addManaged(srv), nil 96 | } 97 | 98 | return r.addUnmanaged(srv), nil 99 | } 100 | 101 | func (r *responder) Respond(ctx context.Context) error { 102 | r.mutex.Lock() 103 | err := func() error { 104 | r.isRunning = true 105 | for _, h := range r.unmanaged { 106 | log.Debug.Println(h.service) 107 | srv, err := r.register(ctx, *h.service) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | h.service = &srv 113 | r.managed = append(r.managed, h) 114 | } 115 | r.unmanaged = []*serviceHandle{} 116 | return nil 117 | }() 118 | r.mutex.Unlock() 119 | 120 | if err != nil { 121 | return err 122 | } 123 | 124 | go r.linkSubscribe(ctx) 125 | 126 | return r.respond(ctx) 127 | } 128 | 129 | // announce sends announcement messages including all services. 130 | func (r *responder) announce(services []*Service) { 131 | for _, service := range services { 132 | for _, iface := range service.Interfaces() { 133 | service, iface := service, iface 134 | go r.announceAtInterface(service, iface) 135 | } 136 | } 137 | } 138 | 139 | func (r *responder) announceAtInterface(service *Service, iface *net.Interface) { 140 | ips := service.IPsAtInterface(iface) 141 | if len(ips) == 0 { 142 | log.Debug.Printf("No IPs for service %s at %s\n", service.ServiceInstanceName(), iface.Name) 143 | return 144 | } 145 | 146 | var answer []dns.RR 147 | answer = append(answer, SRV(*service)) 148 | answer = append(answer, PTR(*service)) 149 | answer = append(answer, TXT(*service)) 150 | answer = append(answer, aOrAaaaFilter(service, iface)...) 151 | 152 | msg := new(dns.Msg) 153 | msg.Answer = answer 154 | msg.Response = true 155 | msg.Authoritative = true 156 | 157 | setAnswerCacheFlushBit(msg) 158 | 159 | resp := &Response{msg: msg, iface: iface} 160 | 161 | log.Debug.Println("Sending 1st announcement", msg) 162 | if err := r.conn.SendResponse(resp); err != nil { 163 | log.Debug.Println("1st announcement:", err) 164 | } 165 | time.Sleep(1 * time.Second) 166 | log.Debug.Println("Sending 2nd announcement", msg) 167 | if err := r.conn.SendResponse(resp); err != nil { 168 | log.Debug.Println("2nd announcement:", err) 169 | } 170 | } 171 | 172 | func (r *responder) register(ctx context.Context, srv Service) (Service, error) { 173 | if !r.isRunning { 174 | return srv, fmt.Errorf("cannot register service when responder is not responding") 175 | } 176 | 177 | log.Debug.Printf("Probing for host %s and service %s…\n", srv.Hostname(), srv.ServiceInstanceName()) 178 | probed, err := ProbeService(ctx, srv) 179 | if err != nil { 180 | return srv, err 181 | } 182 | 183 | srvs := []*Service{&probed} 184 | for _, h := range r.managed { 185 | srvs = append(srvs, h.service) 186 | } 187 | go r.announce(srvs) 188 | 189 | return probed, nil 190 | } 191 | 192 | func (r *responder) addManaged(srv Service) ServiceHandle { 193 | h := &serviceHandle{&srv} 194 | r.managed = append(r.managed, h) 195 | return h 196 | } 197 | 198 | func (r *responder) addUnmanaged(srv Service) ServiceHandle { 199 | h := &serviceHandle{&srv} 200 | r.unmanaged = append(r.unmanaged, h) 201 | return h 202 | } 203 | 204 | func (r *responder) respond(ctx context.Context) error { 205 | if !r.isRunning { 206 | return fmt.Errorf("isRunning should be true before calling respond()") 207 | } 208 | 209 | readCtx, readCancel := context.WithCancel(ctx) 210 | defer readCancel() 211 | ch := r.conn.Read(readCtx) 212 | 213 | for { 214 | select { 215 | case req := <-ch: 216 | r.mutex.Lock() 217 | r.handleRequest(req) 218 | r.mutex.Unlock() 219 | 220 | case <-ctx.Done(): 221 | r.unannounce(services(r.managed)) 222 | r.conn.Close() 223 | r.isRunning = false 224 | return ctx.Err() 225 | } 226 | } 227 | } 228 | 229 | func (r *responder) handleRequest(req *Request) { 230 | if len(r.managed) == 0 { 231 | // Ignore requests when no services are managed 232 | return 233 | } 234 | 235 | // If messages is truncated, we wait for the next message to come (RFC6762 18.5) 236 | if req.msg.Truncated { 237 | r.truncated = req 238 | log.Debug.Println("Waiting for additional answers...") 239 | return 240 | } 241 | 242 | // append request 243 | if r.truncated != nil && r.truncated.from.IP.Equal(req.from.IP) { 244 | log.Debug.Println("Add answers to truncated message") 245 | msgs := []*dns.Msg{r.truncated.msg, req.msg} 246 | r.truncated = nil 247 | req.msg = mergeMsgs(msgs) 248 | } 249 | 250 | if len(req.msg.Question) > 0 { 251 | r.handleQuery(req, services(r.managed)) 252 | } else { 253 | // Check if the request contains any conflicting records. 254 | conflicts := findConflicts(req, r.managed) 255 | for _, h := range conflicts { 256 | log.Debug.Println("Reprobe for", h.service) 257 | go r.reprobe(h) 258 | 259 | for i, m := range r.managed { 260 | if h == m { 261 | r.managed = append(r.managed[:i], r.managed[i+1:]...) 262 | break 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | func (r *responder) unannounce(services []*Service) { 270 | if len(services) == 0 { 271 | return 272 | } 273 | 274 | log.Debug.Println("Send goodbye for", services) 275 | 276 | // collect records per interface 277 | rrsByIfaceName := map[string][]dns.RR{} 278 | for _, srv := range services { 279 | rr := PTR(*srv) 280 | rr.Header().Ttl = 0 281 | for _, iface := range srv.Interfaces() { 282 | ips := srv.IPsAtInterface(iface) 283 | if len(ips) == 0 { 284 | continue 285 | } 286 | if rrs, ok := rrsByIfaceName[iface.Name]; ok { 287 | rrsByIfaceName[iface.Name] = append(rrs, rr) 288 | } else { 289 | rrsByIfaceName[iface.Name] = []dns.RR{rr} 290 | } 291 | } 292 | } 293 | 294 | // send on goodbye packet on every interface 295 | for name, rrs := range rrsByIfaceName { 296 | iface, err := net.InterfaceByName(name) 297 | if err != nil { 298 | log.Debug.Printf("Interface %s not found\n", name) 299 | continue 300 | } 301 | msg := new(dns.Msg) 302 | msg.Answer = rrs 303 | msg.Response = true 304 | msg.Authoritative = true 305 | resp := &Response{msg: msg, iface: iface} 306 | if err := r.conn.SendResponse(resp); err != nil { 307 | log.Debug.Println("1st goodbye:", err) 308 | } 309 | time.Sleep(250 * time.Millisecond) 310 | if err := r.conn.SendResponse(resp); err != nil { 311 | log.Debug.Println("2nd goodbye:", err) 312 | } 313 | } 314 | } 315 | 316 | func (r *responder) handleQuery(req *Request, services []*Service) { 317 | for _, q := range req.msg.Question { 318 | msgs := []*dns.Msg{} 319 | for _, srv := range services { 320 | log.Debug.Printf("%s tries to give response to question %v @%s\n", srv.ServiceInstanceName(), q, req.IfaceName()) 321 | if msg := r.handleQuestion(q, req, *srv); msg != nil { 322 | msgs = append(msgs, msg) 323 | } else { 324 | log.Debug.Println("No response") 325 | } 326 | } 327 | 328 | msg := mergeMsgs(msgs) 329 | msg.SetReply(req.msg) 330 | msg.Response = true 331 | msg.Authoritative = true 332 | 333 | // Legacy unicast response MUST be a conventional DNS server response (and thus, includes the question). 334 | if isLegacyUnicastSource(req.from) { 335 | msg.Question = []dns.Question{q} 336 | } else { 337 | msg.Question = nil 338 | } 339 | 340 | if len(msg.Answer) == 0 { 341 | log.Debug.Println("No answers") 342 | continue 343 | } 344 | 345 | if isUnicastQuestion(q) || isLegacyUnicastSource(req.from) { 346 | resp := &Response{msg: msg, addr: req.from, iface: req.iface} 347 | log.Debug.Printf("Send unicast response\n%v to %v\n", msg, resp.addr) 348 | if err := r.conn.SendResponse(resp); err != nil { 349 | log.Debug.Println(err) 350 | } 351 | } else { 352 | resp := &Response{msg: msg, iface: req.iface} 353 | log.Debug.Printf("Send multicast response\n%v\n", msg) 354 | if err := r.conn.SendResponse(resp); err != nil { 355 | log.Debug.Println(err) 356 | } 357 | } 358 | } 359 | } 360 | 361 | func (r *responder) reprobe(h *serviceHandle) { 362 | ctx, cancel := context.WithCancel(context.TODO()) 363 | defer cancel() 364 | 365 | probed, err := ReprobeService(ctx, *h.service) 366 | if err != nil { 367 | return 368 | } 369 | h.service = &probed 370 | 371 | r.mutex.Lock() 372 | managed := append(r.managed, h) 373 | r.managed = managed 374 | r.mutex.Unlock() 375 | 376 | log.Debug.Println("Reannouncing services", managed) 377 | go r.announce(services(managed)) 378 | } 379 | 380 | func (r *responder) handleQuestion(q dns.Question, req *Request, srv Service) *dns.Msg { 381 | resp := new(dns.Msg) 382 | switch strings.ToLower(q.Name) { 383 | case strings.ToLower(srv.ServiceName()): 384 | ptr := PTR(srv) 385 | resp.Answer = []dns.RR{ptr} 386 | 387 | extra := []dns.RR{SRV(srv), TXT(srv)} 388 | 389 | extra = append(extra, aOrAaaaFilter(&srv, req.iface)...) 390 | 391 | if nsec := NSEC(ptr, srv, req.iface); nsec != nil { 392 | extra = append(extra, nsec) 393 | } 394 | 395 | resp.Extra = extra 396 | 397 | // Wait 20-125 msec for shared resource responses 398 | delay := time.Duration(r.random.Intn(105)+20) * time.Millisecond 399 | log.Debug.Println("Shared record response wait", delay) 400 | time.Sleep(delay) 401 | 402 | case strings.ToLower(srv.EscapedServiceInstanceName()): 403 | resp.Answer = []dns.RR{SRV(srv), TXT(srv), PTR(srv)} 404 | 405 | var extra []dns.RR 406 | 407 | extra = append(extra, aOrAaaaFilter(&srv, req.iface)...) 408 | 409 | if nsec := NSEC(SRV(srv), srv, req.iface); nsec != nil { 410 | extra = append(extra, nsec) 411 | } 412 | 413 | resp.Extra = extra 414 | 415 | if !isLegacyUnicastSource(req.from) { 416 | // Set cache flush bit for non-shared records 417 | setAnswerCacheFlushBit(resp) 418 | } 419 | 420 | case strings.ToLower(srv.Hostname()): 421 | var answer []dns.RR 422 | 423 | answer = append(answer, aOrAaaaFilter(&srv, req.iface)...) 424 | 425 | resp.Answer = answer 426 | 427 | if nsec := NSEC(SRV(srv), srv, req.iface); nsec != nil { 428 | resp.Extra = []dns.RR{nsec} 429 | } 430 | 431 | if !isLegacyUnicastSource(req.from) { 432 | // Set cache flush bit for non-shared records 433 | setAnswerCacheFlushBit(resp) 434 | } 435 | 436 | case strings.ToLower(srv.ServicesMetaQueryName()): 437 | resp.Answer = []dns.RR{DNSSDServicesPTR(srv)} 438 | 439 | default: 440 | return nil 441 | } 442 | 443 | // Supress known answers 444 | resp.Answer = remove(req.msg.Answer, resp.Answer) 445 | 446 | resp.SetReply(req.msg) 447 | if !isLegacyUnicastSource(req.from) { 448 | resp.Question = nil 449 | } 450 | resp.Response = true 451 | resp.Authoritative = true 452 | 453 | return resp 454 | } 455 | 456 | func findConflicts(req *Request, hs []*serviceHandle) []*serviceHandle { 457 | var conflicts []*serviceHandle 458 | for _, h := range hs { 459 | if containsConflictingAnswers(req, h) { 460 | log.Debug.Println("Received conflicting record", req.msg) 461 | conflicts = append(conflicts, h) 462 | } 463 | } 464 | 465 | return conflicts 466 | } 467 | 468 | func services(hs []*serviceHandle) []*Service { 469 | var result []*Service 470 | for _, h := range hs { 471 | result = append(result, h.service) 472 | } 473 | 474 | return result 475 | } 476 | 477 | // containsConflictingAnswers return true, if the request contains A or AAAA records 478 | // which deny any A or AAAA records for a service. 479 | // 480 | // 2024-08-07 (mah) Because this method ignores SRV records, it should only 481 | // be used to check for conlict answers for a registered service and not for probing. 482 | // It is the responsibility of the probed service to find conflicting SRV records 483 | // and resolve them during probing. 484 | func containsConflictingAnswers(req *Request, handle *serviceHandle) bool { 485 | as := A(*handle.service, req.iface) 486 | aaaas := AAAA(*handle.service, req.iface) 487 | reqAs, reqAAAAs, _ := splitRecords(filterRecords(req, handle.service)) 488 | 489 | if len(reqAs) > 0 && areDenyingAs(reqAs, as) { 490 | log.Debug.Printf("%v != %v\n", reqAs, as) 491 | return true 492 | } 493 | 494 | if len(reqAAAAs) > 0 && areDenyingAAAAs(reqAAAAs, aaaas) { 495 | log.Debug.Printf("%v != %v\n", reqAAAAs, aaaas) 496 | return true 497 | } 498 | 499 | return false 500 | } 501 | 502 | func aOrAaaaFilter(service *Service, iface *net.Interface) []dns.RR { 503 | var result []dns.RR 504 | switch service.AdvertiseIPType { 505 | case IPv4: 506 | for _, a := range A(*service, iface) { 507 | result = append(result, a) 508 | } 509 | case IPv6: 510 | for _, aaaa := range AAAA(*service, iface) { 511 | result = append(result, aaaa) 512 | } 513 | default: 514 | for _, a := range A(*service, iface) { 515 | result = append(result, a) 516 | } 517 | for _, aaaa := range AAAA(*service, iface) { 518 | result = append(result, aaaa) 519 | } 520 | } 521 | return result 522 | } 523 | -------------------------------------------------------------------------------- /responder_debug.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func (r *responder) Debug(ctx context.Context, fn ReadFunc) { 8 | conn := r.conn.(*mdnsConn) 9 | 10 | readCtx, readCancel := context.WithCancel(ctx) 11 | defer readCancel() 12 | 13 | ch := conn.read(readCtx) 14 | 15 | for { 16 | select { 17 | case req := <-ch: 18 | fn(req) 19 | case <-ctx.Done(): 20 | return 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /responder_test.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "context" 5 | "github.com/brutella/dnssd/log" 6 | "github.com/miekg/dns" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestRemove(t *testing.T) { 13 | cfg := Config{ 14 | Name: "Test", 15 | Type: "_asdf._tcp", 16 | Port: 1234, 17 | } 18 | si, err := NewService(cfg) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | msg := new(dns.Msg) 24 | msg.Answer = []dns.RR{SRV(si), TXT(si)} 25 | 26 | answers := []dns.RR{SRV(si), TXT(si), PTR(si)} 27 | unknown := remove(msg.Answer, answers) 28 | 29 | if x := len(unknown); x != 1 { 30 | t.Fatal(x) 31 | } 32 | 33 | rr := unknown[0] 34 | if _, ok := rr.(*dns.PTR); !ok { 35 | t.Fatal("Invalid type", rr) 36 | } 37 | } 38 | 39 | func TestRegisterServiceWithExplicitIP(t *testing.T) { 40 | cfg := Config{ 41 | Host: "Computer", 42 | Name: "Test", 43 | Type: "_asdf._tcp", 44 | Domain: "local", 45 | Port: 12345, 46 | Ifaces: []string{"lo0"}, 47 | } 48 | sv, err := NewService(cfg) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | sv.ifaceIPs = map[string][]net.IP{ 53 | "lo0": {net.IP{192, 168, 0, 123}}, 54 | } 55 | 56 | conn := newTestConn() 57 | otherConn := newTestConn() 58 | conn.in = otherConn.out 59 | conn.out = otherConn.in 60 | 61 | ctx, cancel := context.WithCancel(context.Background()) 62 | t.Run("resolver", func(t *testing.T) { 63 | t.Parallel() 64 | 65 | lookupCtx, lookupCancel := context.WithTimeout(ctx, 5*time.Second) 66 | 67 | defer lookupCancel() 68 | defer cancel() 69 | 70 | srv, err := lookupInstance(lookupCtx, "Test._asdf._tcp.local.", otherConn) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | if is, want := srv.Name, "Test"; is != want { 76 | t.Fatalf("%v != %v", is, want) 77 | } 78 | 79 | if is, want := srv.Type, "_asdf._tcp"; is != want { 80 | t.Fatalf("%v != %v", is, want) 81 | } 82 | 83 | if is, want := srv.Host, "Computer"; is != want { 84 | t.Fatalf("%v != %v", is, want) 85 | } 86 | 87 | ips := srv.IPsAtInterface(&net.Interface{Name: "lo0"}) 88 | if is, want := len(ips), 1; is != want { 89 | t.Fatalf("%v != %v", is, want) 90 | } 91 | 92 | if is, want := ips[0].String(), "192.168.0.123"; is != want { 93 | t.Fatalf("%v != %v", is, want) 94 | } 95 | }) 96 | 97 | t.Run("responder", func(t *testing.T) { 98 | t.Parallel() 99 | 100 | r := newResponder(conn) 101 | r.addManaged(sv) // don't probe 102 | r.Respond(ctx) 103 | }) 104 | } 105 | 106 | type expectedIP struct { 107 | advType IPType 108 | expected []net.IP 109 | } 110 | 111 | func TestRegisterServiceWithSpecifiedAdvertisedIP(t *testing.T) { 112 | log.Debug.Enable() 113 | 114 | v4 := net.IP{192, 168, 0, 123} 115 | v6 := net.ParseIP("fe80::1") 116 | 117 | var expectedIPs = map[string]expectedIP{ 118 | "v4 only": {IPv4, []net.IP{v4}}, 119 | "v6 only": {IPv6, []net.IP{v6}}, 120 | "both / unspecified": {IPType(0), []net.IP{v4, v6}}, 121 | } 122 | 123 | for name, expected := range expectedIPs { 124 | t.Run(name, func(t *testing.T) { 125 | cfg := Config{ 126 | Host: "Computer", 127 | Name: "Test", 128 | Type: "_asdf._tcp", 129 | Domain: "local", 130 | Port: 12345, 131 | Ifaces: []string{"lo0"}, 132 | AdvertiseIPType: expected.advType, 133 | } 134 | sv, err := NewService(cfg) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | sv.ifaceIPs = map[string][]net.IP{ 139 | "lo0": {v4, v6}, 140 | } 141 | 142 | conn := newTestConn() 143 | otherConn := newTestConn() 144 | conn.in = otherConn.out 145 | conn.out = otherConn.in 146 | 147 | ctx, cancel := context.WithCancel(context.Background()) 148 | t.Run("resolver", func(t *testing.T) { 149 | t.Parallel() 150 | 151 | lookupCtx, lookupCancel := context.WithTimeout(ctx, 5*time.Second) 152 | 153 | defer lookupCancel() 154 | defer cancel() 155 | 156 | srv, err := lookupInstance(lookupCtx, "Test._asdf._tcp.local.", otherConn) 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | if is, want := srv.Name, "Test"; is != want { 162 | t.Fatalf("%v != %v", is, want) 163 | } 164 | 165 | if is, want := srv.Type, "_asdf._tcp"; is != want { 166 | t.Fatalf("%v != %v", is, want) 167 | } 168 | 169 | if is, want := srv.Host, "Computer"; is != want { 170 | t.Fatalf("%v != %v", is, want) 171 | } 172 | 173 | ips := srv.IPsAtInterface(&net.Interface{Name: "lo0"}) 174 | if is, want := len(ips), len(expected.expected); is != want { 175 | t.Fatalf("%v != %v", is, want) 176 | } 177 | 178 | for i, ip := range ips { // this should always be the same order as a records are processed before aaaa records 179 | if is, want := ip, expected.expected[i]; !is.Equal(want) { 180 | t.Fatalf("%v != %v", is, want) 181 | } 182 | } 183 | }) 184 | 185 | t.Run("responder", func(t *testing.T) { 186 | t.Parallel() 187 | 188 | r := newResponder(conn) 189 | r.addManaged(sv) // don't probe 190 | r.Respond(ctx) 191 | }) 192 | }) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package dnssd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/brutella/dnssd/log" 6 | 7 | "fmt" 8 | "net" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Config struct { 15 | // Name of the service. 16 | Name string 17 | 18 | // Type is the service type, for example "_hap._tcp". 19 | Type string 20 | 21 | // Domain is the name of the domain, for example "local". 22 | // If empty, "local" is used. 23 | Domain string 24 | 25 | // Host is the name of the host (no trailing dot). 26 | // If empty the local host name is used. 27 | Host string 28 | 29 | // Txt records 30 | Text map[string]string 31 | 32 | // IP addresses of the service. 33 | // This field is deprecated and should not be used. 34 | IPs []net.IP 35 | 36 | // Port is the port of the service. 37 | Port int 38 | 39 | // Interfaces at which the service should be registered 40 | Ifaces []string 41 | 42 | // The addresses for the interface which should be used (A / AAAA / Both) 43 | // If empty, all addresses are used. 44 | AdvertiseIPType IPType 45 | } 46 | 47 | func (c Config) Copy() Config { 48 | return Config{ 49 | Name: c.Name, 50 | Type: c.Type, 51 | Domain: c.Domain, 52 | Host: c.Host, 53 | Text: c.Text, 54 | IPs: c.IPs, 55 | Port: c.Port, 56 | Ifaces: c.Ifaces, 57 | AdvertiseIPType: c.AdvertiseIPType, 58 | } 59 | } 60 | 61 | func isDigit(r rune) bool { 62 | return r >= '0' && r <= '9' 63 | } 64 | 65 | func isAlpha(r rune) bool { 66 | return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') 67 | } 68 | 69 | func isWhitespace(r rune) bool { 70 | return r == ' ' 71 | } 72 | 73 | // validHostname returns a valid hostname as specified in RFC-952 and RFC1123. 74 | func validHostname(host string) string { 75 | result := "" 76 | z := len(host) - 1 77 | for i, r := range host { 78 | if isWhitespace(r) { 79 | r = '-' 80 | } 81 | // hostname must start with an alpha [RFC-952 ASSUMPTIONS] or digit [RFC1123 2.1] character. 82 | if i == 0 && (!isDigit(r) && !isAlpha(r)) { 83 | log.Debug.Printf(`hostname "%s" starts with "%s"`, host, string(r)) 84 | continue 85 | } 86 | 87 | // [RFC-952 ASSUMPTIONS] The last character must not be a minus sign or period. 88 | if i == z && (r == '-' || r == '.') { 89 | log.Debug.Printf(`hostname "%s" ends with "%s"`, host, string(r)) 90 | continue 91 | } 92 | 93 | if !isDigit(r) && !isAlpha(r) && r != '-' && r != '.' { 94 | log.Debug.Printf(`hostname "%s" contains "%s"`, host, string(r)) 95 | continue 96 | } 97 | 98 | result += string(r) 99 | } 100 | 101 | return result 102 | } 103 | 104 | type IPType int 105 | 106 | const ( 107 | Both = IPType(0) 108 | IPv4 = IPType(4) 109 | IPv6 = IPType(6) 110 | ) 111 | 112 | // Service represents a DNS-SD service instance 113 | type Service struct { 114 | Name string 115 | Type string 116 | Domain string 117 | Host string 118 | Text map[string]string 119 | TTL time.Duration // Original time to live 120 | Port int 121 | IPs []net.IP 122 | Ifaces []string 123 | AdvertiseIPType IPType 124 | 125 | // stores ips by interface name for caching purposes 126 | ifaceIPs map[string][]net.IP 127 | expiration time.Time 128 | } 129 | 130 | // NewService returns a new service for the given config. 131 | func NewService(cfg Config) (s Service, err error) { 132 | name := cfg.Name 133 | typ := cfg.Type 134 | port := cfg.Port 135 | 136 | if len(name) == 0 { 137 | err = fmt.Errorf("invalid name \"%s\"", name) 138 | return 139 | } 140 | 141 | if len(typ) == 0 { 142 | err = fmt.Errorf("invalid type \"%s\"", typ) 143 | return 144 | } 145 | 146 | if port == 0 { 147 | err = fmt.Errorf("invalid port \"%d\"", port) 148 | return 149 | } 150 | 151 | domain := cfg.Domain 152 | if len(domain) == 0 { 153 | domain = "local" 154 | } 155 | 156 | host := cfg.Host 157 | if len(host) == 0 { 158 | host = hostname() 159 | } 160 | 161 | text := cfg.Text 162 | if text == nil { 163 | text = map[string]string{} 164 | } 165 | 166 | ips := []net.IP{} 167 | var ifaces []string 168 | 169 | if cfg.IPs != nil && len(cfg.IPs) > 0 { 170 | ips = cfg.IPs 171 | } 172 | 173 | if cfg.Ifaces != nil && len(cfg.Ifaces) > 0 { 174 | ifaces = cfg.Ifaces 175 | } 176 | 177 | return Service{ 178 | Name: trimServiceNameSuffixRight(name), 179 | Type: typ, 180 | Domain: domain, 181 | Host: validHostname(host), 182 | Text: text, 183 | Port: port, 184 | IPs: ips, 185 | AdvertiseIPType: cfg.AdvertiseIPType, 186 | Ifaces: ifaces, 187 | ifaceIPs: map[string][]net.IP{}, 188 | }, nil 189 | } 190 | 191 | // Interfaces returns the network interfaces for which the service is registered, 192 | // or all multicast network interfaces, if no IP addresses are specified. 193 | func (s *Service) Interfaces() []*net.Interface { 194 | if len(s.Ifaces) > 0 { 195 | ifis := []*net.Interface{} 196 | for _, name := range s.Ifaces { 197 | if ifi, err := net.InterfaceByName(name); err == nil { 198 | ifis = append(ifis, ifi) 199 | } 200 | } 201 | 202 | return ifis 203 | } 204 | 205 | return MulticastInterfaces() 206 | } 207 | 208 | // IsVisibleAtInterface returns true, if the service is published 209 | // at the network interface with name n. 210 | func (s *Service) IsVisibleAtInterface(n string) bool { 211 | if len(s.Ifaces) == 0 { 212 | return true 213 | } 214 | 215 | for _, name := range s.Ifaces { 216 | if name == n { 217 | return true 218 | } 219 | } 220 | 221 | return false 222 | } 223 | 224 | // IPsAtInterface returns the ip address at a specific interface. 225 | func (s *Service) IPsAtInterface(iface *net.Interface) []net.IP { 226 | if iface == nil { 227 | return []net.IP{} 228 | } 229 | 230 | if ips, ok := s.ifaceIPs[iface.Name]; ok { 231 | return ips 232 | } 233 | 234 | if len(s.IPs) > 0 { 235 | return s.IPs 236 | } 237 | 238 | addrs, err := iface.Addrs() 239 | if err != nil { 240 | return []net.IP{} 241 | } 242 | 243 | ips := []net.IP{} 244 | for _, addr := range addrs { 245 | if ip, _, err := net.ParseCIDR(addr.String()); err == nil { 246 | ips = append(ips, ip) 247 | } else { 248 | log.Debug.Println(err) 249 | } 250 | } 251 | 252 | return ips 253 | } 254 | 255 | // HasIPOnAnyInterface returns true, if the service defines 256 | // the ip address on any network interface. 257 | func (s *Service) HasIPOnAnyInterface(ip net.IP) bool { 258 | for _, iface := range s.Interfaces() { 259 | ips := s.IPsAtInterface(iface) 260 | for _, ifaceIP := range ips { 261 | if ifaceIP.Equal(ip) { 262 | return true 263 | } 264 | } 265 | } 266 | 267 | return false 268 | } 269 | 270 | // Copy returns a copy of the service. 271 | func (s Service) Copy() *Service { 272 | return &Service{ 273 | Name: s.Name, 274 | Type: s.Type, 275 | Domain: s.Domain, 276 | Host: s.Host, 277 | Text: s.Text, 278 | TTL: s.TTL, 279 | IPs: s.IPs, 280 | Port: s.Port, 281 | AdvertiseIPType: s.AdvertiseIPType, 282 | Ifaces: s.Ifaces, 283 | ifaceIPs: s.ifaceIPs, 284 | expiration: s.expiration, 285 | } 286 | } 287 | 288 | func (s Service) EscapedName() string { 289 | return escape.Replace(s.Name) 290 | } 291 | 292 | func incrementHostname(name string, count int) string { 293 | return fmt.Sprintf("%s-%d", trimHostNameSuffixRight(name), count) 294 | } 295 | 296 | func trimHostNameSuffixRight(name string) string { 297 | minus := strings.LastIndex(name, "-") 298 | if minus == -1 || /* not found*/ 299 | minus == len(name)-1 /* at the end */ { 300 | return name 301 | } 302 | 303 | // after minus 304 | after := name[minus+1:] 305 | for _, r := range after { 306 | if !isDigit(r) { 307 | return name 308 | } 309 | } 310 | 311 | trimmed := name[:minus] 312 | if len(trimmed) == 0 { 313 | return name 314 | } 315 | return trimmed 316 | } 317 | 318 | // trimServiceNameSuffixRight removes any suffix with the format " (%d)". 319 | func trimServiceNameSuffixRight(name string) string { 320 | open := strings.LastIndex(name, "(") 321 | close := strings.LastIndex(name, ")") 322 | if open == -1 || close == -1 || /* not found*/ 323 | open >= close || /* wrong order */ 324 | open == 0 || /* at the beginning */ 325 | close != len(name)-1 /* not at the end */ { 326 | return name 327 | } 328 | 329 | // between brackets are only numbers 330 | between := name[open+1 : close-1] 331 | for _, r := range between { 332 | if !isDigit(r) { 333 | return name 334 | } 335 | } 336 | 337 | // before opening bracket is a whitespace 338 | if name[open-1] != ' ' { 339 | return name 340 | } 341 | 342 | trimmed := name[:open] 343 | trimmed = strings.TrimRight(trimmed, " ") 344 | if len(trimmed) == 0 { 345 | return name 346 | } 347 | return trimmed 348 | } 349 | 350 | func incrementServiceName(name string, count int) string { 351 | return fmt.Sprintf("%s (%d)", trimServiceNameSuffixRight(name), count) 352 | } 353 | 354 | // EscapedServiceInstanceName returns the same as `ServiceInstanceName()` 355 | // but escapes any special characters. 356 | func (s Service) EscapedServiceInstanceName() string { 357 | return fmt.Sprintf("%s.%s.%s.", s.EscapedName(), s.Type, s.Domain) 358 | } 359 | 360 | // ServiceInstanceName returns the service instance name 361 | // in the form of ... 362 | // (Note the trailing dot.) 363 | func (s Service) ServiceInstanceName() string { 364 | return fmt.Sprintf("%s.%s.%s.", s.Name, s.Type, s.Domain) 365 | } 366 | 367 | // ServiceName returns the service name in the 368 | // form of ".." 369 | // (Note the trailing dot.) 370 | func (s Service) ServiceName() string { 371 | return fmt.Sprintf("%s.%s.", s.Type, s.Domain) 372 | } 373 | 374 | // Hostname returns the hostname in the 375 | // form of ".." 376 | // (Note the trailing dot.) 377 | 378 | func (s Service) Hostname() string { 379 | return fmt.Sprintf("%s.%s.", s.Host, s.Domain) 380 | } 381 | 382 | // SetHostname sets the service's host name and 383 | // domain (if specified as ".."). 384 | // (Note the trailing dot.) 385 | func (s *Service) SetHostname(hostname string) { 386 | name, domain := parseHostname(hostname) 387 | 388 | if domain == s.Domain { 389 | s.Host = name 390 | } 391 | } 392 | 393 | // ServicesMetaQueryName returns the name of the meta query 394 | // for the service domain in the form of "_services._dns-sd._udp.