1# Copyright 2014 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 dpkt
6import logging
7import time
8
9from autotest_lib.client.bin import test
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.common_lib.cros.tendo import peerd_config
12from autotest_lib.client.cros import chrooted_avahi
13from autotest_lib.client.cros.netprotos import interface_host
14from autotest_lib.client.cros.netprotos import zeroconf
15from autotest_lib.client.cros.tendo import peerd_dbus_helper
16
17
18class peerd_HandlesNameConflicts(test.test):
19    """Test that peerd can handle mDNS name conflicts."""
20    version = 1
21
22    CACHE_REFRESH_PERIOD_SECONDS = 5
23    FAKE_HOST_HOSTNAME = 'a-test-host'
24    TEST_TIMEOUT_SECONDS = 30
25    TEST_SERVICE_ID = 'test-service-0'
26    TEST_SERVICE_INFO = {'some_data': 'a value',
27                          'other_data': 'another value'}
28    INITIAL_MDNS_PREFIX = 'initial_mdns_prefix'
29    SERBUS_SERVICE_ID = 'serbus'
30    SERBUS_SERVICE_NAME = '_serbus'
31    SERBUS_PROTOCOL = '_tcp'
32    SERBUS_PORT = 0
33
34
35    def reset_peerd(self):
36        """Start up a peerd instance.
37
38        This instance will have really verbose logging and will attempt
39        to use a known MDNS prefix to start out.
40
41        """
42        self._peerd = peerd_dbus_helper.make_helper(
43                peerd_config.PeerdConfig(verbosity_level=3,
44                                         mdns_prefix=self.INITIAL_MDNS_PREFIX))
45
46
47    def initialize(self):
48        # Make sure these are initiallized to None in case we throw
49        # during self.initialize().
50        self._chrooted_avahi = None
51        self._peerd = None
52        self._host = None
53        self._zc_listener = None
54        self._chrooted_avahi = chrooted_avahi.ChrootedAvahi()
55        self._chrooted_avahi.start()
56        self.reset_peerd()
57        # Listen on our half of the interface pair for mDNS advertisements.
58        self._host = interface_host.InterfaceHost(
59                self._chrooted_avahi.unchrooted_interface_name)
60        self._zc_listener = zeroconf.ZeroconfDaemon(self._host,
61                                                    self.FAKE_HOST_HOSTNAME)
62        # The queries for hostname/dns_domain are IPCs and therefor relatively
63        # expensive.  Do them just once.
64        hostname = self._chrooted_avahi.hostname
65        dns_domain = self._chrooted_avahi.dns_domain
66        if not hostname or not dns_domain:
67            raise error.TestFail('Failed to get hostname/domain from avahi.')
68        self._dns_domain = dns_domain
69        self._hostname = '%s.%s' % (hostname, dns_domain)
70        self._last_cache_refresh_seconds = 0
71
72
73    def cleanup(self):
74        for obj in (self._chrooted_avahi,
75                    self._host,
76                    self._peerd):
77            if obj is not None:
78                obj.close()
79
80
81    def _get_PTR_prefix(self, service_id):
82        ptr_name = '_%s._tcp.%s' % (service_id, self._dns_domain)
83        found_records = self._zc_listener.cached_results(
84                ptr_name, dpkt.dns.DNS_PTR)
85        if len(found_records) == 0:
86            logging.debug('Found no PTR records for %s', ptr_name)
87            return None
88        if len(found_records) > 1:
89            logging.debug('Found multiple PTR records for %s', ptr_name)
90            return None
91        unique_name = found_records[0].data
92        expected_suffix = '.' + ptr_name
93        if not unique_name.endswith(expected_suffix):
94            logging.error('PTR record for "%s" points to odd name: "%s"',
95                          ptr_name, unique_name)
96            return None
97        ptr_prefix = unique_name[0:-len(expected_suffix)]
98        logging.debug('PTR record for "%s" points to service with name "%s"',
99                      ptr_name, ptr_prefix)
100        return ptr_prefix
101
102
103    def _found_expected_PTR_records(self, forbidden_record_prefix):
104        for service_id in (self.SERBUS_SERVICE_ID, self.TEST_SERVICE_ID):
105            prefix = self._get_PTR_prefix(service_id)
106            if prefix is None:
107                break
108            if prefix == forbidden_record_prefix:
109                logging.debug('Ignoring service with conflicting prefix')
110                break
111        else:
112            return True
113        delta_seconds = time.time() - self._last_cache_refresh_seconds
114        if delta_seconds > self.CACHE_REFRESH_PERIOD_SECONDS:
115            self._zc_listener.clear_cache()
116            self._last_cache_refresh_seconds = time.time()
117        return False
118
119
120    def run_once(self):
121        # Tell peerd about this exciting new service we have.
122        self._peerd.expose_service(self.TEST_SERVICE_ID, self.TEST_SERVICE_INFO)
123        # Wait for advertisements of that service to appear from avahi.
124        # They should be prefixed with our special name, since there are no
125        # conflicts.
126        logging.info('Waiting to receive mDNS advertisements of '
127                     'peerd services.')
128        success, duration = self._host.run_until(
129                lambda: self._found_expected_PTR_records(None),
130                self.TEST_TIMEOUT_SECONDS)
131        if not success:
132            raise error.TestFail('Did not receive mDNS advertisements in time.')
133        actual_prefix = self._get_PTR_prefix(self.SERBUS_SERVICE_ID)
134        if actual_prefix != self.INITIAL_MDNS_PREFIX:
135            raise error.TestFail('Expected initial mDNS advertisements to have '
136                                 'a prefix=%s' % self.INITIAL_MDNS_PREFIX)
137        logging.info('Found initial records advertised by peerd.')
138        # Now register services with the same name, and restart peerd.
139        # 1) The old instance of peerd should withdraw its services from Avahi
140        # 2) The new instance of peerd should re-register services with Avahi
141        # 3) Avahi should notice that the name.service_type.domain tuple
142        #    conflicts with existing records, and signal this to peerd.
143        # 4) Peerd should pick a new prefix and try again.
144        self.reset_peerd()
145        self._zc_listener.register_service(
146                self.INITIAL_MDNS_PREFIX,
147                self.SERBUS_SERVICE_NAME,
148                self.SERBUS_PROTOCOL,
149                self.SERBUS_PORT,
150                ['invalid=record'])
151        self._peerd.expose_service(self.TEST_SERVICE_ID, self.TEST_SERVICE_INFO)
152        run_until_predicate = lambda: self._found_expected_PTR_records(
153                self.INITIAL_MDNS_PREFIX)
154        success, duration = self._host.run_until(run_until_predicate,
155                                                 self.TEST_TIMEOUT_SECONDS)
156        if not success:
157            raise error.TestFail('Timed out waiting for peerd to change the '
158                                 'record prefix.')
159