1 # Copyright (c) 2010 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 5 import logging 6 import os 7 8 from autotest_lib.client.bin import test, utils 9 from autotest_lib.client.common_lib import error 10 from autotest_lib.client.common_lib.cros import chrome 11 from autotest_lib.client.common_lib.cros.tendo import webservd_helper 12 13 # Since we parse lsof output in several places, these centralize the 14 # column numbering for finding things in lsof output. For example: 15 # autotest 1915 root 3u IPv4 9221 0t0 TCP *:https (LISTEN) 16 _LSOF_COMMAND = 0 17 _LSOF_PID = 1 18 _LSOF_USER = 2 19 _LSOF_FD = 3 20 _LSOF_TYPE = 4 21 _LSOF_DEVICE = 5 22 _LSOF_NAME = 7 23 # In certain cases, the size/offset column is empty, making it more 24 # reliable to locate the last couple columns by counting from the right. 25 _LSOF_SIZE_OFF = 6 26 _LSOF_NODE = -3 27 _LSOF_NAME = -2 28 29 # Open ports on ARC-enabled test firmwares are different from the non-ARC case 30 # These files provide a whitelist of services expected to listen in each case 31 # (ARC and non-ARC) 32 _BASELINE_DEFAULT_NAME = 'baseline' 33 _BASELINE_ARC_NAME = 'baseline.arc' 34 35 # We log in so that we include any daemons that 36 # might be spawned at login in our test results. 37 class security_NetworkListeners(test.test): 38 """Check the system against a whitelist of expected network-listeners.""" 39 version = 1 40 41 def load_baseline(self, baseline_filename): 42 """Loads the baseline of expected listeners. 43 44 @param baseline_filename: string name of file containing relevant rules. 45 46 """ 47 baseline_path = os.path.join(self.bindir, baseline_filename) 48 with open(baseline_path) as f: 49 lines = [line.strip() for line in f.readlines()] 50 return set([line for line in lines 51 if line and not line.startswith('#')]) 52 53 54 def remove_autotest_noise(self, lsof_lines): 55 """ 56 Processes underneath 'autotest' in the process tree 57 unfortunately can inherit open sockets created by 58 autotest. That leads to crazy-looking test failures where 59 e.g. "sed" and "bash" appear to be listening on ports 60 80/443. So, this takes the output of lsof and returns a 61 filtered subset of it, with autotest and telemetry stuff removed. 62 63 @param lsof_lines: a list of lines as output by the 'lsof' util. 64 """ 65 # Compile a set of the listening sockets to ignore. 66 sockets_to_ignore = set([]) 67 for line in lsof_lines: 68 fields = line.split() 69 if (fields[_LSOF_COMMAND] == 'autotest' or ( 70 fields[_LSOF_COMMAND] == 'python' and 71 fields[_LSOF_NAME].startswith('127.0.0.1:')) or 72 fields[_LSOF_NAME] == '127.0.0.1:%d' % 73 utils.get_chrome_remote_debugging_port()): 74 sockets_to_ignore.add(fields[_LSOF_DEVICE]) 75 76 # Now that we know which ones to ignore, iterate the output again. 77 lines_to_keep = [] 78 for line in lsof_lines: 79 fields = line.split() 80 if fields[_LSOF_DEVICE] in sockets_to_ignore: 81 logging.debug('Ignoring %s', line) 82 else: 83 lines_to_keep.append(line) 84 return lines_to_keep 85 86 87 def run_once(self): 88 """ 89 Compare a list of processes, listening on TCP ports, to a 90 baseline. Test fails if there are mismatches. 91 """ 92 baseline_filename = _BASELINE_DEFAULT_NAME 93 arc_mode = None 94 95 if utils.is_arc_available(): 96 baseline_filename = _BASELINE_ARC_NAME 97 arc_mode = 'enabled' 98 99 with chrome.Chrome(arc_mode=arc_mode): 100 cmd = (r'lsof -n -i -sTCP:LISTEN') 101 cmd_output = utils.system_output(cmd, ignore_status=True, 102 retain_output=True) 103 # Use the [1:] slice to discard line 0, the lsof output header. 104 lsof_lines = cmd_output.splitlines()[1:] 105 # Unlike ps, we don't have a format option so we have to parse 106 # lines that look like this: 107 # sshd 1915 root 3u IPv4 9221 0t0 TCP *:ssh (LISTEN) 108 # Out of that, we just want e.g. sshd *:ssh 109 observed_set = set([]) 110 for line in self.remove_autotest_noise(lsof_lines): 111 fields = line.split() 112 observed_set.add('%s %s' % (fields[_LSOF_COMMAND], 113 fields[_LSOF_NAME])) 114 115 baseline_set = self.load_baseline(baseline_filename) 116 # TODO(wiley) Remove when we get per-board 117 # baselines (crbug.com/406013) 118 if webservd_helper.webservd_is_installed(): 119 baseline_set.update(self.load_baseline('baseline.webservd')) 120 121 # If something in the observed set is not 122 # covered by the baseline... 123 new_listeners = observed_set.difference(baseline_set) 124 if new_listeners: 125 for daemon in new_listeners: 126 logging.error('Unexpected network listener: %s', daemon) 127 128 # Or, things in baseline are missing from the system: 129 missing_listeners = baseline_set.difference(observed_set) 130 if missing_listeners: 131 for daemon in missing_listeners: 132 logging.warning('Missing expected network listener: %s', 133 daemon) 134 135 # Only fail if there's unexpected listeners. 136 if new_listeners: 137 raise error.TestFail('Found unexpected network listeners') 138