1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import dpkt 7import logging 8import socket 9import time 10 11 12DnsRecord = collections.namedtuple('DnsResult', ['rrname', 'rrtype', 'data', 'ts']) 13 14MDNS_IP_ADDR = '224.0.0.251' 15MDNS_PORT = 5353 16 17# Value to | to a class value to signal cache flush. 18DNS_CACHE_FLUSH = 0x8000 19 20# When considering SRV records, clients are supposed to unilaterally prefer 21# numerically lower priorities, then pick probabilistically by weight. 22# See RFC2782. 23# An arbitrary number that will fit in 16 bits. 24DEFAULT_PRIORITY = 500 25# An arbitrary number that will fit in 16 bits. 26DEFAULT_WEIGHT = 500 27 28def _RR_equals(rra, rrb): 29 """Returns whether the two dpkt.dns.DNS.RR objects are equal.""" 30 # Compare all the members present in either object and on any RR object. 31 keys = set(rra.__dict__.keys() + rrb.__dict__.keys() + 32 dpkt.dns.DNS.RR.__slots__) 33 # On RR objects, rdata is packed based on the other members and the final 34 # packed string depends on other RR and Q elements on the same DNS/mDNS 35 # packet. 36 keys.discard('rdata') 37 for key in keys: 38 if hasattr(rra, key) != hasattr(rrb, key): 39 return False 40 if not hasattr(rra, key): 41 continue 42 if key == 'cls': 43 # cls attribute should be masked for the cache flush bit. 44 if (getattr(rra, key) & ~DNS_CACHE_FLUSH != 45 getattr(rrb, key) & ~DNS_CACHE_FLUSH): 46 return False 47 else: 48 if getattr(rra, key) != getattr(rrb, key): 49 return False 50 return True 51 52 53class ZeroconfDaemon(object): 54 """Implements a simulated Zeroconf daemon running on the given host. 55 56 This class implements part of the Multicast DNS RFC 6762 able to simulate 57 a host exposing services or consuming services over mDNS. 58 """ 59 def __init__(self, host, hostname, domain='local'): 60 """Initializes the ZeroconfDameon running on the given host. 61 62 For the purposes of the Zeroconf implementation, a host must have a 63 hostname and a domain that defaults to 'local'. The ZeroconfDaemon will 64 by default advertise the host address it is running on, which is 65 required by some services. 66 67 @param host: The Host instance where this daemon runs on. 68 @param hostname: A string representing the hostname 69 """ 70 self._host = host 71 self._hostname = hostname 72 self._domain = domain 73 self._response_ttl = 60 # Default TTL in seconds. 74 75 self._a_records = {} # Local A records. 76 self._srv_records = {} # Local SRV records. 77 self._ptr_records = {} # Local PTR records. 78 self._txt_records = {} # Local TXT records. 79 80 # dict() of name --> (dict() of type --> (dict() of data --> timeout)) 81 # For example: _peer_records['somehost.local'][dpkt.dns.DNS_A] \ 82 # ['192.168.0.1'] = time.time() + 3600 83 self._peer_records = {} 84 85 # Register the host address locally. 86 self.register_A(self.full_hostname, host.ip_addr) 87 88 # Attend all the traffic to the mDNS port (unicast, multicast or 89 # broadcast). 90 self._sock = host.socket(socket.AF_INET, socket.SOCK_DGRAM) 91 self._sock.listen(MDNS_IP_ADDR, MDNS_PORT, self._mdns_request) 92 93 # Observer list for new responses. 94 self._answer_callbacks = [] 95 96 97 def __del__(self): 98 self._sock.close() 99 100 101 @property 102 def host(self): 103 """The Host object where this daemon is running.""" 104 return self._host 105 106 107 @property 108 def hostname(self): 109 """The hostname part within a domain.""" 110 return self._hostname 111 112 113 @property 114 def domain(self): 115 """The domain where the given hostname is running.""" 116 return self._domain 117 118 119 @property 120 def full_hostname(self): 121 """The full hostname designation including host and domain name.""" 122 return self._hostname + '.' + self._domain 123 124 125 def _mdns_request(self, data, addr, port): 126 """Handles a mDNS multicast packet. 127 128 This method will generate and send a mDNS response to any query 129 for which it has new authoritative information. Called by the Simulator 130 as a callback for every mDNS received packet. 131 132 @param data: The string contained on the UDP message. 133 @param addr: The address where the message comes from. 134 @param port: The port number where the message comes from. 135 """ 136 # Parse the mDNS request using dpkt's DNS module. 137 mdns = dpkt.dns.DNS(data) 138 if mdns.op == 0x0000: # Query 139 QUERY_HANDLERS = { 140 dpkt.dns.DNS_A: self._process_A, 141 dpkt.dns.DNS_PTR: self._process_PTR, 142 dpkt.dns.DNS_TXT: self._process_TXT, 143 dpkt.dns.DNS_SRV: self._process_SRV, 144 } 145 146 answers = [] 147 for q in mdns.qd: # Query entries 148 if q.type in QUERY_HANDLERS: 149 answers += QUERY_HANDLERS[q.type](q) 150 elif q.type == dpkt.dns.DNS_ANY: 151 # Special type matching any known type. 152 for _, handler in QUERY_HANDLERS.iteritems(): 153 answers += handler(q) 154 # Remove all the already known answers from the list. 155 answers = [ans for ans in answers if not any(True 156 for known_ans in mdns.an if _RR_equals(known_ans, ans))] 157 158 self._send_answers(answers) 159 160 # Always process the received authoritative answers. 161 answers = mdns.ns 162 163 # Process the answers for response packets. 164 if mdns.op == 0x8400: # Standard response 165 answers.extend(mdns.an) 166 167 if answers: 168 cur_time = time.time() 169 new_answers = [] 170 for rr in answers: # Answers RRs 171 # dpkt decodes the information on different fields depending on 172 # the response type. 173 if rr.type == dpkt.dns.DNS_A: 174 data = socket.inet_ntoa(rr.ip) 175 elif rr.type == dpkt.dns.DNS_PTR: 176 data = rr.ptrname 177 elif rr.type == dpkt.dns.DNS_TXT: 178 data = tuple(rr.text) # Convert the list to a hashable tuple 179 elif rr.type == dpkt.dns.DNS_SRV: 180 data = rr.srvname, rr.priority, rr.weight, rr.port 181 else: 182 continue # Ignore unsupported records. 183 if not rr.name in self._peer_records: 184 self._peer_records[rr.name] = {} 185 # Start a new cache or clear the existing if required. 186 if not rr.type in self._peer_records[rr.name] or ( 187 rr.cls & DNS_CACHE_FLUSH): 188 self._peer_records[rr.name][rr.type] = {} 189 190 new_answers.append((rr.type, rr.name, data)) 191 cached_ans = self._peer_records[rr.name][rr.type] 192 rr_timeout = cur_time + rr.ttl 193 # Update the answer timeout if already cached. 194 if data in cached_ans: 195 cached_ans[data] = max(cached_ans[data], rr_timeout) 196 else: 197 cached_ans[data] = rr_timeout 198 if new_answers: 199 for cbk in self._answer_callbacks: 200 cbk(new_answers) 201 202 203 def clear_cache(self): 204 """Discards all the cached records.""" 205 self._peer_records = {} 206 207 208 def _send_answers(self, answers): 209 """Send a mDNS reply with the provided answers. 210 211 This method uses the undelying Host to send an IP packet with a mDNS 212 response containing the list of answers of the type dpkt.dns.DNS.RR. 213 If the list is empty, no packet is sent. 214 215 @param answers: The list of answers to send. 216 """ 217 if not answers: 218 return 219 logging.debug('Sending response with answers: %r.', answers) 220 resp_dns = dpkt.dns.DNS( 221 op = dpkt.dns.DNS_AA, # Authoritative Answer. 222 rcode = dpkt.dns.DNS_RCODE_NOERR, 223 an = answers) 224 # This property modifies the "op" field: 225 resp_dns.qr = dpkt.dns.DNS_R, # Response. 226 self._sock.send(str(resp_dns), MDNS_IP_ADDR, MDNS_PORT) 227 228 229 ### RFC 2782 - RR for specifying the location of services (DNS SRV). 230 def register_SRV(self, service, proto, priority, weight, port): 231 """Publishes the SRV specified record. 232 233 A SRV record defines a service on a port of a host with given properties 234 like priority and weight. The service has a name of the form 235 "service.proto.domain". The target host, this is, the host where the 236 announced service is running on is set to the host where this zeroconf 237 daemon is running, "hostname.domain". 238 239 @param service: A string with the service name. 240 @param proto: A string with the protocol name, for example "_tcp". 241 @param priority: The service priority number as defined by RFC2782. 242 @param weight: The service weight number as defined by RFC2782. 243 @param port: The port number where the service is running on. 244 """ 245 srvname = service + '.' + proto + '.' + self._domain 246 self._srv_records[srvname] = priority, weight, port 247 248 249 def _process_SRV(self, q): 250 """Process a SRV query provided in |q|. 251 252 @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_SRV. 253 @return: A list of dns.DNS.RR responses to the provided query that can 254 be empty. 255 """ 256 if not q.name in self._srv_records: 257 return [] 258 priority, weight, port = self._srv_records[q.name] 259 full_hostname = self._hostname + '.' + self._domain 260 ans = dpkt.dns.DNS.RR( 261 type = dpkt.dns.DNS_SRV, 262 cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH, 263 ttl = self._response_ttl, 264 name = q.name, 265 srvname = full_hostname, 266 priority = priority, 267 weight = weight, 268 port = port) 269 # The target host (srvname) requires to send an A record with its IP 270 # address. We do this as if a query for it was sent. 271 a_qry = dpkt.dns.DNS.Q(name=full_hostname, type=dpkt.dns.DNS_A) 272 return [ans] + self._process_A(a_qry) 273 274 275 ### RFC 1035 - 3.4.1, Domains Names - A (IPv4 address). 276 def register_A(self, hostname, ip_addr): 277 """Registers an Address record (A) pointing to the given IP addres. 278 279 Records registered with method are assumed authoritative. 280 281 @param hostname: The full host name, for example, "somehost.local". 282 @param ip_addr: The IPv4 address of the host, for example, "192.0.1.1". 283 """ 284 if not hostname in self._a_records: 285 self._a_records[hostname] = [] 286 self._a_records[hostname].append(socket.inet_aton(ip_addr)) 287 288 289 def _process_A(self, q): 290 """Process an A query provided in |q|. 291 292 @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_A. 293 @return: A list of dns.DNS.RR responses to the provided query that can 294 be empty. 295 """ 296 if not q.name in self._a_records: 297 return [] 298 answers = [] 299 for ip_addr in self._a_records[q.name]: 300 answers.append(dpkt.dns.DNS.RR( 301 type = dpkt.dns.DNS_A, 302 cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH, 303 ttl = self._response_ttl, 304 name = q.name, 305 ip = ip_addr)) 306 return answers 307 308 309 ### RFC 1035 - 3.3.12, Domain names - PTR (domain name pointer). 310 def register_PTR(self, domain, destination): 311 """Register a domain pointer record. 312 313 A domain pointer record is simply a pointer to a hostname on the domain. 314 315 @param domain: A domain name that can include a proto name, for 316 example, "_workstation._tcp.local". 317 @param destination: The hostname inside the given domain, for example, 318 "my-desktop". 319 """ 320 if not domain in self._ptr_records: 321 self._ptr_records[domain] = [] 322 self._ptr_records[domain].append(destination) 323 324 325 def _process_PTR(self, q): 326 """Process a PTR query provided in |q|. 327 328 @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_PTR. 329 @return: A list of dns.DNS.RR responses to the provided query that can 330 be empty. 331 """ 332 if not q.name in self._ptr_records: 333 return [] 334 answers = [] 335 for dest in self._ptr_records[q.name]: 336 answers.append(dpkt.dns.DNS.RR( 337 type = dpkt.dns.DNS_PTR, 338 cls = dpkt.dns.DNS_IN, # Don't cache flush for PTR records. 339 ttl = self._response_ttl, 340 name = q.name, 341 ptrname = dest + '.' + q.name)) 342 return answers 343 344 345 ### RFC 1035 - 3.3.14, Domain names - TXT (descriptive text). 346 def register_TXT(self, domain, txt_list, announce=False): 347 """Register a TXT record on a domain with given list of strings. 348 349 A TXT record can hold any list of text entries whos format depends on 350 the domain. This method replaces any previous TXT record previously 351 registered for the given domain. 352 353 @param domain: A domain name that normally can include a proto name and 354 a service or host name. 355 @param txt_list: A list of strings. 356 @param announce: If True, the method will also announce the changes 357 on the network. 358 """ 359 self._txt_records[domain] = txt_list 360 if announce: 361 self._send_answers(self._process_TXT(dpkt.dns.DNS.Q(name=domain))) 362 363 364 def _process_TXT(self, q): 365 """Process a TXT query provided in |q|. 366 367 @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_TXT. 368 @return: A list of dns.DNS.RR responses to the provided query that can 369 be empty. 370 """ 371 if not q.name in self._txt_records: 372 return [] 373 text_list = self._txt_records[q.name] 374 answer = dpkt.dns.DNS.RR( 375 type = dpkt.dns.DNS_TXT, 376 cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH, 377 ttl = self._response_ttl, 378 name = q.name, 379 text = text_list) 380 return [answer] 381 382 383 def register_service(self, unique_prefix, service_type, 384 protocol, port, txt_list): 385 """Register a service in the Avahi style. 386 387 Avahi exposes a convenient set of methods for manipulating "services" 388 which are a trio of PTR, SRV, and TXT records. This is a similar 389 helper method for our daemon. 390 391 @param unique_prefix: string unique prefix of service (part of the 392 canonical name). 393 @param service_type: string type of service (e.g. '_privet'). 394 @param protocol: string protocol to use for service (e.g. '_tcp'). 395 @param port: IP port of service (e.g. 53). 396 @param txt_list: list of txt records (e.g. ['vers=1.0', 'foo']). 397 """ 398 service_name = '.'.join([unique_prefix, service_type]) 399 fq_service_name = '.'.join([service_name, protocol, self._domain]) 400 logging.debug('Registering service=%s on port=%d with txt records=%r', 401 fq_service_name, port, txt_list) 402 self.register_SRV( 403 service_name, protocol, DEFAULT_PRIORITY, DEFAULT_WEIGHT, port) 404 self.register_PTR('.'.join([service_type, protocol, self._domain]), 405 unique_prefix) 406 self.register_TXT(fq_service_name, txt_list) 407 408 409 def cached_results(self, rrname, rrtype, timestamp=None): 410 """Return all the cached results for the requested rrname and rrtype. 411 412 This method is used to request all the received mDNS answers present 413 on the cache that were valid at the provided timestamp or later. 414 Answers received before this timestamp whose TTL isn't long enough to 415 make them valid at the timestamp aren't returned. On the other hand, 416 answers received *after* the provided timestamp will always be 417 considered, even if they weren't known at the provided timestamp point. 418 A timestamp of None will return them all. 419 420 This method allows to retrieve "volatile" answers with a TTL of zero. 421 According to the RFC, these answers should be only considered for the 422 "ongoing" request. To do this, call this method after a few seconds (the 423 request timeout) after calling the send_request() method, passing to 424 this method the returned timestamp. 425 426 @param rrname: The requested domain name. 427 @param rrtype: The DNS record type. For example, dpkt.dns.DNS_TXT. 428 @param timestamp: The request timestamp. See description. 429 @return: The list of matching records of the form (rrname, rrtype, data, 430 timeout). 431 """ 432 if timestamp is None: 433 timestamp = 0 434 if not rrname in self._peer_records: 435 return [] 436 if not rrtype in self._peer_records[rrname]: 437 return [] 438 res = [] 439 for data, data_ts in self._peer_records[rrname][rrtype].iteritems(): 440 if data_ts >= timestamp: 441 res.append(DnsRecord(rrname, rrtype, data, data_ts)) 442 return res 443 444 445 def send_request(self, queries): 446 """Sends a request for the provided rrname and rrtype. 447 448 All the known and valid answers for this request will be included in the 449 non authoritative list of known answers together with the request. This 450 is recommended by the RFC and avoid unnecessary responses. 451 452 @param queries: A list of pairs (rrname, rrtype) where rrname is the 453 domain name you are requesting for and the rrtype is the DNS record 454 type. For example, ('somehost.local', dpkt.dns.DNS_ANY). 455 @return: The timestamp where this request is sent. See cached_results(). 456 """ 457 queries = [dpkt.dns.DNS.Q(name=rrname, type=rrtype) 458 for rrname, rrtype in queries] 459 # TODO(deymo): Inlcude the already known answers on the request. 460 answers = [] 461 mdns = dpkt.dns.DNS( 462 op = dpkt.dns.DNS_QUERY, 463 qd = queries, 464 an = answers) 465 self._sock.send(str(mdns), MDNS_IP_ADDR, MDNS_PORT) 466 return time.time() 467 468 469 def add_answer_observer(self, callback): 470 """Adds the callback to the list of observers for new answers. 471 472 @param callback: A callable object accepting a list of tuples (rrname, 473 rrtype, data) where rrname is the domain name, rrtype the DNS record 474 type and data is the information associated with the answers, similar to 475 what cached_results() returns. 476 """ 477 self._answer_callbacks.append(callback) 478