1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Lifrom __future__ import absolute_import 7*9c5db199SXin Lifrom __future__ import division 8*9c5db199SXin Lifrom __future__ import print_function 9*9c5db199SXin Li 10*9c5db199SXin Liimport errno 11*9c5db199SXin Liimport os 12*9c5db199SXin Liimport shutil 13*9c5db199SXin Liimport six 14*9c5db199SXin Liimport time 15*9c5db199SXin Li 16*9c5db199SXin Lifrom autotest_lib.client.bin import utils 17*9c5db199SXin Li 18*9c5db199SXin Liclass NetworkChroot(object): 19*9c5db199SXin Li """Implements a chroot environment that runs in a separate network 20*9c5db199SXin Li namespace from the caller. This is useful for network tests that 21*9c5db199SXin Li involve creating a server on the other end of a virtual ethernet 22*9c5db199SXin Li pair. This object is initialized with an interface name to pass 23*9c5db199SXin Li to the chroot, as well as the IP address to assign to this 24*9c5db199SXin Li interface, since in passing the interface into the chroot, any 25*9c5db199SXin Li pre-configured address is removed. 26*9c5db199SXin Li 27*9c5db199SXin Li The startup of the chroot is an orchestrated process where a 28*9c5db199SXin Li small startup script is run to perform the following tasks: 29*9c5db199SXin Li - Write out pid file which will be a handle to the 30*9c5db199SXin Li network namespace that that |interface| should be passed to. 31*9c5db199SXin Li - Wait for the network namespace to be passed in, by performing 32*9c5db199SXin Li a "sleep" and writing the pid of this process as well. Our 33*9c5db199SXin Li parent will kill this process to resume the startup process. 34*9c5db199SXin Li - We can now configure the network interface with an address. 35*9c5db199SXin Li - At this point, we can now start any user-requested server 36*9c5db199SXin Li processes. 37*9c5db199SXin Li """ 38*9c5db199SXin Li BIND_ROOT_DIRECTORIES = ('bin', 'dev', 'dev/pts', 'lib', 'lib32', 'lib64', 39*9c5db199SXin Li 'proc', 'sbin', 'sys', 'usr', 'usr/local') 40*9c5db199SXin Li # Subset of BIND_ROOT_DIRECTORIES that should be mounted writable. 41*9c5db199SXin Li BIND_ROOT_WRITABLE_DIRECTORIES = frozenset(('dev/pts',)) 42*9c5db199SXin Li # Directories we'll bind mount when we want to bridge DBus namespaces. 43*9c5db199SXin Li # Includes directories containing the system bus socket and machine ID. 44*9c5db199SXin Li DBUS_BRIDGE_DIRECTORIES = ('run/dbus/', 'var/lib/dbus/') 45*9c5db199SXin Li 46*9c5db199SXin Li ROOT_DIRECTORIES = ('etc', 'etc/ssl', 'tmp', 'var', 'var/log', 'run', 47*9c5db199SXin Li 'run/lock') 48*9c5db199SXin Li ROOT_SYMLINKS = ( 49*9c5db199SXin Li ('var/run', '/run'), 50*9c5db199SXin Li ('var/lock', '/run/lock'), 51*9c5db199SXin Li ) 52*9c5db199SXin Li STARTUP = 'etc/chroot_startup.sh' 53*9c5db199SXin Li STARTUP_DELAY_SECONDS = 5 54*9c5db199SXin Li STARTUP_PID_FILE = 'run/vpn_startup.pid' 55*9c5db199SXin Li STARTUP_SLEEPER_PID_FILE = 'run/vpn_sleeper.pid' 56*9c5db199SXin Li COPIED_CONFIG_FILES = [ 57*9c5db199SXin Li 'etc/ld.so.cache', 58*9c5db199SXin Li 'etc/ssl/openssl.cnf.compat' 59*9c5db199SXin Li ] 60*9c5db199SXin Li CONFIG_FILE_TEMPLATES = { 61*9c5db199SXin Li STARTUP: 62*9c5db199SXin Li '#!/bin/sh\n' 63*9c5db199SXin Li 'exec > /var/log/startup.log 2>&1\n' 64*9c5db199SXin Li 'set -x\n' 65*9c5db199SXin Li 'echo $$ > /%(startup-pidfile)s\n' 66*9c5db199SXin Li 'sleep %(startup-delay-seconds)d &\n' 67*9c5db199SXin Li 'echo $! > /%(sleeper-pidfile)s &\n' 68*9c5db199SXin Li 'wait\n' 69*9c5db199SXin Li 'ip addr add %(local-ip-and-prefix)s dev %(local-interface-name)s\n' 70*9c5db199SXin Li 'ip link set %(local-interface-name)s up\n' 71*9c5db199SXin Li # For running strongSwan VPN with flag --with-piddir=/run/ipsec. We 72*9c5db199SXin Li # want to use /run/ipsec for strongSwan runtime data dir instead of 73*9c5db199SXin Li # /run, and the cmdline flag applies to both client and server. 74*9c5db199SXin Li 'mkdir -p /run/ipsec\n' 75*9c5db199SXin Li } 76*9c5db199SXin Li CONFIG_FILE_VALUES = { 77*9c5db199SXin Li 'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE, 78*9c5db199SXin Li 'startup-delay-seconds': STARTUP_DELAY_SECONDS, 79*9c5db199SXin Li 'startup-pidfile': STARTUP_PID_FILE 80*9c5db199SXin Li } 81*9c5db199SXin Li 82*9c5db199SXin Li def __init__(self, interface, address, prefix): 83*9c5db199SXin Li self._interface = interface 84*9c5db199SXin Li 85*9c5db199SXin Li # Copy these values from the class-static since specific instances 86*9c5db199SXin Li # of this class are allowed to modify their contents. 87*9c5db199SXin Li self._bind_root_directories = list(self.BIND_ROOT_DIRECTORIES) 88*9c5db199SXin Li self._root_directories = list(self.ROOT_DIRECTORIES) 89*9c5db199SXin Li self._copied_config_files = list(self.COPIED_CONFIG_FILES) 90*9c5db199SXin Li self._config_file_templates = self.CONFIG_FILE_TEMPLATES.copy() 91*9c5db199SXin Li self._config_file_values = self.CONFIG_FILE_VALUES.copy() 92*9c5db199SXin Li self._env = dict(os.environ) 93*9c5db199SXin Li 94*9c5db199SXin Li self._config_file_values.update({ 95*9c5db199SXin Li 'local-interface-name': interface, 96*9c5db199SXin Li 'local-ip': address, 97*9c5db199SXin Li 'local-ip-and-prefix': '%s/%d' % (address, prefix) 98*9c5db199SXin Li }) 99*9c5db199SXin Li 100*9c5db199SXin Li 101*9c5db199SXin Li def startup(self): 102*9c5db199SXin Li """Create the chroot and start user processes.""" 103*9c5db199SXin Li self.make_chroot() 104*9c5db199SXin Li self.write_configs() 105*9c5db199SXin Li self.run(['/bin/bash', os.path.join('/', self.STARTUP), '&']) 106*9c5db199SXin Li self.move_interface_to_chroot_namespace() 107*9c5db199SXin Li self.kill_pid_file(self.STARTUP_SLEEPER_PID_FILE) 108*9c5db199SXin Li 109*9c5db199SXin Li 110*9c5db199SXin Li def shutdown(self): 111*9c5db199SXin Li """Remove the chroot filesystem in which the VPN server was running""" 112*9c5db199SXin Li # TODO(pstew): Some processes take a while to exit, which will cause 113*9c5db199SXin Li # the cleanup below to fail to complete successfully... 114*9c5db199SXin Li time.sleep(10) 115*9c5db199SXin Li utils.system_output('rm -rf --one-file-system %s' % self._temp_dir, 116*9c5db199SXin Li ignore_status=True) 117*9c5db199SXin Li 118*9c5db199SXin Li 119*9c5db199SXin Li def add_config_templates(self, template_dict): 120*9c5db199SXin Li """Add a filename-content dict to the set of templates for the chroot 121*9c5db199SXin Li 122*9c5db199SXin Li @param template_dict dict containing filename-content pairs for 123*9c5db199SXin Li templates to be applied to the chroot. The keys to this dict 124*9c5db199SXin Li should not contain a leading '/'. 125*9c5db199SXin Li 126*9c5db199SXin Li """ 127*9c5db199SXin Li self._config_file_templates.update(template_dict) 128*9c5db199SXin Li 129*9c5db199SXin Li 130*9c5db199SXin Li def add_config_values(self, value_dict): 131*9c5db199SXin Li """Add a name-value dict to the set of values for the config template 132*9c5db199SXin Li 133*9c5db199SXin Li @param value_dict dict containing key-value pairs of values that will 134*9c5db199SXin Li be applied to the config file templates. 135*9c5db199SXin Li 136*9c5db199SXin Li """ 137*9c5db199SXin Li self._config_file_values.update(value_dict) 138*9c5db199SXin Li 139*9c5db199SXin Li 140*9c5db199SXin Li def add_copied_config_files(self, files): 141*9c5db199SXin Li """Add |files| to the set to be copied to the chroot. 142*9c5db199SXin Li 143*9c5db199SXin Li @param files iterable object containing a list of files to 144*9c5db199SXin Li be copied into the chroot. These elements should not contain a 145*9c5db199SXin Li leading '/'. 146*9c5db199SXin Li 147*9c5db199SXin Li """ 148*9c5db199SXin Li self._copied_config_files += files 149*9c5db199SXin Li 150*9c5db199SXin Li 151*9c5db199SXin Li def add_root_directories(self, directories): 152*9c5db199SXin Li """Add |directories| to the set created within the chroot. 153*9c5db199SXin Li 154*9c5db199SXin Li @param directories list/tuple containing a list of directories to 155*9c5db199SXin Li be created in the chroot. These elements should not contain a 156*9c5db199SXin Li leading '/'. 157*9c5db199SXin Li 158*9c5db199SXin Li """ 159*9c5db199SXin Li self._root_directories += directories 160*9c5db199SXin Li 161*9c5db199SXin Li 162*9c5db199SXin Li def add_startup_command(self, command): 163*9c5db199SXin Li """Add a command to the script run when the chroot starts up. 164*9c5db199SXin Li 165*9c5db199SXin Li @param command string containing the command line to run. 166*9c5db199SXin Li 167*9c5db199SXin Li """ 168*9c5db199SXin Li self._config_file_templates[self.STARTUP] += '%s\n' % command 169*9c5db199SXin Li 170*9c5db199SXin Li 171*9c5db199SXin Li def add_environment(self, env_dict): 172*9c5db199SXin Li """Add variables to the chroot environment. 173*9c5db199SXin Li 174*9c5db199SXin Li @param env_dict dict dictionary containing environment variables 175*9c5db199SXin Li """ 176*9c5db199SXin Li self._env.update(env_dict) 177*9c5db199SXin Li 178*9c5db199SXin Li 179*9c5db199SXin Li def get_log_contents(self): 180*9c5db199SXin Li """Return the logfiles from the chroot.""" 181*9c5db199SXin Li return utils.system_output("head -10000 %s" % 182*9c5db199SXin Li self.chroot_path("var/log/*")) 183*9c5db199SXin Li 184*9c5db199SXin Li 185*9c5db199SXin Li def bridge_dbus_namespaces(self): 186*9c5db199SXin Li """Make the system DBus daemon visible inside the chroot.""" 187*9c5db199SXin Li # Need the system socket and the machine-id. 188*9c5db199SXin Li self._bind_root_directories += self.DBUS_BRIDGE_DIRECTORIES 189*9c5db199SXin Li 190*9c5db199SXin Li 191*9c5db199SXin Li def chroot_path(self, path): 192*9c5db199SXin Li """Returns the the path within the chroot for |path|. 193*9c5db199SXin Li 194*9c5db199SXin Li @param path string filename within the choot. This should not 195*9c5db199SXin Li contain a leading '/'. 196*9c5db199SXin Li 197*9c5db199SXin Li """ 198*9c5db199SXin Li return os.path.join(self._temp_dir, path.lstrip('/')) 199*9c5db199SXin Li 200*9c5db199SXin Li 201*9c5db199SXin Li def get_pid_file(self, pid_file, missing_ok=False): 202*9c5db199SXin Li """Returns the integer contents of |pid_file| in the chroot. 203*9c5db199SXin Li 204*9c5db199SXin Li @param pid_file string containing the filename within the choot 205*9c5db199SXin Li to read and convert to an integer. This should not contain a 206*9c5db199SXin Li leading '/'. 207*9c5db199SXin Li @param missing_ok bool indicating whether exceptions due to failure 208*9c5db199SXin Li to open the pid file should be caught. If true a missing pid 209*9c5db199SXin Li file will cause this method to return 0. If false, a missing 210*9c5db199SXin Li pid file will cause an exception. 211*9c5db199SXin Li 212*9c5db199SXin Li """ 213*9c5db199SXin Li chroot_pid_file = self.chroot_path(pid_file) 214*9c5db199SXin Li try: 215*9c5db199SXin Li with open(chroot_pid_file) as f: 216*9c5db199SXin Li return int(f.read()) 217*9c5db199SXin Li except IOError as e: 218*9c5db199SXin Li if not missing_ok or e.errno != errno.ENOENT: 219*9c5db199SXin Li raise e 220*9c5db199SXin Li 221*9c5db199SXin Li return 0 222*9c5db199SXin Li 223*9c5db199SXin Li 224*9c5db199SXin Li def kill_pid_file(self, pid_file, missing_ok=False): 225*9c5db199SXin Li """Kills the process belonging to |pid_file| in the chroot. 226*9c5db199SXin Li 227*9c5db199SXin Li @param pid_file string filename within the chroot to gain the process ID 228*9c5db199SXin Li which this method will kill. 229*9c5db199SXin Li @param missing_ok bool indicating whether a missing pid file is okay, 230*9c5db199SXin Li and should be ignored. 231*9c5db199SXin Li 232*9c5db199SXin Li """ 233*9c5db199SXin Li pid = self.get_pid_file(pid_file, missing_ok=missing_ok) 234*9c5db199SXin Li if missing_ok and pid == 0: 235*9c5db199SXin Li return 236*9c5db199SXin Li utils.system('kill %d' % pid, ignore_status=True) 237*9c5db199SXin Li 238*9c5db199SXin Li 239*9c5db199SXin Li def make_chroot(self): 240*9c5db199SXin Li """Make a chroot filesystem.""" 241*9c5db199SXin Li self._temp_dir = utils.system_output( 242*9c5db199SXin Li 'mktemp -d /usr/local/tmp/chroot.XXXXXXXXX') 243*9c5db199SXin Li utils.system('chmod go+rX %s' % self._temp_dir) 244*9c5db199SXin Li for rootdir in self._root_directories: 245*9c5db199SXin Li os.mkdir(self.chroot_path(rootdir)) 246*9c5db199SXin Li 247*9c5db199SXin Li self._jail_args = [] 248*9c5db199SXin Li for rootdir in self._bind_root_directories: 249*9c5db199SXin Li src_path = os.path.join('/', rootdir) 250*9c5db199SXin Li dst_path = self.chroot_path(rootdir) 251*9c5db199SXin Li if not os.path.exists(src_path): 252*9c5db199SXin Li continue 253*9c5db199SXin Li elif os.path.islink(src_path): 254*9c5db199SXin Li link_path = os.readlink(src_path) 255*9c5db199SXin Li os.symlink(link_path, dst_path) 256*9c5db199SXin Li else: 257*9c5db199SXin Li os.makedirs(dst_path) # Recursively create directories. 258*9c5db199SXin Li mount_arg = '%s,%s' % (src_path, src_path) 259*9c5db199SXin Li if rootdir in self.BIND_ROOT_WRITABLE_DIRECTORIES: 260*9c5db199SXin Li mount_arg += ',1' 261*9c5db199SXin Li self._jail_args += [ '-b', mount_arg ] 262*9c5db199SXin Li 263*9c5db199SXin Li for config_file in self._copied_config_files: 264*9c5db199SXin Li src_path = os.path.join('/', config_file) 265*9c5db199SXin Li dst_path = self.chroot_path(config_file) 266*9c5db199SXin Li if os.path.exists(src_path): 267*9c5db199SXin Li shutil.copyfile(src_path, dst_path) 268*9c5db199SXin Li 269*9c5db199SXin Li for src_path, target_path in self.ROOT_SYMLINKS: 270*9c5db199SXin Li link_path = self.chroot_path(src_path) 271*9c5db199SXin Li os.symlink(target_path, link_path) 272*9c5db199SXin Li 273*9c5db199SXin Li 274*9c5db199SXin Li def move_interface_to_chroot_namespace(self): 275*9c5db199SXin Li """Move network interface to the network namespace of the server.""" 276*9c5db199SXin Li utils.system('ip link set %s netns %d' % 277*9c5db199SXin Li (self._interface, 278*9c5db199SXin Li self.get_pid_file(self.STARTUP_PID_FILE))) 279*9c5db199SXin Li 280*9c5db199SXin Li 281*9c5db199SXin Li def run(self, args, ignore_status=False): 282*9c5db199SXin Li """Run a command in a chroot, within a separate network namespace. 283*9c5db199SXin Li 284*9c5db199SXin Li @param args list containing the command line arguments to run. 285*9c5db199SXin Li @param ignore_status bool set to true if a failure should be ignored. 286*9c5db199SXin Li 287*9c5db199SXin Li """ 288*9c5db199SXin Li utils.run('minijail0 -e -C %s %s' % 289*9c5db199SXin Li (self._temp_dir, ' '.join(self._jail_args + args)), 290*9c5db199SXin Li timeout=None, 291*9c5db199SXin Li ignore_status=ignore_status, 292*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, 293*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 294*9c5db199SXin Li env=self._env) 295*9c5db199SXin Li 296*9c5db199SXin Li 297*9c5db199SXin Li def write_configs(self): 298*9c5db199SXin Li """Write out config files""" 299*9c5db199SXin Li for config_file, template in six.iteritems(self._config_file_templates): 300*9c5db199SXin Li with open(self.chroot_path(config_file), 'w') as f: 301*9c5db199SXin Li f.write(template % self._config_file_values) 302