barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 5 | import dbus, logging, os, socket, stat, sys, threading, time |
| 6 | from dbus.mainloop.glib import DBusGMainLoop |
barfab@chromium.org | b6d2993 | 2012-04-11 09:46:43 +0200 | [diff] [blame] | 7 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 8 | import common, constants, flimflam_test_path |
Eric Li | f7b8192 | 2011-03-04 14:39:35 -0800 | [diff] [blame] | 9 | from autotest_lib.client.bin import utils |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 10 | import flimflam # Requires flimflam_test_path to be imported first. |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 11 | |
| 12 | class LocalDns(object): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 13 | """A wrapper around miniFakeDns that runs the server in a separate thread |
| 14 | and redirects all DNS queries to it. |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 15 | """ |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 16 | # This is a symlink. We look up the real path at runtime by following it. |
| 17 | _resolv_bak_file = 'resolv.conf.bak' |
| 18 | |
| 19 | def __connect_to_flimflam(self): |
| 20 | """Connect to the network manager via DBus. |
| 21 | |
| 22 | Stores dbus connection in self._flim upon success, throws on failure. |
| 23 | """ |
| 24 | self._bus_loop = DBusGMainLoop(set_as_default=True) |
| 25 | self._system_bus = dbus.SystemBus(mainloop=self._bus_loop) |
| 26 | self._flim = flimflam.FlimFlam(self._system_bus) |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 27 | |
Chris Masone | 7d04f57 | 2010-05-30 18:07:11 -0700 | [diff] [blame] | 28 | def __init__(self, fake_ip="127.0.0.1", local_port=53): |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 29 | import miniFakeDns # So we don't need to install it in the chroot. |
Chris Masone | e0d3913 | 2011-07-29 19:06:42 -0700 | [diff] [blame] | 30 | self._dns = miniFakeDns.DNSServer(fake_ip=fake_ip, port=local_port) |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 31 | self._stopper = threading.Event() |
| 32 | self._thread = threading.Thread(target=self._dns.run, |
| 33 | args=(self._stopper,)) |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 34 | self.__connect_to_flimflam() |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 35 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 36 | def __get_host_by_name(self, hostname): |
| 37 | """Resolve the dotted-quad IPv4 address of |hostname| |
| 38 | |
| 39 | This used to use suave python code, like this: |
| 40 | hosts = socket.getaddrinfo(hostname, 80, socket.AF_INET) |
| 41 | (fam, socktype, proto, canonname, (host, port)) = hosts[0] |
| 42 | return host |
| 43 | |
| 44 | But that hangs sometimes, and we don't understand why. So, use |
| 45 | a subprocess with a timeout. |
| 46 | """ |
| 47 | try: |
| 48 | host = utils.system_output('%s -c "import socket; ' |
| 49 | 'print socket.gethostbyname(\'%s\')"' % ( |
| 50 | sys.executable, hostname), |
| 51 | ignore_status=True, timeout=2) |
| 52 | except Exception as e: |
| 53 | logging.warning(e) |
| 54 | return None |
| 55 | return host or None |
| 56 | |
| 57 | def __attempt_resolve(self, hostname, ip, expected=True): |
| 58 | logging.debug('Attempting to resolve %s to %s' % (hostname, ip)) |
| 59 | host = self.__get_host_by_name(hostname) |
| 60 | logging.debug('Resolve attempt for %s got %s' % (hostname, host)) |
| 61 | return host and (host == ip) == expected |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 62 | |
| 63 | def run(self): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 64 | """Start the mock DNS server and redirect all queries to it.""" |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 65 | self._thread.start() |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 66 | # Turn off captive portal checking, until we fix |
| 67 | # http://code.google.com/p/chromium-os/issues/detail?id=19640 |
| 68 | self.check_portal_list = self._flim.GetCheckPortalList() |
| 69 | self._flim.SetCheckPortalList('') |
| 70 | # Redirect all DNS queries to the mock DNS server. |
| 71 | try: |
| 72 | # Follow resolv.conf symlink. |
| 73 | resolv = os.path.realpath(constants.RESOLV_CONF_FILE) |
| 74 | # Grab path to the real file, do following work in that directory. |
| 75 | resolv_dir = os.path.dirname(resolv) |
| 76 | resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) |
| 77 | resolv_contents = 'nameserver 127.0.0.1' |
| 78 | # Test to make sure the current resolv.conf isn't already our |
| 79 | # specially modified version. If this is the case, we have |
| 80 | # probably been interrupted while in the middle of this test |
| 81 | # in a previous run. The last thing we want to do at this point |
| 82 | # is to overwrite a legitimate backup. |
| 83 | if (utils.read_one_line(resolv) == resolv_contents and |
| 84 | os.path.exists(resolv_bak)): |
| 85 | logging.error('Current resolv.conf is setup for our local ' |
| 86 | 'server, and a backup already exists! ' |
| 87 | 'Skipping the backup step.') |
| 88 | else: |
| 89 | # Back up the current resolv.conf. |
| 90 | os.rename(resolv, resolv_bak) |
| 91 | # To stop flimflam from editing resolv.conf while we're working |
| 92 | # with it, we want to make the directory -r-xr-xr-x. Open an |
| 93 | # fd to the file first, so that we'll retain the ability to |
| 94 | # alter it. |
| 95 | resolv_fd = open(resolv, 'w') |
| 96 | self._resolv_dir_mode = os.stat(resolv_dir).st_mode |
| 97 | os.chmod(resolv_dir, (stat.S_IRUSR | stat.S_IXUSR | |
| 98 | stat.S_IRGRP | stat.S_IXGRP | |
| 99 | stat.S_IROTH | stat.S_IXOTH)) |
| 100 | resolv_fd.write(resolv_contents) |
| 101 | resolv_fd.close() |
| 102 | assert utils.read_one_line(resolv) == resolv_contents |
| 103 | except Exception as e: |
| 104 | logging.error(str(e)) |
| 105 | raise e |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 106 | |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 107 | utils.poll_for_condition( |
| 108 | lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1'), |
| 109 | utils.TimeoutError('Timed out waiting for DNS changes.'), |
| 110 | timeout=10) |
Chris Masone | f7ae525 | 2010-04-21 08:21:25 -0700 | [diff] [blame] | 111 | |
| 112 | def stop(self): |
Bartosz Fabianowski | 6fe8dee | 2012-06-21 16:35:57 +0200 | [diff] [blame^] | 113 | """Restore the backed-up DNS settings and stop the mock DNS server.""" |
| 114 | try: |
| 115 | # Follow resolv.conf symlink. |
| 116 | resolv = os.path.realpath(constants.RESOLV_CONF_FILE) |
| 117 | # Grab path to the real file, do following work in that directory. |
| 118 | resolv_dir = os.path.dirname(resolv) |
| 119 | resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file) |
| 120 | os.chmod(resolv_dir, self._resolv_dir_mode) |
| 121 | os.rename(resolv_bak, resolv) |
| 122 | |
| 123 | utils.poll_for_condition( |
| 124 | lambda: self.__attempt_resolve('www.google.com.', |
| 125 | '127.0.0.1', |
| 126 | expected=False), |
| 127 | utils.TimeoutError('Timed out waiting to revert DNS. ' |
| 128 | 'resolv.conf contents are: ' + |
| 129 | utils.read_one_line(resolv)), |
| 130 | timeout=10) |
| 131 | finally: |
| 132 | # Set captive portal checking to whatever it was at the start. |
| 133 | self._flim.SetCheckPortalList(self.check_portal_list) |
| 134 | # Stop the DNS server. |
| 135 | self._stopper.set() |
| 136 | self._thread.join() |