zeroconf.py revision 2c5c348c740930e6effa3e25441e6ba0080b3221
1732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# Use of this source code is governed by a BSD-style license that can be
3732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# found in the LICENSE file.
4732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
5732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymoimport dpkt
6732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymoimport socket
7e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymoimport time
8732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
9732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
10732dfe9c1124b51b906cb6510f219ad84be58836Alex DeymoMDNS_IP_ADDR = '224.0.0.251'
11732dfe9c1124b51b906cb6510f219ad84be58836Alex DeymoMDNS_PORT = 5353
12732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
13732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo# Value to | to a class value to signal cache flush.
14732dfe9c1124b51b906cb6510f219ad84be58836Alex DeymoDNS_CACHE_FLUSH = 0x8000
15732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
16732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
17732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymodef _RR_equals(rra, rrb):
18732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    """Returns whether the two dpkt.dns.DNS.RR objects are equal."""
19732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    # Compare all the members present in either object and on any RR object.
20732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    keys = set(rra.__dict__.keys() + rrb.__dict__.keys() +
21732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo               dpkt.dns.DNS.RR.__slots__)
22732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    # On RR objects, rdata is packed based on the other members and the final
23732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    # packed string depends on other RR and Q elements on the same DNS/mDNS
24732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    # packet.
25732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    keys.discard('rdata')
26732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    for key in keys:
27732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if hasattr(rra, key) != hasattr(rrb, key):
28732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return False
29732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not hasattr(rra, key):
30732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            continue
31732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if key == 'cls':
32732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo          # cls attribute should be masked for the cache flush bit.
33732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo          if (getattr(rra, key) & ~DNS_CACHE_FLUSH !=
34732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                getattr(rrb, key) & ~DNS_CACHE_FLUSH):
35732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo              return False
36732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        else:
37732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo          if getattr(rra, key) != getattr(rrb, key):
38732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo              return False
39732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    return True
40732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
41732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
42732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymoclass ZeroconfDaemon(object):
43732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    """Implements a simulated Zeroconf daemon running on the given host.
44732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
45732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    This class implements part of the Multicast DNS RFC 6762 able to simulate
46732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    a host exposing services or consuming services over mDNS.
47732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    """
48732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def __init__(self, host, hostname, domain='local'):
49732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Initializes the ZeroconfDameon running on the given host.
50732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
51732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        For the purposes of the Zeroconf implementation, a host must have a
52732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        hostname and a domain that defaults to 'local'. The ZeroconfDaemon will
53732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        by default advertise the host address it is running on, which is
54732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        required by some services.
55732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
56732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param host: The Host instance where this daemon runs on.
57732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param hostname: A string representing the hostname
58732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
59732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._host = host
60732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._hostname = hostname
61732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._domain = domain
62732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._response_ttl = 60 # Default TTL in seconds.
63732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
64732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._a_records = {} # Local A records.
65732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._srv_records = {} # Local SRV records.
66732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._ptr_records = {} # Local PTR records.
67732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._txt_records = {} # Local TXT records.
68732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
69e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        # dict() of name --> (dict() of type --> (dict() of data --> timeout))
70e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        # For example: _peer_records['somehost.local'][dpkt.dns.DNS_A] \
71e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        #     ['192.168.0.1'] = time.time() + 3600
72e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        self._peer_records = {}
73e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
74732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # Register the host address locally.
75732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self.register_A(self.full_hostname, host.ip_addr)
76732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
77732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # Attend all the traffic to the mDNS port (unicast, multicast or
78732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # broadcast).
79732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._sock = host.socket(socket.AF_INET, socket.SOCK_DGRAM)
80732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._sock.listen(MDNS_IP_ADDR, MDNS_PORT, self._mdns_request)
81732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
82c39594fae840955bf530305e9349694be5953c8fAlex Deymo        # Observer list for new responses.
83c39594fae840955bf530305e9349694be5953c8fAlex Deymo        self._answer_callbacks = []
84c39594fae840955bf530305e9349694be5953c8fAlex Deymo
85732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
86732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def __del__(self):
87732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._sock.close()
88732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
89732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
90732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    @property
91732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def host(self):
92732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """The Host object where this daemon is running."""
93732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return self._host
94732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
95732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
96732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    @property
97732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def hostname(self):
98732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """The hostname part within a domain."""
99732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return self._hostname
100732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
101732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
102732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    @property
103732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def domain(self):
104732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """The domain where the given hostname is running."""
105732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return self._domain
106732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
107732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
108732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    @property
109732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def full_hostname(self):
110732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """The full hostname designation including host and domain name."""
111732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return self._hostname + '.' + self._domain
112732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
113732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
114732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _mdns_request(self, data, addr, port):
115732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Handles an Ethernet packet containing a mDNS multicast packet.
116732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
117732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        This method will generate and send a mDNS response to any query
118732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        for which it has new authoritative information. Called by the Simulator
119732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        as a callback for every mDNS received packet.
120732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
121732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param data: The string contained on the UDP message.
122732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param addr: The address where the message comes from.
123732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param port: The port number where the message comes from.
124732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
125732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # Parse the mDNS request using dpkt's DNS module.
126732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        mdns = dpkt.dns.DNS(data)
127732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if mdns.op == 0x0000: # Query
128732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            QUERY_HANDLERS = {
129732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                dpkt.dns.DNS_A: self._process_A,
130732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                dpkt.dns.DNS_PTR: self._process_PTR,
131732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                dpkt.dns.DNS_TXT: self._process_TXT,
132732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                dpkt.dns.DNS_SRV: self._process_SRV,
133732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            }
134732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
135732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            answers = []
136732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            for q in mdns.qd: # Query entries
137732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                if q.type in QUERY_HANDLERS:
138732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                    answers += QUERY_HANDLERS[q.type](q)
139732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                elif q.type == dpkt.dns.DNS_ANY:
140732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                    # Special type matching any known type.
141732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                    for _, handler in QUERY_HANDLERS.iteritems():
142732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                        answers += handler(q)
143732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            # Remove all the already known answers from the list.
144732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            answers = [ans for ans in answers if not any(True
145732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                for known_ans in mdns.an if _RR_equals(known_ans, ans))]
146732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
147732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            self._send_answers(answers)
148e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        elif mdns.op == 0x8400: # Standard response
149e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            cur_time = time.time()
150c39594fae840955bf530305e9349694be5953c8fAlex Deymo            new_answers = []
151e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            for rr in mdns.an: # Answers RRs
152e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                # dpkt decodes the information on different fields depending on
153e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                # the response type.
154e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                if rr.type == dpkt.dns.DNS_A:
155e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    data = socket.inet_ntoa(rr.ip)
156e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                elif rr.type == dpkt.dns.DNS_PTR:
157e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    data = rr.ptrname
158e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                elif rr.type == dpkt.dns.DNS_TXT:
159e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    data = tuple(rr.text) # Convert the list to a hashable tuple
160e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                elif rr.type == dpkt.dns.DNS_SRV:
161e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    data = rr.srvname, rr.priority, rr.weight, rr.port
162e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                else:
163e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    continue # Ignore unsupported records.
164e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                if not rr.name in self._peer_records:
165e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    self._peer_records[rr.name] = {}
166e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                # Start a new cache or clear the existing if required.
167e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                if not rr.type in self._peer_records[rr.name] or (
168e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                        rr.cls & DNS_CACHE_FLUSH):
169e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    self._peer_records[rr.name][rr.type] = {}
170e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
171c39594fae840955bf530305e9349694be5953c8fAlex Deymo                new_answers.append((rr.type, rr.name, data))
172e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                cached_ans = self._peer_records[rr.name][rr.type]
173e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                rr_timeout = cur_time + rr.ttl
174e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                # Update the answer timeout if already cached.
175e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                if data in cached_ans:
176e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    cached_ans[data] = max(cached_ans[data], rr_timeout)
177e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                else:
178e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                    cached_ans[data] = rr_timeout
179c39594fae840955bf530305e9349694be5953c8fAlex Deymo            if new_answers:
180c39594fae840955bf530305e9349694be5953c8fAlex Deymo                for cbk in self._answer_callbacks:
181c39594fae840955bf530305e9349694be5953c8fAlex Deymo                    cbk(new_answers)
182e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
183e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
184e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo    def clear_cache(self):
185e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        """Discards all the cached records."""
186e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        self._peer_records = {}
187732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
188732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
189732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _send_answers(self, answers):
190732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Send a mDNS reply with the provided answers.
191732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
192732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        This method uses the undelying Host to send an IP packet with a mDNS
193732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        response containing the list of answers of the type dpkt.dns.DNS.RR.
194732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        If the list is empty, no packet is sent.
195732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
196732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param answers: The list of answers to send.
197732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
198732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not answers:
199732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return
200732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        resp_dns = dpkt.dns.DNS(
201732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            op = dpkt.dns.DNS_AA, # Authoritative Answer.
202732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            rcode = dpkt.dns.DNS_RCODE_NOERR,
203732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            an = answers)
204732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # This property modifies the "op" field:
205732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        resp_dns.qr = dpkt.dns.DNS_R, # Response.
206732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._sock.send(str(resp_dns), MDNS_IP_ADDR, MDNS_PORT)
207732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
208732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
209732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    ### RFC 2782 - RR for specifying the location of services (DNS SRV).
210732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def register_SRV(self, service, proto, priority, weight, port):
211732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Publishes the SRV specified record.
212732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
213732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        A SRV record defines a service on a port of a host with given properties
214732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        like priority and weight. The service has a name of the form
215732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        "service.proto.domain". The target host, this is, the host where the
216732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        announced service is running on is set to the host where this zeroconf
217732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        daemon is running, "hostname.domain".
218732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
219732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param service: A string with the service name.
220732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param proto: A string with the protocol name, for example "_tcp".
221732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param priority: The service priority number as defined by RFC2782.
222732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param weight: The service weight number as defined by RFC2782.
223732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param port: The port number where the service is running on.
224732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
225732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        srvname = service + '.' + proto + '.' + self._domain
226732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._srv_records[srvname] = priority, weight, port
227732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
228732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
229732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _process_SRV(self, q):
230732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Process a SRV query provided in |q|.
231732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
232732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_SRV.
233732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @return: A list of dns.DNS.RR responses to the provided query that can
234732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        be empty.
235732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
236732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not q.name in self._srv_records:
237732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return []
238732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        priority, weight, port = self._srv_records[q.name]
239732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        full_hostname = self._hostname + '.' + self._domain
240732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        ans = dpkt.dns.DNS.RR(
241732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            type = dpkt.dns.DNS_SRV,
242732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH,
243732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            ttl = self._response_ttl,
244732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            name = q.name,
245732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            srvname = full_hostname,
246732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            priority = priority,
247732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            weight = weight,
248732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            port = port)
249732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # The target host (srvname) requires to send an A record with its IP
250732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        # address. We do this as if a query for it was sent.
251732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        a_qry = dpkt.dns.DNS.Q(name=full_hostname, type=dpkt.dns.DNS_A)
252732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return [ans] + self._process_A(a_qry)
253732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
254732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
255732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    ### RFC 1035 - 3.4.1, Domains Names - A (IPv4 address).
256732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def register_A(self, hostname, ip_addr):
257732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Registers an Address record (A) pointing to the given IP addres.
258732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
259732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        Records registered with method are assumed authoritative.
260732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
261732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param hostname: The full host name, for example, "somehost.local".
262732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param ip_addr: The IPv4 address of the host, for example, "192.0.1.1".
263732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
264732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not hostname in self._a_records:
265732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            self._a_records[hostname] = []
266732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._a_records[hostname].append(socket.inet_aton(ip_addr))
267732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
268732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
269732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _process_A(self, q):
270732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Process an A query provided in |q|.
271732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
272732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_A.
273732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @return: A list of dns.DNS.RR responses to the provided query that can
274732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        be empty.
275732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
276732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not q.name in self._a_records:
277732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return []
278732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        answers = []
279732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        for ip_addr in self._a_records[q.name]:
280732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            answers.append(dpkt.dns.DNS.RR(
281732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                type = dpkt.dns.DNS_A,
282732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH,
283732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                ttl = self._response_ttl,
284732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                name = q.name,
285732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                ip = ip_addr))
286732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return answers
287732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
288732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
289732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    ### RFC 1035 - 3.3.12, Domain names - PTR (domain name pointer).
290732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def register_PTR(self, domain, destination):
291732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Register a domain pointer record.
292732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
293732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        A domain pointer record is simply a pointer to a hostname on the domain.
294732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
295732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param domain: A domain name that can include a proto name, for
296732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        example, "_workstation._tcp.local".
297732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param destination: The hostname inside the given domain, for example,
298732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        "my-desktop".
299732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
300732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not domain in self._ptr_records:
301732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            self._ptr_records[domain] = []
302732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._ptr_records[domain].append(destination)
303732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
304732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
305732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _process_PTR(self, q):
306732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Process a PTR query provided in |q|.
307732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
308732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_PTR.
309732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @return: A list of dns.DNS.RR responses to the provided query that can
310732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        be empty.
311732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
312732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not q.name in self._ptr_records:
313732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return []
314732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        answers = []
315732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        for dest in self._ptr_records[q.name]:
316732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            answers.append(dpkt.dns.DNS.RR(
317732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                type = dpkt.dns.DNS_PTR,
3188cbb993ebc2483d0576ecefbd05d425abd91b5d9Alex Deymo                cls = dpkt.dns.DNS_IN, # Don't cache flush for PTR records.
319732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                ttl = self._response_ttl,
320732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                name = q.name,
321732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo                ptrname = dest + '.' + q.name))
322732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return answers
323732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
324732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
325732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    ### RFC 1035 - 3.3.14, Domain names - TXT (descriptive text).
3262c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo    def register_TXT(self, domain, txt_list, announce=False):
327732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Register a TXT record on a domain with given list of strings.
328732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
329732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        A TXT record can hold any list of text entries whos format depends on
330732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        the domain. This method replaces any previous TXT record previously
331732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        registered for the given domain.
332732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
333732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param domain: A domain name that normally can include a proto name and
334732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        a service or host name.
335732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param txt_list: A list of strings.
3362c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo        @param announce: If True, the method will also announce the changes
3372c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo        on the network.
338732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
339732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        self._txt_records[domain] = txt_list
3402c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo        if announce:
3412c5c348c740930e6effa3e25441e6ba0080b3221Alex Deymo            self._send_answers(self._process_TXT(dpkt.dns.DNS.Q(name=domain)))
342732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
343732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
344732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo    def _process_TXT(self, q):
345732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """Process a TXT query provided in |q|.
346732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo
347732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @param q: The dns.DNS.Q query object with type dpkt.dns.DNS_TXT.
348732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        @return: A list of dns.DNS.RR responses to the provided query that can
349732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        be empty.
350732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        """
351732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        if not q.name in self._txt_records:
352732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            return []
353732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        text_list = self._txt_records[q.name]
354732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        answer = dpkt.dns.DNS.RR(
355732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            type = dpkt.dns.DNS_TXT,
356732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            cls = dpkt.dns.DNS_IN | DNS_CACHE_FLUSH,
357732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            ttl = self._response_ttl,
358732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            name = q.name,
359732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo            text = text_list)
360732dfe9c1124b51b906cb6510f219ad84be58836Alex Deymo        return [answer]
361e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
362e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
363e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo    def cached_results(self, rrname, rrtype, timestamp=None):
364e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        """Return all the cached results for the requested rrname and rrtype.
365e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
366e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        This method is used to request all the received mDNS answers present
367e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        on the cache that were valid at the provided timestamp or later.
368e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        Answers received before this timestamp whose TTL isn't long enough to
369e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        make them valid at the timestamp aren't returned. On the other hand,
370e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        answers received *after* the provided timestamp will always be
371e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        considered, even if they weren't known at the provided timestamp point.
372e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        A timestamp of None will return them all.
373e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
374e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        This method allows to retrieve "volatile" answers with a TTL of zero.
375e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        According to the RFC, these answers should be only considered for the
376e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        "ongoing" request. To do this, call this method after a few seconds (the
377e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        request timeout) after calling the send_request() method, passing to
378e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        this method the returned timestamp.
379e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo
380e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        @param rrname: The requested domain name.
381e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        @param rrtype: The DNS record type. For example, dpkt.dns.DNS_TXT.
382e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        @param timestamp: The request timestamp. See description.
383e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        @return: The list of matching records of the form (rrname, rrtype, data,
384e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                 timeout).
385e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        """
386e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        if timestamp is None:
387e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            timestamp = 0
388e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        if not rrname in self._peer_records:
389e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            return []
390e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        if not rrtype in self._peer_records[rrname]:
391e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            return []
392e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        res = []
393e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        for data, data_ts in self._peer_records[rrname][rrtype].iteritems():
394e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo            if data_ts >= timestamp:
395e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo                res.append((rrname, rrtype, data, data_ts))
396e24633e8df180e0479e78d641a6643e3ec36845dAlex Deymo        return res
397c39594fae840955bf530305e9349694be5953c8fAlex Deymo
398c39594fae840955bf530305e9349694be5953c8fAlex Deymo
399c39594fae840955bf530305e9349694be5953c8fAlex Deymo    def send_request(self, queries):
400c39594fae840955bf530305e9349694be5953c8fAlex Deymo        """Sends a request for the provided rrname and rrtype.
401c39594fae840955bf530305e9349694be5953c8fAlex Deymo
402c39594fae840955bf530305e9349694be5953c8fAlex Deymo        All the known and valid answers for this request will be included in the
403c39594fae840955bf530305e9349694be5953c8fAlex Deymo        non authoritative list of known answers together with the request. This
404c39594fae840955bf530305e9349694be5953c8fAlex Deymo        is recommended by the RFC and avoid unnecessary responses.
405c39594fae840955bf530305e9349694be5953c8fAlex Deymo
406c39594fae840955bf530305e9349694be5953c8fAlex Deymo        @param queries: A list of pairs (rrname, rrtype) where rrname is the
407c39594fae840955bf530305e9349694be5953c8fAlex Deymo        domain name you are requesting for and the rrtype is the DNS record
408c39594fae840955bf530305e9349694be5953c8fAlex Deymo        type. For example, ('somehost.local', dpkt.dns.DNS_ANY).
409c39594fae840955bf530305e9349694be5953c8fAlex Deymo        @return: The timestamp where this request is sent. See cached_results().
410c39594fae840955bf530305e9349694be5953c8fAlex Deymo        """
411c39594fae840955bf530305e9349694be5953c8fAlex Deymo        queries = [dpkt.dns.DNS.Q(name=rrname, type=rrtype)
412c39594fae840955bf530305e9349694be5953c8fAlex Deymo                for rrname, rrtype in queries]
413c39594fae840955bf530305e9349694be5953c8fAlex Deymo        # TODO(deymo): Inlcude the already known answers on the request.
414c39594fae840955bf530305e9349694be5953c8fAlex Deymo        answers = []
415c39594fae840955bf530305e9349694be5953c8fAlex Deymo        mdns = dpkt.dns.DNS(
416c39594fae840955bf530305e9349694be5953c8fAlex Deymo            op = dpkt.dns.DNS_QUERY,
417c39594fae840955bf530305e9349694be5953c8fAlex Deymo            qd = queries,
418c39594fae840955bf530305e9349694be5953c8fAlex Deymo            an = answers)
419c39594fae840955bf530305e9349694be5953c8fAlex Deymo        self._sock.send(str(mdns), MDNS_IP_ADDR, MDNS_PORT)
420c39594fae840955bf530305e9349694be5953c8fAlex Deymo        return time.time()
421c39594fae840955bf530305e9349694be5953c8fAlex Deymo
422c39594fae840955bf530305e9349694be5953c8fAlex Deymo
423c39594fae840955bf530305e9349694be5953c8fAlex Deymo    def add_answer_observer(self, callback):
424c39594fae840955bf530305e9349694be5953c8fAlex Deymo        """Adds the callback to the list of observers for new answers.
425c39594fae840955bf530305e9349694be5953c8fAlex Deymo
426c39594fae840955bf530305e9349694be5953c8fAlex Deymo        @param callback: A callable object accepting a list of tuples (rrname,
427c39594fae840955bf530305e9349694be5953c8fAlex Deymo        rrtype, data) where rrname is the domain name, rrtype the DNS record
428c39594fae840955bf530305e9349694be5953c8fAlex Deymo        type and data is the information associated with the answers, similar to
429c39594fae840955bf530305e9349694be5953c8fAlex Deymo        what cached_results() returns.
430c39594fae840955bf530305e9349694be5953c8fAlex Deymo        """
431c39594fae840955bf530305e9349694be5953c8fAlex Deymo        self._answer_callbacks.append(callback)
432