toolmantim-zeroconf 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,278 @@
1
+ TODO
2
+
3
+ Release: 0.0 "seeing if its possible" - done
4
+
5
+ Release: 0.1 "making it work"
6
+
7
+ A complete rewrite, too many changes to even begin to mention.
8
+
9
+ Release: 0.2 "doing the right thing"
10
+
11
+ - must always respond to requests for the A record for this host's .local
12
+ address
13
+
14
+ - cache the answers in the additional section
15
+
16
+ - in MDNSSD, put the handle into the reply as the "service" attribute
17
+
18
+ - need to send TTL=0 message on service shutdown
19
+
20
+ - implement flags, particularly add/remove flags
21
+
22
+ - remove all use of absolute in name comparisons, I have had too many bugs
23
+ involving this
24
+
25
+ - deal with timeouts in Resolv::MDNS, how long, can they be set, etc.
26
+
27
+ - reverse lookup of names by addresses using mDNS, see notes in
28
+ #generate_candidates. I think I should allow reverse lookup of addresss in
29
+ the private address spaces [RFC1918] (address prefixes of 10/8, 172.16/12,
30
+ and 192.168/16).
31
+
32
+ - correctly encode a nil in a text_record, and allow "key" w/no value for "mdns.rb -R"
33
+
34
+ - probe for name conflicts before registering a service
35
+
36
+ - when we see a question, flush answers over a few seconds old from cache
37
+ .. and do we notify queries that the answers are being deleted, too?
38
+
39
+ - escape/parse DNS-SD names ...
40
+
41
+ - try the meta-queries (_services._dns-sd._udp.local.)
42
+
43
+ - add known answers to queries
44
+
45
+ - answer unicast questions
46
+
47
+ - implement a MDNSSD api for DNSServiceQueryRecord, and 'mdns.rb -Q"
48
+
49
+ Release: 0.3 "keep it working"
50
+
51
+ - Change the technique for looking up IPv4 address of the default interface,
52
+ the old technique stopped working on OS X boxes that had IPv6 enabled (which
53
+ was the default).
54
+
55
+ - Fixed a bug causing infinite loops (in the wrong place) in background query.
56
+
57
+
58
+ Todo:
59
+
60
+ - define constants in module MDNSSD::Types like Http = '_http._tcp', ...?
61
+
62
+ - mdns.rb -K, watch for records and immediately register replacements pointing
63
+ to somewhere else
64
+
65
+ - general code cleanups
66
+
67
+ - Add #to_s to the RR types.
68
+
69
+ - Add #to_s the *classes* of the RR types ("IN::TXT", ...)
70
+
71
+ - HINFO RR service
72
+
73
+ - A RR service
74
+
75
+ - wildcard answering services
76
+
77
+ - Query#start, #restart
78
+
79
+ - let Query.new take a block, and yield itself, or yield with every answer?
80
+
81
+ - move hierarchal DNS Name comparison operators to an optional file.
82
+
83
+ - DNS packet dumper
84
+
85
+ - to make both TXT objects and TXT classes have a value,
86
+ add methods type_value and type_class.
87
+
88
+
89
+
90
+ dns-sd test mode results:
91
+ -A ok, but don't support -Q so don't see HINFO add/update/remove
92
+ -U ok
93
+ -N ok, but don't support -Q so don't see NULL add
94
+ -T ok
95
+ -M no, we don't support multiple TXT records for a service
96
+ -I ok
97
+
98
+
99
+
100
+ Open resolv.rb issues:
101
+
102
+ - Reverse DNS lookups are done by sock.recv, seems like its a problem for a
103
+ DNS library...
104
+
105
+ Shouldn't BasicSocket.do_not_reverse_lookup be set? Or be set on our sockets?
106
+ It's weird that in @sock.recv we get a tuple with the address... when we ARE
107
+ a resolver library.... and if you call resolv-replace, won't we call
108
+ ourselves in order to resolve the IP when you call recv to get the peer info?
109
+ And isn't resolv-replace ignoring BasicSocket.do_not_reverse_lookup? So, it
110
+ is very weird that resolv.rb does ad socket.recv that causes (blocking) DNS
111
+ queries to occure for its peer IP address...
112
+
113
+ - Resolv::Hosts returns IPv6 addresses before IPv4, so Resolv.getaddress('localhost')
114
+ is ::1 on my system - this is not good.
115
+
116
+ - See [DNSOPV6:3.1], does resolv.rb do this?
117
+
118
+
119
+ [DNSOPV6]
120
+ [DNSOPBADRES] draft-ietf-dnsop-bad-dns-res-03.tx
121
+
122
+ Open design issues:
123
+
124
+ - how to get the local ifx ipv4 address?
125
+
126
+ - Net classes that do socksetopts, so you don't have to pack the structs
127
+ yourself! or maybe implement the 8 functions of Stevens?
128
+
129
+
130
+ System differences in multicast:
131
+
132
+ [EACCES] The destination address is a broadcast address, and SO_BROADCAST has not been set on the socket.
133
+
134
+
135
+ Comments for DNS-SD:
136
+
137
+ * domain and type end in a '.', is that necessary?
138
+
139
+ * Names of Stuff
140
+
141
+ I'm getting lost in trying to remember what goes into a method, and what is available
142
+ in it's Reply object. I think the following convention would help:
143
+
144
+ Every function argument maps to a Reply attribute of the same name.
145
+
146
+ The Reply attributes and function arguments should be the same.
147
+
148
+ This makes things easy to remember, as data goes into #browse, comes out
149
+ BrowseReply, goes into #resolve, comes out ResolveReply it doesn't change its
150
+ name!
151
+
152
+
153
+
154
+ Currently:
155
+
156
+ DNSSD::BrowseReply.instance_methods:
157
+ ["flags", "service", "domain", "fullname", "interface", "name", "type"]
158
+
159
+ Note that #type overrides Object#type.
160
+ service_type had its name changed to type
161
+
162
+ DNSSD::ResolveReply.instance_methods:
163
+ [ "flags", "service", "fullname", "interface", "port", "target", "text_record"]
164
+
165
+ Missing #domain!
166
+ Missing #name!
167
+ Missing #type!
168
+
169
+ #resolve takes argument #service_name, it comes from BrowseReply#name
170
+ #resolve takes argument #service_type, it comes from BrowseReply#type
171
+ #resolve takes argument #service_domain, it comes from BrowseReply#domain
172
+
173
+ Note that interface is still interface.
174
+
175
+ #browse has argument domain, #register has argument #service_domain
176
+
177
+
178
+ DNSSD::RegisterReply.instance_methods:
179
+
180
+ [ "flags", "service", "domain", "name", "type" ]
181
+
182
+ Missing #interface, #port, #target, #text_record.
183
+
184
+
185
+ As it is, I find the naming really confusing. Changing the names in the replies
186
+ causes interface breakage, so how about changing the names in the arguments so
187
+ they are the same as the attributes in the reply, and so that all 3 functions
188
+ use the same name for the same thing?
189
+
190
+
191
+ This code supplies some missing methods:
192
+
193
+ module DNSSD
194
+ def self.namesplit(n)
195
+ n.scan(/(?:\\.|[^\.])+/)
196
+ end
197
+ class ResolveReply
198
+ def domain
199
+ DNSSD.namesplit(fullname)[-1]
200
+ end
201
+ def type
202
+ DNSSD.namesplit(fullname)[1,2].join('.')
203
+ end
204
+ def name
205
+ DNSSD.namesplit(fullname)[0]
206
+ end
207
+ end
208
+ end
209
+
210
+
211
+
212
+
213
+ Comments for Apple:
214
+
215
+ * [MDNS:5] Reverse Address Mapping - should allow reverse mapping
216
+ in the [RFC1918] address space as well. Does Apple's resolver
217
+ do this?
218
+
219
+ * TXT record is required by mDNSResponder
220
+
221
+ DNS-SD does not require a responder advertising a service to have
222
+ a TXT record.
223
+
224
+
225
+ Problem:
226
+
227
+ I assume it is done because if you ask for SRV and TXT (or ANY), and
228
+ get only SRV, you don't know if it is because there is no TXT, or because
229
+ it didn't make it across unreliable UDP.
230
+
231
+ Solutions:
232
+
233
+ Require a TXT record (curent mDNSResponder behaviour).
234
+
235
+ Require that all the questions in a single DNS message be responded
236
+ to in a single DNS message. This allows:
237
+
238
+ 1 - asking for ANY?, and if you get a SRV but no TXT, it means there
239
+ is no TXT
240
+ 2 - asking for TXT? and SRV?, and if you get a SRV but no TXT, it means
241
+ there is no TXT
242
+
243
+ Recommend:
244
+
245
+ I prefer the latter. It scales to more than two records associated with a
246
+ service, so in the future if a service needs a SRV, TXT, and NUL record, a
247
+ single query can be sent, and a single query can be expected as a response.
248
+
249
+ For legacy interop reasons, we are probably stuck with having to advertise a
250
+ TXT record. I still think all questions from a single message should be answered
251
+ in a single message. Possible exception is when to large, in which case the
252
+ TC bit should be set so the resolver knows to expect another message.
253
+
254
+
255
+ * TXT records from mDNSResponder may have zero character-strings
256
+
257
+ Violates DNS spec.
258
+
259
+ DNS-SD claims to conform to DNS.
260
+
261
+ DNS-SD says TXT has zero or more character-strings.
262
+
263
+ Two statements not in agreement.
264
+
265
+ Recommend that:
266
+ - DNS-SD be altered to state TXT records MUST conform to DNS specs.
267
+ - DNS-SD include a warning that deployed responders generate TXT
268
+ with zero char-strings, and implementations SHOULD be capable of handling
269
+ this as being equivalent to a TXT with one zero-length character string.
270
+
271
+
272
+ * OS X puts additional answers in the answers section, not the additional section
273
+
274
+ * OS X resolver generates queries for ensemble.local.local
275
+
276
+ This violates DNS-SD.
277
+
278
+
@@ -0,0 +1,50 @@
1
+ #!/usr/local/bin/ruby18 -w
2
+ # Author: Sam Roberts <sroberts@uniserve.com>
3
+ # Licence: this file is placed in the public domain
4
+
5
+ require 'net/http'
6
+ require 'thread'
7
+ require 'pp'
8
+
9
+ # For MDNSSD
10
+ require 'net/dns/mdns-sd'
11
+
12
+ # To make Resolv aware of mDNS
13
+ require 'net/dns/resolv-mdns'
14
+
15
+ # To make TCPSocket use Resolv, not the C library resolver.
16
+ require 'net/dns/resolv-replace'
17
+
18
+ # Use a short name.
19
+ DNSSD = Net::DNS::MDNSSD
20
+
21
+ # Sync stdout, and don't write to console from multiple threads.
22
+ $stdout.sync
23
+ $lock = Mutex.new
24
+
25
+ # Be quiet.
26
+ debug = false
27
+
28
+ DNSSD.browse('_http._tcp') do |b|
29
+ $lock.synchronize { pp b } if debug
30
+ DNSSD.resolve(b.name, b.type) do |r|
31
+ $lock.synchronize { pp r } if debug
32
+ begin
33
+ http = Net::HTTP.new(r.target, r.port)
34
+
35
+ path = r.text_record['path'] || '/'
36
+
37
+ headers = http.head(path)
38
+
39
+ $lock.synchronize do
40
+ puts "#{r.name.inspect} on #{r.target}:#{r.port}#{path} using server #{headers['server']}"
41
+ end
42
+ rescue
43
+ $lock.synchronize { puts $!; puts $!.backtrace }
44
+ end
45
+ end
46
+ end
47
+
48
+ # Hit enter when you think that's all.
49
+ STDIN.gets
50
+
@@ -0,0 +1,29 @@
1
+ #!/usr/local/bin/ruby18 -w
2
+ # Author: Sam Roberts <sroberts@uniserve.com>
3
+ # Licence: this file is placed in the public domain
4
+
5
+ require 'net/http'
6
+ require 'net/dns/resolv-mdns'
7
+
8
+ mdns = Resolv::MDNS.default
9
+
10
+ mdns.each_resource('_http._tcp.local', Resolv::DNS::Resource::IN::PTR) do |rrhttp|
11
+ service = rrhttp.name
12
+ host = nil
13
+ port = nil
14
+ path = '/'
15
+
16
+ rrsrv = mdns.getresource(rrhttp.name, Resolv::DNS::Resource::IN::SRV)
17
+ host, port = rrsrv.target.to_s, rrsrv.port
18
+ rrtxt = mdns.getresource(rrhttp.name, Resolv::DNS::Resource::IN::TXT)
19
+ if rrtxt.data =~ /path=(.*)/
20
+ path = $1
21
+ end
22
+
23
+ http = Net::HTTP.new(host, port)
24
+
25
+ headers = http.head(path)
26
+
27
+ puts "#{service[0]} on #{host}:#{port}#{path} was last-modified #{headers['last-modified']}"
28
+ end
29
+
@@ -0,0 +1,56 @@
1
+ #!/usr/local/bin/ruby18 -w
2
+ # Author: Sam Roberts <sroberts@uniserve.com>
3
+ # Licence: this file is placed in the public domain
4
+ #
5
+ # Advertise a webrick server over mDNS.
6
+
7
+ require 'webrick'
8
+ require 'net/dns/mdns-sd'
9
+
10
+ DNSSD = Net::DNS::MDNSSD
11
+
12
+ class HelloServlet < WEBrick::HTTPServlet::AbstractServlet
13
+ def do_GET(req, resp)
14
+ resp.body = "hello, world\n"
15
+ resp['content-type'] = 'text/plain'
16
+ raise WEBrick::HTTPStatus::OK
17
+ end
18
+ end
19
+
20
+ # This may seem convoluted... but if there are multiple address families
21
+ # available, like AF_INET6 and AF_INET, this should create multiple TCPServer
22
+ # sockets for them.
23
+ families = Socket.getaddrinfo(nil, 1, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 0, Socket::AI_PASSIVE)
24
+
25
+ listeners = []
26
+ port = 0
27
+
28
+ families.each do |af, one, dns, addr|
29
+ p port, addr
30
+ listeners << TCPServer.new(addr, port)
31
+ port = listeners.first.addr[1] unless port != 0
32
+ end
33
+
34
+ listeners.each do |s|
35
+ puts "listen on #{s.addr.inspect}"
36
+ end
37
+
38
+ # This will dynamically allocate multiple TCPServers, each on a different port.
39
+ server = WEBrick::HTTPServer.new( :Port => 0 )
40
+
41
+ # So we replace them with our TCPServer sockets which are all on the same
42
+ # (dynamically assigned) port.
43
+ server.listeners.each do |s| s.close end
44
+ server.listeners.replace listeners
45
+ server.config[:Port] = port
46
+
47
+ server.mount( '/hello/', HelloServlet )
48
+
49
+ handle = DNSSD.register("hello", '_http._tcp', 'local', port, 'path' => '/hello/')
50
+
51
+ ['INT', 'TERM'].each { |signal|
52
+ trap(signal) { server.shutdown; handle.stop; }
53
+ }
54
+
55
+ server.start
56
+
@@ -0,0 +1,132 @@
1
+ #!/usr/local/bin/ruby18
2
+
3
+ require 'socket'
4
+ require 'ipaddr'
5
+ require 'net/dns'
6
+
7
+ $stderr.sync = true
8
+ $stdout.sync = true
9
+
10
+ Addr = "224.0.0.251"
11
+ Port = 5353
12
+
13
+ include Net::DNS
14
+
15
+ @hostname = Name.create(Socket.gethostname)
16
+ @hostname.absolute = true
17
+ @hostaddr = Socket.getaddrinfo(@hostname.to_s, 0, Socket::AF_INET, Socket::SOCK_STREAM)[0][3]
18
+ @hostrr = [ @hostname, 240, IN::A.new(@hostaddr) ]
19
+ @hostaddr = IPAddr.new(@hostaddr).hton
20
+
21
+ @sock = UDPSocket.new
22
+
23
+ # TODO - do we need this?
24
+ @sock.fcntl(Fcntl::F_SETFD, 1)
25
+
26
+ # Allow 5353 to be shared.
27
+ so_reuseport = 0x0200 # The definition on OS X, where it is required.
28
+ if Socket.constants.include? 'SO_REUSEPORT'
29
+ so_reuseport = Socket::SO_REUSEPORT
30
+ end
31
+ begin
32
+ @sock.setsockopt(Socket::SOL_SOCKET, so_reuseport, 1)
33
+ rescue
34
+ warn( "set SO_REUSEPORT raised #{$!}, try SO_REUSEADDR" )
35
+ so_reuseport = Socket::SO_REUSEADDR
36
+ @sock.setsockopt(Socket::SOL_SOCKET, so_reuseport, 1)
37
+ end
38
+
39
+ # Request dest addr and ifx ids... no.
40
+
41
+ # Join the multicast group.
42
+ # option is a struct ip_mreq { struct in_addr, struct in_addr }
43
+ ip_mreq = IPAddr.new(Addr).hton + @hostaddr
44
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip_mreq)
45
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, @hostaddr)
46
+
47
+ # Set IP TTL for outgoing packets.
48
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, 255)
49
+ @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255)
50
+
51
+ # Apple source makes it appear that optval may need to be a "char" on
52
+ # some systems:
53
+ # @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255 as int)
54
+ # - or -
55
+ # @sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 255 as byte)
56
+
57
+ # Bind to our port.
58
+ @sock.bind(Socket::INADDR_ANY, Port)
59
+
60
+ class Resolv
61
+ class DNS
62
+ class Resource
63
+ module IN
64
+ class SRV
65
+ def inspect
66
+ "#{target}:#{port} weight=#{weight} priority=#{priority}"
67
+ end
68
+ end
69
+ class TXT
70
+ def inspect
71
+ strings.inspect
72
+ end
73
+ end
74
+ class PTR
75
+ def inspect
76
+ name.to_s
77
+ end
78
+ end
79
+ class A
80
+ def inspect
81
+ address.to_s
82
+ end
83
+ end
84
+ class HINFO
85
+ def inspect
86
+ "os=#{os.inspect}\ncpu=#{cpu.inspect}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ loop do
95
+
96
+ reply, from = @sock.recvfrom(9000)
97
+
98
+ puts "++ from #{from.inspect}"
99
+
100
+ if false
101
+ puts reply.inspect
102
+ puts "--"
103
+ end
104
+
105
+ msg = Resolv::DNS::Message.decode(reply)
106
+
107
+ qr = msg.qr==0 ? 'Q' : 'R'
108
+ qrstr = msg.qr==0 ? 'Query' : 'Resp'
109
+
110
+ opcode = { 0=>'QUERY', 1=>'IQUERY', 2=>'STATUS'}[msg.opcode]
111
+
112
+ puts "#{qrstr}: id #{msg.id} qr #{qr} opcode #{opcode} aa #{msg.aa} tc #{msg.tc} rd #{msg.rd} ra #{msg.ra} rcode #{msg.rcode}"
113
+
114
+ msg.question.each do |name, type, unicast|
115
+ puts "qu #{Net::DNS.rrname type} #{name.to_s.inspect} unicast=#{unicast}"
116
+ end
117
+ msg.answer.each do |name, ttl, data, cacheflush|
118
+ puts "an #{Net::DNS.rrname data} #{name.to_s.inspect} ttl=#{ttl} cacheflush=#{cacheflush}"
119
+ puts " #{data.inspect}"
120
+ end
121
+ msg.authority.each do |name, ttl, data, cacheflush|
122
+ puts "au #{Net::DNS.rrname data} #{name.to_s.inspect} ttl=#{ttl} cacheflush=#{cacheflush.inspect}"
123
+ puts " #{data.inspect}"
124
+ end
125
+ msg.additional.each do |name, ttl, data, cacheflush|
126
+ puts "ad #{Net::DNS.rrname data} #{name.to_s.inspect} ttl=#{ttl} cacheflush=#{cacheflush.inspect}"
127
+ puts " #{data.inspect}"
128
+ end
129
+
130
+ puts
131
+ end
132
+