forked from ghetzel/go-stockutil
/
mdns.go
213 lines (184 loc) · 5.33 KB
/
mdns.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package netutil
import (
"context"
"crypto/sha256"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/PerformLine/go-stockutil/rxutil"
"github.com/PerformLine/go-stockutil/stringutil"
"github.com/PerformLine/go-stockutil/typeutil"
"github.com/grandcat/zeroconf"
"github.com/mcuadros/go-defaults"
)
var registered sync.Map
type ZeroconfOptions struct {
Context context.Context
Limit int
Timeout time.Duration `default:"30s"`
Service string `default:"_http._tcp"`
Domain string `default:".local"`
CheckInterval time.Duration `default:"100ms"`
MatchInstance string
MatchPort string
MatchHostname string
MatchAddress string
}
type Service struct {
Hostname string `json:"hostname"`
Instance string `json:"instance"`
Service string `json:"service"`
Domain string `json:"domain"`
Port int `json:"port"`
Text []string `json:"txt"`
Address string `json:"address"`
Addresses []net.IP `json:"addresses,omitempty"`
Interfaces []net.Interface `json:"interfaces,omitempty"`
}
func (self *Service) String() string {
return strings.TrimSuffix(
fmt.Sprintf("%s.%s%s:%d %v", self.Instance, self.Service, self.Domain, self.Port, self.Text),
` []`,
)
}
type ServiceFunc func(*Service) bool
func isEntryMatch(options *ZeroconfOptions, entry *zeroconf.ServiceEntry) bool {
if rx := options.MatchInstance; rx == `` || rxutil.IsMatchString(rx, entry.Instance) {
return true
} else if rx := options.MatchPort; rx == `` || rxutil.IsMatchString(rx, typeutil.String(entry.Port)) {
return true
} else if rx := options.MatchHostname; rx == `` || rxutil.IsMatchString(rx, typeutil.String(entry.HostName)) {
return true
} else if rx := options.MatchAddress; rx == `` {
return true
} else {
for _, ip := range entry.AddrIPv4 {
if rxutil.IsMatchString(rx, ip.String()) {
return true
}
}
for _, ip := range entry.AddrIPv6 {
if rxutil.IsMatchString(rx, ip.String()) {
return true
}
}
}
return false
}
// Perform Multicast DNS discovery on the local network, calling the fn callback for each
// discovered service.
func ZeroconfDiscover(options *ZeroconfOptions, fn ServiceFunc) error {
if fn == nil {
return fmt.Errorf("Must provide a callback function to receive discover services")
}
if options == nil {
options = new(ZeroconfOptions)
}
defaults.SetDefaults(options)
if options.Context == nil {
options.Context = context.Background()
}
found := 0
// setup mDNS resolver
if resolver, err := zeroconf.NewResolver(
zeroconf.SelectIPTraffic(zeroconf.IPv4AndIPv6),
); err == nil {
entries := make(chan *zeroconf.ServiceEntry)
ctx, cancel := context.WithTimeout(options.Context, options.Timeout)
defer cancel()
// receive discovered services
go func(results <-chan *zeroconf.ServiceEntry) {
for entry := range results {
if isEntryMatch(options, entry) {
found += 1
addrs := make([]net.IP, 0)
addrs = append(addrs, entry.AddrIPv4...)
addrs = append(addrs, entry.AddrIPv6...)
addr := ``
if len(addrs) > 0 {
addr = fmt.Sprintf("%v:%d", addrs[0], entry.Port)
}
// fire off callback for this service
if !fn(&Service{
Hostname: entry.HostName,
Instance: entry.Instance,
Service: entry.Service,
Port: entry.Port,
Domain: entry.Domain,
Text: entry.Text,
Addresses: addrs,
Address: addr,
}) {
cancel()
}
}
if options.Limit > 0 && found >= options.Limit {
cancel()
}
}
}(entries)
// actually start mDNS discovery
if err := resolver.Browse(ctx, options.Service, options.Domain, entries); err == nil {
select {
case <-ctx.Done():
}
return nil
} else {
return fmt.Errorf("browse error: %v", err)
}
} else {
return err
}
}
// Register the given service in Multicast DNS. Returns an ID that can be used to unregister
// the service later.
func ZeroconfRegister(svc *Service) (string, error) {
if svc == nil {
return ``, fmt.Errorf("Must provide a service configuration to register mDNS")
} else if svc.Instance == `` {
svc.Instance = stringutil.UUID().String()
} else if svc.Service == `` {
return ``, fmt.Errorf("Must provide a service type")
} else if svc.Domain == `` {
return ``, fmt.Errorf("Must provide a service domain")
} else if svc.Port == 0 {
return ``, fmt.Errorf("Must specify a service port")
}
slug := fmt.Sprintf("%x", sha256.Sum256(
[]byte(fmt.Sprintf("%s.%s%s:%d", svc.Instance, svc.Service, svc.Domain, svc.Port)),
))
if _, ok := registered.Load(slug); ok {
return slug, fmt.Errorf("A service matching these parameters is already registered")
}
if server, err := zeroconf.Register(
svc.Instance,
svc.Service,
svc.Domain,
svc.Port,
svc.Text,
svc.Interfaces,
); err == nil {
registered.Store(slug, server)
return slug, nil
} else {
return ``, err
}
}
// Unregister a previously-registered service.
func ZeroconfUnregister(id string) {
defer registered.Delete(id)
if s, ok := registered.Load(id); ok {
if server, ok := s.(*zeroconf.Server); ok {
server.Shutdown()
}
}
}
// Unregister all Multicast DNS services.
func ZeroconfUnregisterAll() {
registered.Range(func(key, value interface{}) bool {
ZeroconfUnregister(typeutil.String(key))
return true
})
}