1# Lint as: python2, python3 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7Programmable testing DHCP server. 8 9Simple DHCP server you can program with expectations of future packets and 10responses to those packets. The server is basically a thin wrapper around a 11server socket with some utility logic to make setting up tests easier. To write 12a test, you start a server, construct a sequence of handling rules. 13 14Handling rules let you set up expectations of future packets of certain types. 15Handling rules are processed in order, and only the first remaining handler 16handles a given packet. In theory you could write the entire test into a single 17handling rule and keep an internal state machine for how far that handler has 18gotten through the test. This would be poor style however. Correct style is to 19write (or reuse) a handler for each packet the server should see, leading us to 20a happy land where any conceivable packet handler has already been written for 21us. 22 23Example usage: 24 25# Start up the DHCP server, which will ignore packets until a test is started 26server = DhcpTestServer(interface=interface_name) 27server.start() 28 29# Given a list of handling rules, start a test with a 30 sec timeout. 30handling_rules = [] 31handling_rules.append(DhcpHandlingRule_RespondToDiscovery(intended_ip, 32 intended_subnet_mask, 33 dhcp_server_ip, 34 lease_time_seconds) 35server.start_test(handling_rules, 30.0) 36 37# Trigger DHCP clients to do various test related actions 38... 39 40# Get results 41server.wait_for_test_to_finish() 42if (server.last_test_passed): 43 ... 44else: 45 ... 46 47 48Note that if you make changes, make sure that the tests in dhcp_unittest.py 49still pass. 50""" 51 52from __future__ import absolute_import 53from __future__ import division 54from __future__ import print_function 55 56import logging 57import six 58from six.moves import range 59import socket 60import threading 61import time 62import traceback 63 64from autotest_lib.client.cros import dhcp_packet 65from autotest_lib.client.cros import dhcp_handling_rule 66 67# From socket.h 68SO_BINDTODEVICE = 25 69 70# These imports are purely for handling of namespaces 71import os 72import subprocess 73from ctypes import CDLL, get_errno 74from ctypes.util import find_library 75 76 77# Let's throw an exception (with formatted error message) in case of 78# 'setns' failure instead of returning an error code 79def errcheck(ret, func, args): 80 if ret == -1: 81 e = get_errno() 82 raise OSError(e, os.strerror(e)) 83 84 85libc = CDLL(find_library('c')) 86libc.setns.errcheck = errcheck 87CLONE_NEWNET = 0x40000000 88 89 90class DhcpTestServer(threading.Thread): 91 def __init__(self, 92 interface=None, 93 ingress_address="<broadcast>", 94 ingress_port=67, 95 broadcast_address="255.255.255.255", 96 broadcast_port=68, 97 namespace=None): 98 super(DhcpTestServer, self).__init__() 99 self._mutex = threading.Lock() 100 self._ingress_address = ingress_address 101 self._ingress_port = ingress_port 102 self._broadcast_port = broadcast_port 103 self._broadcast_address = broadcast_address 104 self._socket = None 105 self._interface = interface 106 self._namespace = namespace 107 self._stopped = False 108 self._test_in_progress = False 109 self._last_test_passed = False 110 self._test_timeout = 0 111 self._handling_rules = [] 112 self._logger = logging.getLogger("dhcp.test_server") 113 self._exception = None 114 self.daemon = False 115 116 @property 117 def stopped(self): 118 with self._mutex: 119 return self._stopped 120 121 @property 122 def is_healthy(self): 123 with self._mutex: 124 return self._socket is not None 125 126 @property 127 def test_in_progress(self): 128 with self._mutex: 129 return self._test_in_progress 130 131 @property 132 def last_test_passed(self): 133 with self._mutex: 134 return self._last_test_passed 135 136 @property 137 def current_rule(self): 138 """ 139 Return the currently active DhcpHandlingRule. 140 """ 141 with self._mutex: 142 return self._handling_rules[0] 143 144 def start(self): 145 """ 146 Start the DHCP server. Only call this once. 147 """ 148 if self.is_alive(): 149 return False 150 self._logger.info("DhcpTestServer started; opening sockets.") 151 try: 152 if self._namespace: 153 self._logger.info("Moving to namespace %s.", self._namespace) 154 # Figure out where the mount bind is - ChromeOS does not 155 # follow standard /var/run/netns path so lets try to be more 156 # generic and get it from runtime 157 tgtpath = subprocess.check_output('mount | grep "netns/%s"' % 158 self._namespace, 159 shell=True).split()[2] 160 self._tgtns = open(tgtpath) 161 self._myns = open('/proc/self/ns/net') 162 libc.setns(self._tgtns.fileno(), CLONE_NEWNET) 163 self._logger.info("Opening socket on '%s' port %d.", 164 self._ingress_address, self._ingress_port) 165 self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 166 self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 167 self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 168 if self._interface is not None: 169 self._logger.info("Binding to %s", self._interface) 170 if six.PY2: 171 self._socket.setsockopt(socket.SOL_SOCKET, SO_BINDTODEVICE, 172 self._interface) 173 else: 174 self._socket.setsockopt( 175 socket.SOL_SOCKET, SO_BINDTODEVICE, 176 self._interface.encode('ISO-8859-1')) 177 self._socket.bind((self._ingress_address, self._ingress_port)) 178 # Wait 100 ms for a packet, then return, thus keeping the thread 179 # active but mostly idle. 180 self._socket.settimeout(0.1) 181 except socket.error as socket_error: 182 self._logger.error("Socket error: %s.", str(socket_error)) 183 self._logger.error(traceback.format_exc()) 184 if not self._socket is None: 185 self._socket.close() 186 self._socket = None 187 self._logger.error("Failed to open server socket. Aborting.") 188 return 189 except OSError as os_err: 190 self._logger.error("System error: %s.", str(os_err)) 191 self._logger.error(traceback.format_exc()) 192 self._logger.error("Failed to change namespace. Aborting.") 193 return 194 finally: 195 if self._namespace: 196 self._tgtns.close() 197 libc.setns(self._myns.fileno(), CLONE_NEWNET) 198 self._myns.close() 199 super(DhcpTestServer, self).start() 200 201 def stop(self): 202 """ 203 Stop the DHCP server and free its socket. 204 """ 205 with self._mutex: 206 self._stopped = True 207 208 def start_test(self, handling_rules, test_timeout_seconds): 209 """ 210 Start a new test using |handling_rules|. The server will call the 211 test successfull if it receives a RESPONSE_IGNORE_SUCCESS (or 212 RESPONSE_RESPOND_SUCCESS) from a handling_rule before 213 |test_timeout_seconds| passes. If the timeout passes without that 214 message, the server runs out of handling rules, or a handling rule 215 return RESPONSE_FAIL, the test is ended and marked as not passed. 216 217 All packets received before start_test() is called are received and 218 ignored. 219 """ 220 with self._mutex: 221 self._test_timeout = time.time() + test_timeout_seconds 222 self._handling_rules = handling_rules 223 self._test_in_progress = True 224 self._last_test_passed = False 225 self._exception = None 226 227 def wait_for_test_to_finish(self): 228 """ 229 Block on the test finishing in a CPU friendly way. Timeouts, successes, 230 and failures count as finishes. 231 """ 232 while self.test_in_progress: 233 time.sleep(0.1) 234 if self._exception: 235 raise self._exception 236 237 def abort_test(self): 238 """ 239 Abort a test prematurely, counting the test as a failure. 240 """ 241 with self._mutex: 242 self._logger.info("Manually aborting test.") 243 self._end_test_unsafe(False) 244 245 def _teardown(self): 246 with self._mutex: 247 self._socket.close() 248 self._socket = None 249 250 def _end_test_unsafe(self, passed): 251 if not self._test_in_progress: 252 return 253 if passed: 254 self._logger.info("DHCP server says test passed.") 255 else: 256 self._logger.info("DHCP server says test failed.") 257 self._test_in_progress = False 258 self._last_test_passed = passed 259 260 def _send_response_unsafe(self, packet): 261 if packet is None: 262 self._logger.error("Handling rule failed to return a packet.") 263 return False 264 self._logger.debug("Sending response: %s", packet) 265 binary_string = packet.to_binary_string() 266 if binary_string is None or len(binary_string) < 1: 267 self._logger.error("Packet failed to serialize to binary string.") 268 return False 269 270 self._socket.sendto(binary_string, 271 (self._broadcast_address, self._broadcast_port)) 272 return True 273 274 def _loop_body(self): 275 with self._mutex: 276 if self._test_in_progress and self._test_timeout < time.time(): 277 # The test has timed out, so we abort it. However, we should 278 # continue to accept packets, so we fall through. 279 self._logger.error("Test in progress has timed out.") 280 self._end_test_unsafe(False) 281 try: 282 data, _ = self._socket.recvfrom(1024) 283 self._logger.info("Server received packet of length %d.", 284 len(data)) 285 except socket.timeout: 286 # No packets available, lets return and see if the server has 287 # been shut down in the meantime. 288 return 289 290 # Receive packets when no test is in progress, just don't process 291 # them. 292 if not self._test_in_progress: 293 return 294 295 packet = dhcp_packet.DhcpPacket(byte_str=data) 296 if not packet.is_valid: 297 self._logger.warning("Server received an invalid packet over a " 298 "DHCP port?") 299 return 300 301 logging.debug("Server received a DHCP packet: %s.", packet) 302 if len(self._handling_rules) < 1: 303 self._logger.info("No handling rule for packet: %s.", 304 str(packet)) 305 self._end_test_unsafe(False) 306 return 307 308 handling_rule = self._handling_rules[0] 309 response_code = handling_rule.handle(packet) 310 logging.info("Handler gave response: %d", response_code) 311 if response_code & dhcp_handling_rule.RESPONSE_POP_HANDLER: 312 self._handling_rules.pop(0) 313 314 if response_code & dhcp_handling_rule.RESPONSE_HAVE_RESPONSE: 315 for response_instance in range( 316 handling_rule.response_packet_count): 317 response = handling_rule.respond(packet) 318 if not self._send_response_unsafe(response): 319 self._logger.error( 320 "Failed to send packet, ending test.") 321 self._end_test_unsafe(False) 322 return 323 324 if response_code & dhcp_handling_rule.RESPONSE_TEST_FAILED: 325 self._logger.info("Handling rule %s rejected packet %s.", 326 (handling_rule, packet)) 327 self._end_test_unsafe(False) 328 return 329 330 if response_code & dhcp_handling_rule.RESPONSE_TEST_SUCCEEDED: 331 self._end_test_unsafe(True) 332 return 333 334 def run(self): 335 """ 336 Main method of the thread. Never call this directly, since it assumes 337 some setup done in start(). 338 """ 339 with self._mutex: 340 if self._socket is None: 341 self._logger.error("Failed to create server socket, exiting.") 342 return 343 344 self._logger.info("DhcpTestServer entering handling loop.") 345 while not self.stopped: 346 try: 347 self._loop_body() 348 # Python does not have waiting queues on Lock objects. 349 # Give other threads a change to hold the mutex by 350 # forcibly releasing the GIL while we sleep. 351 time.sleep(0.01) 352 except Exception as e: 353 with self._mutex: 354 self._end_test_unsafe(False) 355 self._exception = e 356 with self._mutex: 357 self._end_test_unsafe(False) 358 self._logger.info("DhcpTestServer closing sockets.") 359 self._teardown() 360 self._logger.info("DhcpTestServer exiting.") 361