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