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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module ZeroConf
2
+ VERSION = "1.0.0"
3
+ end