zeroconf 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Gemfile +3 -0
- data/LICENSE +201 -0
- data/README.md +129 -0
- data/Rakefile +9 -0
- data/lib/zeroconf/service.rb +399 -0
- data/lib/zeroconf/utils.rb +65 -0
- data/lib/zeroconf/version.rb +3 -0
- data/lib/zeroconf.rb +288 -0
- data/test/client_test.rb +139 -0
- data/test/helper.rb +42 -0
- data/test/service_test.rb +334 -0
- data/zeroconf.gemspec +18 -0
- metadata +100 -0
@@ -0,0 +1,399 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ZeroConf
|
4
|
+
class Service
|
5
|
+
attr_reader :service, :service_port, :hostname, :service_interfaces,
|
6
|
+
:service_name, :qualified_host, :text
|
7
|
+
|
8
|
+
def initialize service, service_port, hostname = Socket.gethostname, service_interfaces: ZeroConf.service_interfaces, text: [""]
|
9
|
+
@service = service
|
10
|
+
@service_port = service_port
|
11
|
+
@hostname = hostname
|
12
|
+
@service_interfaces = service_interfaces
|
13
|
+
@service_name = "#{hostname}.#{service}"
|
14
|
+
@qualified_host = "#{hostname}.local."
|
15
|
+
@text = text
|
16
|
+
@rd, @wr = IO.pipe
|
17
|
+
end
|
18
|
+
|
19
|
+
def announcement
|
20
|
+
msg = Resolv::DNS::Message.new(0)
|
21
|
+
msg.qr = 1
|
22
|
+
msg.aa = 1
|
23
|
+
|
24
|
+
msg.add_additional service_name, 60, MDNS::Announce::IN::SRV.new(0, 0, service_port, qualified_host)
|
25
|
+
|
26
|
+
service_interfaces.each do |iface|
|
27
|
+
if iface.addr.ipv4?
|
28
|
+
msg.add_additional qualified_host,
|
29
|
+
60,
|
30
|
+
MDNS::Announce::IN::A.new(iface.addr.ip_address)
|
31
|
+
else
|
32
|
+
msg.add_additional qualified_host,
|
33
|
+
60,
|
34
|
+
MDNS::Announce::IN::AAAA.new(iface.addr.ip_address)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if @text
|
39
|
+
msg.add_additional service_name,
|
40
|
+
60,
|
41
|
+
MDNS::Announce::IN::TXT.new(*@text)
|
42
|
+
end
|
43
|
+
|
44
|
+
msg.add_answer service,
|
45
|
+
60,
|
46
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
47
|
+
|
48
|
+
msg
|
49
|
+
end
|
50
|
+
|
51
|
+
def disconnect_msg
|
52
|
+
msg = Resolv::DNS::Message.new(0)
|
53
|
+
msg.qr = 1
|
54
|
+
msg.aa = 1
|
55
|
+
|
56
|
+
msg.add_additional service_name, 0, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
57
|
+
|
58
|
+
service_interfaces.each do |iface|
|
59
|
+
if iface.addr.ipv4?
|
60
|
+
msg.add_additional qualified_host,
|
61
|
+
0,
|
62
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
63
|
+
else
|
64
|
+
msg.add_additional qualified_host,
|
65
|
+
0,
|
66
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if @text
|
71
|
+
msg.add_additional service_name,
|
72
|
+
0,
|
73
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
74
|
+
end
|
75
|
+
|
76
|
+
msg.add_answer service,
|
77
|
+
0,
|
78
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
79
|
+
|
80
|
+
msg
|
81
|
+
end
|
82
|
+
|
83
|
+
include Utils
|
84
|
+
|
85
|
+
def stop
|
86
|
+
@wr.write "x"
|
87
|
+
@wr.close
|
88
|
+
end
|
89
|
+
|
90
|
+
def start
|
91
|
+
sock = open_ipv4 Addrinfo.new(Socket.sockaddr_in(Resolv::MDNS::Port, Socket::INADDR_ANY)), Resolv::MDNS::Port
|
92
|
+
|
93
|
+
sockets = [sock, @rd]
|
94
|
+
|
95
|
+
msg = announcement
|
96
|
+
|
97
|
+
# announce
|
98
|
+
multicast_send(sock, msg.encode)
|
99
|
+
|
100
|
+
loop do
|
101
|
+
readers, = IO.select(sockets, [], [])
|
102
|
+
next unless readers
|
103
|
+
|
104
|
+
readers.each do |reader|
|
105
|
+
return if reader == @rd
|
106
|
+
|
107
|
+
buf, from = reader.recvfrom 2048
|
108
|
+
msg = Resolv::DNS::Message.decode(buf)
|
109
|
+
|
110
|
+
has_flags = (buf.getbyte(3) << 8 | buf.getbyte(2)) != 0
|
111
|
+
|
112
|
+
msg.question.each do |name, type|
|
113
|
+
class_type = type::ClassValue & ~MDNS_CACHE_FLUSH
|
114
|
+
|
115
|
+
break unless class_type == 1 || class_type == 255
|
116
|
+
|
117
|
+
unicast = type::ClassValue & PTR::MDNS_UNICAST_RESPONSE > 0
|
118
|
+
|
119
|
+
qn = name.to_s + "."
|
120
|
+
|
121
|
+
res = case qn
|
122
|
+
when DISCOVERY_NAME
|
123
|
+
break if has_flags
|
124
|
+
|
125
|
+
if unicast
|
126
|
+
dnssd_unicast_answer
|
127
|
+
else
|
128
|
+
dnssd_multicast_answer
|
129
|
+
end
|
130
|
+
when service
|
131
|
+
if unicast
|
132
|
+
service_unicast_answer
|
133
|
+
else
|
134
|
+
service_multicast_answer
|
135
|
+
end
|
136
|
+
when service_name
|
137
|
+
if unicast
|
138
|
+
service_instance_unicast_answer
|
139
|
+
else
|
140
|
+
service_instance_multicast_answer
|
141
|
+
end
|
142
|
+
when qualified_host
|
143
|
+
if unicast
|
144
|
+
name_answer_unicast
|
145
|
+
else
|
146
|
+
name_answer_multicast
|
147
|
+
end
|
148
|
+
else
|
149
|
+
#p [:QUERY2, type, type::ClassValue, name]
|
150
|
+
end
|
151
|
+
|
152
|
+
next unless res
|
153
|
+
|
154
|
+
if unicast
|
155
|
+
unicast_send reader, res.encode, from
|
156
|
+
else
|
157
|
+
multicast_send reader, res.encode
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# only yield replies to this question
|
162
|
+
end
|
163
|
+
end
|
164
|
+
ensure
|
165
|
+
multicast_send sock, disconnect_msg.encode
|
166
|
+
sockets.map(&:close)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def service_instance_unicast_answer
|
172
|
+
msg = Resolv::DNS::Message.new(0)
|
173
|
+
msg.qr = 1
|
174
|
+
msg.aa = 1
|
175
|
+
|
176
|
+
service_interfaces.each do |iface|
|
177
|
+
if iface.addr.ipv4?
|
178
|
+
msg.add_additional qualified_host,
|
179
|
+
10,
|
180
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
181
|
+
else
|
182
|
+
msg.add_additional qualified_host,
|
183
|
+
10,
|
184
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
if @text
|
189
|
+
msg.add_additional service_name,
|
190
|
+
10,
|
191
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
192
|
+
end
|
193
|
+
msg.add_answer service_name, 10, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
194
|
+
msg.add_question service_name, ZeroConf::MDNS::Announce::IN::SRV
|
195
|
+
|
196
|
+
msg
|
197
|
+
end
|
198
|
+
|
199
|
+
def service_unicast_answer
|
200
|
+
msg = Resolv::DNS::Message.new(0)
|
201
|
+
msg.qr = 1
|
202
|
+
msg.aa = 1
|
203
|
+
|
204
|
+
msg.add_additional service_name, 10, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
205
|
+
|
206
|
+
service_interfaces.each do |iface|
|
207
|
+
if iface.addr.ipv4?
|
208
|
+
msg.add_additional qualified_host,
|
209
|
+
10,
|
210
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
211
|
+
else
|
212
|
+
msg.add_additional qualified_host,
|
213
|
+
10,
|
214
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
if @text
|
219
|
+
msg.add_additional service_name,
|
220
|
+
10,
|
221
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
222
|
+
end
|
223
|
+
|
224
|
+
msg.add_answer service,
|
225
|
+
10,
|
226
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
227
|
+
|
228
|
+
msg.add_question service, ZeroConf::PTR
|
229
|
+
|
230
|
+
msg
|
231
|
+
end
|
232
|
+
|
233
|
+
def dnssd_unicast_answer
|
234
|
+
msg = Resolv::DNS::Message.new(0)
|
235
|
+
msg.qr = 1
|
236
|
+
msg.aa = 1
|
237
|
+
|
238
|
+
msg.add_answer DISCOVERY_NAME, 10,
|
239
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service))
|
240
|
+
|
241
|
+
msg.add_question DISCOVERY_NAME, ZeroConf::PTR
|
242
|
+
msg
|
243
|
+
end
|
244
|
+
|
245
|
+
def dnssd_multicast_answer
|
246
|
+
msg = Resolv::DNS::Message.new(0)
|
247
|
+
msg.qr = 1
|
248
|
+
msg.aa = 1
|
249
|
+
|
250
|
+
msg.add_answer DISCOVERY_NAME, 60,
|
251
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service))
|
252
|
+
msg
|
253
|
+
end
|
254
|
+
|
255
|
+
def service_multicast_answer
|
256
|
+
msg = Resolv::DNS::Message.new(0)
|
257
|
+
msg.qr = 1
|
258
|
+
msg.aa = 1
|
259
|
+
|
260
|
+
msg.add_additional service_name, 60, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
261
|
+
|
262
|
+
service_interfaces.each do |iface|
|
263
|
+
if iface.addr.ipv4?
|
264
|
+
msg.add_additional qualified_host,
|
265
|
+
60,
|
266
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
267
|
+
else
|
268
|
+
msg.add_additional qualified_host,
|
269
|
+
60,
|
270
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
if @text
|
275
|
+
msg.add_additional service_name,
|
276
|
+
60,
|
277
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
278
|
+
end
|
279
|
+
|
280
|
+
msg.add_answer service,
|
281
|
+
60,
|
282
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
283
|
+
|
284
|
+
msg
|
285
|
+
end
|
286
|
+
|
287
|
+
def service_instance_multicast_answer
|
288
|
+
msg = Resolv::DNS::Message.new(0)
|
289
|
+
msg.qr = 1
|
290
|
+
msg.aa = 1
|
291
|
+
|
292
|
+
msg.add_answer service_name, 60, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
293
|
+
|
294
|
+
service_interfaces.each do |iface|
|
295
|
+
if iface.addr.ipv4?
|
296
|
+
msg.add_additional qualified_host,
|
297
|
+
60,
|
298
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
299
|
+
else
|
300
|
+
msg.add_additional qualified_host,
|
301
|
+
60,
|
302
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
if @text
|
307
|
+
msg.add_additional service_name,
|
308
|
+
60,
|
309
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
310
|
+
end
|
311
|
+
|
312
|
+
msg
|
313
|
+
end
|
314
|
+
|
315
|
+
def name_answer_unicast
|
316
|
+
msg = Resolv::DNS::Message.new(0)
|
317
|
+
msg.qr = 1
|
318
|
+
msg.aa = 1
|
319
|
+
|
320
|
+
first = true
|
321
|
+
|
322
|
+
service_interfaces.each do |iface|
|
323
|
+
if first
|
324
|
+
if iface.addr.ipv4?
|
325
|
+
msg.add_answer qualified_host,
|
326
|
+
10,
|
327
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
328
|
+
else
|
329
|
+
msg.add_answer s.qualified_host,
|
330
|
+
10,
|
331
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
332
|
+
end
|
333
|
+
first = false
|
334
|
+
else
|
335
|
+
if iface.addr.ipv4?
|
336
|
+
msg.add_additional qualified_host,
|
337
|
+
10,
|
338
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
339
|
+
else
|
340
|
+
msg.add_additional qualified_host,
|
341
|
+
10,
|
342
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
msg.add_question qualified_host, ZeroConf::MDNS::Announce::IN::A
|
348
|
+
|
349
|
+
if @text
|
350
|
+
msg.add_additional service_name,
|
351
|
+
10,
|
352
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
353
|
+
end
|
354
|
+
|
355
|
+
msg
|
356
|
+
end
|
357
|
+
|
358
|
+
def name_answer_multicast
|
359
|
+
msg = Resolv::DNS::Message.new(0)
|
360
|
+
msg.qr = 1
|
361
|
+
msg.aa = 1
|
362
|
+
|
363
|
+
first = true
|
364
|
+
|
365
|
+
service_interfaces.each do |iface|
|
366
|
+
if first
|
367
|
+
if iface.addr.ipv4?
|
368
|
+
msg.add_answer qualified_host,
|
369
|
+
60,
|
370
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
371
|
+
else
|
372
|
+
msg.add_answer s.qualified_host,
|
373
|
+
60,
|
374
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
375
|
+
end
|
376
|
+
first = false
|
377
|
+
else
|
378
|
+
if iface.addr.ipv4?
|
379
|
+
msg.add_additional qualified_host,
|
380
|
+
60,
|
381
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
382
|
+
else
|
383
|
+
msg.add_additional qualified_host,
|
384
|
+
60,
|
385
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
if @text
|
391
|
+
msg.add_additional service_name,
|
392
|
+
60,
|
393
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
394
|
+
end
|
395
|
+
|
396
|
+
msg
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "ipaddr"
|
5
|
+
require "fcntl"
|
6
|
+
require "resolv"
|
7
|
+
|
8
|
+
module ZeroConf
|
9
|
+
module Utils
|
10
|
+
DISCOVERY_NAME = "_services._dns-sd._udp.local."
|
11
|
+
|
12
|
+
def open_ipv4 saddr, port
|
13
|
+
sock = UDPSocket.new Socket::AF_INET
|
14
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
15
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
16
|
+
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, true)
|
17
|
+
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, true)
|
18
|
+
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP,
|
19
|
+
IPAddr.new(Resolv::MDNS::AddressV4).hton + IPAddr.new(saddr.ip_address).hton)
|
20
|
+
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(saddr.ip_address).hton)
|
21
|
+
sock.bind saddr.ip_address, port
|
22
|
+
flags = sock.fcntl(Fcntl::F_GETFL, 0)
|
23
|
+
sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK | flags)
|
24
|
+
sock
|
25
|
+
end
|
26
|
+
|
27
|
+
BROADCAST_V4 = Addrinfo.new Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV4)
|
28
|
+
BROADCAST_V6 = Addrinfo.new Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV6)
|
29
|
+
|
30
|
+
def multicast_send sock, query
|
31
|
+
dest = if sock.local_address.ipv4?
|
32
|
+
BROADCAST_V4
|
33
|
+
else
|
34
|
+
BROADCAST_V6
|
35
|
+
end
|
36
|
+
|
37
|
+
sock.send(query, 0, dest)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unicast_send sock, data, to
|
41
|
+
sock.send(data, 0, Addrinfo.new(to))
|
42
|
+
end
|
43
|
+
|
44
|
+
def open_ipv6 saddr, port
|
45
|
+
sock = UDPSocket.new Socket::AF_INET6
|
46
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
47
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
48
|
+
sock.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_MULTICAST_HOPS, true)
|
49
|
+
sock.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_MULTICAST_LOOP, true)
|
50
|
+
|
51
|
+
# This address isn't correct, but giving it to IPAddr seems to result
|
52
|
+
# in the right bytes back from hton.
|
53
|
+
# See: https://github.com/ruby/ipaddr/issues/63
|
54
|
+
s = IPAddr.new("ff02:0000:0000:0000:0000:00fb:0000:0000").hton
|
55
|
+
sock.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_JOIN_GROUP, s)
|
56
|
+
sock.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_MULTICAST_IF, IPAddr.new(saddr.ip_address).hton)
|
57
|
+
sock.bind saddr.ip_address, port
|
58
|
+
flags = sock.fcntl(Fcntl::F_GETFL, 0)
|
59
|
+
sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK | flags)
|
60
|
+
|
61
|
+
sock
|
62
|
+
rescue SystemCallError
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|