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