1# Copyright (c) 2012 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""" 6DHCP handling rules are ways to record expectations for a DhcpTestServer. 7 8When a handling rule reaches the front of the DhcpTestServer handling rule 9queue, the server begins to ask the rule what it should do with each incoming 10DHCP packet (in the form of a DhcpPacket). The handle() method is expected to 11return a tuple (response, action) where response indicates whether the packet 12should be ignored or responded to and whether the test failed, succeeded, or is 13continuing. The action part of the tuple refers to whether or not the rule 14should be be removed from the test server's handling rule queue. 15""" 16 17import logging 18import time 19 20from autotest_lib.client.cros import dhcp_packet 21 22# Drops the packet and acts like it never happened. 23RESPONSE_NO_ACTION = 0 24# Signals that the handler wishes to send a packet. 25RESPONSE_HAVE_RESPONSE = 1 << 0 26# Signals that the handler wishes to be removed from the handling queue. 27# The handler will be asked to generate a packet first if the handler signalled 28# that it wished to do so with RESPONSE_HAVE_RESPONSE. 29RESPONSE_POP_HANDLER = 1 << 1 30# Signals that the handler wants to end the test on a failure. 31RESPONSE_TEST_FAILED = 1 << 2 32# Signals that the handler wants to end the test because it succeeded. 33# Note that the failure bit has precedence over the success bit. 34RESPONSE_TEST_SUCCEEDED = 1 << 3 35 36class DhcpHandlingRule(object): 37 """ 38 DhcpHandlingRule defines an interface between the DhcpTestServer and 39 subclasses of DhcpHandlingRule. A handling rule at the front of the 40 DhcpTestServer rule queue is first asked what should be done with a packet 41 via handle(). handle() returns a bitfield as described above. If the 42 response from handle() indicates that a packet should be sent in response, 43 the server asks the handling rule to construct a response packet via 44 respond(). 45 """ 46 47 def __init__(self, message_type, additional_options, custom_fields): 48 """ 49 |message_type| should be a MessageType, from DhcpPacket. 50 |additional_options| should be a dictionary that maps from 51 dhcp_packet.OPTION_* to values. For instance: 52 53 {dhcp_packet.OPTION_SERVER_ID : "10.10.10.1"} 54 55 These options are injected into response packets if the client requests 56 it. See inject_options(). 57 """ 58 super(DhcpHandlingRule, self).__init__() 59 self._is_final_handler = False 60 self._logger = logging.getLogger("dhcp.handling_rule") 61 self._options = additional_options 62 self._fields = custom_fields 63 self._target_time_seconds = None 64 self._allowable_time_delta_seconds = 0.5 65 self._force_reply_options = [] 66 self._message_type = message_type 67 self._last_warning = None 68 69 def __str__(self): 70 if self._last_warning: 71 return '%s (%s)' % (self.__class__.__name__, self._last_warning) 72 else: 73 return self.__class__.__name__ 74 75 @property 76 def logger(self): 77 return self._logger 78 79 @property 80 def is_final_handler(self): 81 return self._is_final_handler 82 83 @is_final_handler.setter 84 def is_final_handler(self, value): 85 self._is_final_handler = value 86 87 @property 88 def options(self): 89 """ 90 Returns a dictionary that maps from DhcpPacket options to their values. 91 """ 92 return self._options 93 94 @property 95 def fields(self): 96 """ 97 Returns a dictionary that maps from DhcpPacket fields to their values. 98 """ 99 return self._fields 100 101 @property 102 def target_time_seconds(self): 103 """ 104 If this is not None, packets will be rejected if they don't fall within 105 |self.allowable_time_delta_seconds| seconds of 106 |self.target_time_seconds|. A value of None will cause this handler to 107 ignore the target packet time. 108 109 Defaults to None. 110 """ 111 return self._target_time_seconds 112 113 @target_time_seconds.setter 114 def target_time_seconds(self, value): 115 self._target_time_seconds = value 116 117 @property 118 def allowable_time_delta_seconds(self): 119 """ 120 A configurable fudge factor for |self.target_time_seconds|. If a packet 121 comes in at time T and: 122 123 delta = abs(T - |self.target_time_seconds|) 124 125 Then if delta < |self.allowable_time_delta_seconds|, we accept the 126 packet. Otherwise we either fail the test or ignore the packet, 127 depending on whether this packet is before or after the window. 128 129 Defaults to 0.5 seconds. 130 """ 131 return self._allowable_time_delta_seconds 132 133 @allowable_time_delta_seconds.setter 134 def allowable_time_delta_seconds(self, value): 135 self._allowable_time_delta_seconds = value 136 137 @property 138 def packet_is_too_late(self): 139 if self.target_time_seconds is None: 140 return False 141 delta = time.time() - self.target_time_seconds 142 logging.debug("Handler received packet %0.2f seconds from target time.", 143 delta) 144 if delta > self._allowable_time_delta_seconds: 145 logging.info("Packet was too late for handling (+%0.2f seconds)", 146 delta - self._allowable_time_delta_seconds) 147 return True 148 logging.info("Packet was not too late for handling.") 149 return False 150 151 @property 152 def packet_is_too_soon(self): 153 if self.target_time_seconds is None: 154 return False 155 delta = time.time() - self.target_time_seconds 156 logging.debug("Handler received packet %0.2f seconds from target time.", 157 delta) 158 if -delta > self._allowable_time_delta_seconds: 159 logging.info("Packet arrived too soon for handling: " 160 "(-%0.2f seconds)", 161 -delta - self._allowable_time_delta_seconds) 162 return True 163 logging.info("Packet was not too soon for handling.") 164 return False 165 166 @property 167 def force_reply_options(self): 168 return self._force_reply_options 169 170 @force_reply_options.setter 171 def force_reply_options(self, value): 172 self._force_reply_options = value 173 174 @property 175 def response_packet_count(self): 176 return 1 177 178 def emit_warning(self, warning): 179 """ 180 Log a warning, and retain that warning as |_last_warning|. 181 182 @param warning: The warning message 183 """ 184 self.logger.warning(warning) 185 self._last_warning = warning 186 187 def handle(self, query_packet): 188 """ 189 The DhcpTestServer will call this method to ask a handling rule whether 190 it wants to take some action in response to a packet. The handler 191 should return some combination of RESPONSE_* bits as described above. 192 193 |packet| is a valid DHCP packet, but the values of fields and presence 194 of options is not guaranteed. 195 """ 196 if self.packet_is_too_late: 197 return RESPONSE_TEST_FAILED 198 if self.packet_is_too_soon: 199 return RESPONSE_NO_ACTION 200 return self.handle_impl(query_packet) 201 202 def handle_impl(self, query_packet): 203 logging.error("DhcpHandlingRule.handle_impl() called.") 204 return RESPONSE_TEST_FAILED 205 206 def respond(self, query_packet): 207 """ 208 Called by the DhcpTestServer to generate a packet to send back to the 209 client. This method is called if and only if the response returned from 210 handle() had RESPONSE_HAVE_RESPONSE set. 211 """ 212 return None 213 214 def inject_options(self, packet, requested_parameters): 215 """ 216 Adds options listed in the intersection of |requested_parameters| and 217 |self.options| to |packet|. Also include the options in the 218 intersection of |self.force_reply_options| and |self.options|. 219 220 |packet| is a DhcpPacket. 221 222 |requested_parameters| is a list of options numbers as you would find in 223 a DHCP_DISCOVER or DHCP_REQUEST packet after being parsed by DhcpPacket 224 (e.g. [1, 121, 33, 3, 6, 12]). 225 226 Subclassed handling rules may call this to inject options into response 227 packets to the client. This process emulates a real DHCP server which 228 would have a pool of configuration settings to hand out to DHCP clients 229 upon request. 230 """ 231 for option, value in self.options.items(): 232 if (option.number in requested_parameters or 233 option in self.force_reply_options): 234 packet.set_option(option, value) 235 236 def inject_fields(self, packet): 237 """ 238 Adds fields listed in |self.fields| to |packet|. 239 240 |packet| is a DhcpPacket. 241 242 Subclassed handling rules may call this to inject fields into response 243 packets to the client. This process emulates a real DHCP server which 244 would have a pool of configuration settings to hand out to DHCP clients 245 upon request. 246 """ 247 for field, value in self.fields.items(): 248 packet.set_field(field, value) 249 250 def is_our_message_type(self, packet): 251 """ 252 Checks if the Message Type DHCP Option in |packet| matches the message 253 type handled by this rule. Logs a warning if the types do not match. 254 255 @param packet: a DhcpPacket 256 257 @returns True or False 258 """ 259 if packet.message_type == self._message_type: 260 return True 261 else: 262 self.emit_warning("Packet's message type was %s, not %s." % ( 263 packet.message_type.name, 264 self._message_type.name)) 265 return False 266 267 268class DhcpHandlingRule_RespondToDiscovery(DhcpHandlingRule): 269 """ 270 This handler will accept any DISCOVER packet received by the server. In 271 response to such a packet, the handler will construct an OFFER packet 272 offering |intended_ip| from a server at |server_ip| (from the constructor). 273 """ 274 def __init__(self, 275 intended_ip, 276 server_ip, 277 additional_options, 278 custom_fields, 279 should_respond=True): 280 """ 281 |intended_ip| is an IPv4 address string like "192.168.1.100". 282 283 |server_ip| is an IPv4 address string like "192.168.1.1". 284 285 |additional_options| is handled as explained by DhcpHandlingRule. 286 """ 287 super(DhcpHandlingRule_RespondToDiscovery, self).__init__( 288 dhcp_packet.MESSAGE_TYPE_DISCOVERY, additional_options, 289 custom_fields) 290 self._intended_ip = intended_ip 291 self._server_ip = server_ip 292 self._should_respond = should_respond 293 294 def handle_impl(self, query_packet): 295 if not self.is_our_message_type(query_packet): 296 return RESPONSE_NO_ACTION 297 298 self.logger.info("Received valid DISCOVERY packet. Processing.") 299 ret = RESPONSE_POP_HANDLER 300 if self.is_final_handler: 301 ret |= RESPONSE_TEST_SUCCEEDED 302 if self._should_respond: 303 ret |= RESPONSE_HAVE_RESPONSE 304 return ret 305 306 def respond(self, query_packet): 307 if not self.is_our_message_type(query_packet): 308 return None 309 310 self.logger.info("Responding to DISCOVERY packet.") 311 response_packet = dhcp_packet.DhcpPacket.create_offer_packet( 312 query_packet.transaction_id, 313 query_packet.client_hw_address, 314 self._intended_ip, 315 self._server_ip) 316 requested_parameters = query_packet.get_option( 317 dhcp_packet.OPTION_PARAMETER_REQUEST_LIST) 318 if requested_parameters is not None: 319 self.inject_options(response_packet, requested_parameters) 320 self.inject_fields(response_packet) 321 return response_packet 322 323 324class DhcpHandlingRule_RejectRequest(DhcpHandlingRule): 325 """ 326 This handler receives a REQUEST packet, and responds with a NAK. 327 """ 328 def __init__(self): 329 super(DhcpHandlingRule_RejectRequest, self).__init__( 330 dhcp_packet.MESSAGE_TYPE_REQUEST, {}, {}) 331 self._should_respond = True 332 333 def handle_impl(self, query_packet): 334 if not self.is_our_message_type(query_packet): 335 return RESPONSE_NO_ACTION 336 337 ret = RESPONSE_POP_HANDLER 338 if self.is_final_handler: 339 ret |= RESPONSE_TEST_SUCCEEDED 340 if self._should_respond: 341 ret |= RESPONSE_HAVE_RESPONSE 342 return ret 343 344 def respond(self, query_packet): 345 if not self.is_our_message_type(query_packet): 346 return None 347 348 self.logger.info("NAKing the REQUEST packet.") 349 response_packet = dhcp_packet.DhcpPacket.create_nak_packet( 350 query_packet.transaction_id, query_packet.client_hw_address) 351 return response_packet 352 353 354class DhcpHandlingRule_RespondToRequest(DhcpHandlingRule): 355 """ 356 This handler accepts any REQUEST packet that contains options for SERVER_ID 357 and REQUESTED_IP that match |expected_server_ip| and |expected_requested_ip| 358 respectively. It responds with an ACKNOWLEDGEMENT packet from a DHCP server 359 at |response_server_ip| granting |response_granted_ip| to a client at the 360 address given in the REQUEST packet. If |response_server_ip| or 361 |response_granted_ip| are not given, then they default to 362 |expected_server_ip| and |expected_requested_ip| respectively. 363 """ 364 def __init__(self, 365 expected_requested_ip, 366 expected_server_ip, 367 additional_options, 368 custom_fields, 369 should_respond=True, 370 response_server_ip=None, 371 response_granted_ip=None, 372 expect_server_ip_set=True): 373 """ 374 All *_ip arguments are IPv4 address strings like "192.168.1.101". 375 376 |additional_options| is handled as explained by DhcpHandlingRule. 377 """ 378 super(DhcpHandlingRule_RespondToRequest, self).__init__( 379 dhcp_packet.MESSAGE_TYPE_REQUEST, additional_options, 380 custom_fields) 381 self._expected_requested_ip = expected_requested_ip 382 self._expected_server_ip = expected_server_ip 383 self._should_respond = should_respond 384 self._granted_ip = response_granted_ip 385 self._server_ip = response_server_ip 386 self._expect_server_ip_set = expect_server_ip_set 387 if self._granted_ip is None: 388 self._granted_ip = self._expected_requested_ip 389 if self._server_ip is None: 390 self._server_ip = self._expected_server_ip 391 392 def handle_impl(self, query_packet): 393 if not self.is_our_message_type(query_packet): 394 return RESPONSE_NO_ACTION 395 396 self.logger.info("Received REQUEST packet, checking fields...") 397 server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) 398 if dhcp_packet.OPTION_REQUESTED_IP in self.options: 399 requested_ip = query_packet.get_option( 400 dhcp_packet.OPTION_REQUESTED_IP) 401 else: 402 cli_ip = query_packet.get_field(dhcp_packet.FIELD_CLIENT_IP) 403 if cli_ip != dhcp_packet.IPV4_NULL_ADDRESS: 404 requested_ip = cli_ip 405 else: 406 requested_ip = None 407 server_ip_provided = server_ip is not None 408 if ((server_ip_provided != self._expect_server_ip_set) or 409 (requested_ip is None)): 410 self.logger.info("REQUEST packet did not have the expected " 411 "options, discarding.") 412 return RESPONSE_NO_ACTION 413 414 if server_ip_provided and server_ip != self._expected_server_ip: 415 self.emit_warning("REQUEST packet's server ip did not match our " 416 "expectations; expected %s but got %s" % 417 (self._expected_server_ip, server_ip)) 418 return RESPONSE_NO_ACTION 419 420 if requested_ip != self._expected_requested_ip: 421 self.emit_warning("REQUEST packet's requested IP did not match " 422 "our expectations; expected %s but got %s" % 423 (self._expected_requested_ip, requested_ip)) 424 return RESPONSE_NO_ACTION 425 426 self.logger.info("Received valid REQUEST packet, processing") 427 ret = RESPONSE_POP_HANDLER 428 if self.is_final_handler: 429 ret |= RESPONSE_TEST_SUCCEEDED 430 if self._should_respond: 431 ret |= RESPONSE_HAVE_RESPONSE 432 return ret 433 434 def respond(self, query_packet): 435 if not self.is_our_message_type(query_packet): 436 return None 437 438 self.logger.info("Responding to REQUEST packet.") 439 response_packet = dhcp_packet.DhcpPacket.create_acknowledgement_packet( 440 query_packet.transaction_id, 441 query_packet.client_hw_address, 442 self._granted_ip, 443 self._server_ip) 444 requested_parameters = query_packet.get_option( 445 dhcp_packet.OPTION_PARAMETER_REQUEST_LIST) 446 if requested_parameters is not None: 447 self.inject_options(response_packet, requested_parameters) 448 self.inject_fields(response_packet) 449 return response_packet 450 451 452class DhcpHandlingRule_RespondToPostT2Request( 453 DhcpHandlingRule_RespondToRequest): 454 """ 455 This handler is a lot like DhcpHandlingRule_RespondToRequest except that it 456 expects request packets like those sent after the T2 deadline (see RFC 457 2131). This is the time that you can find a request packet without the 458 SERVER_ID option. It responds to packets in exactly the same way. 459 """ 460 def __init__(self, 461 expected_requested_ip, 462 response_server_ip, 463 additional_options, 464 custom_fields, 465 should_respond=True, 466 response_granted_ip=None): 467 """ 468 All *_ip arguments are IPv4 address strings like "192.168.1.101". 469 470 |additional_options| is handled as explained by DhcpHandlingRule. 471 """ 472 super(DhcpHandlingRule_RespondToPostT2Request, 473 self).__init__(expected_requested_ip, 474 None, 475 additional_options, 476 custom_fields, 477 should_respond=should_respond, 478 response_server_ip=response_server_ip, 479 response_granted_ip=response_granted_ip, 480 expect_server_ip_set=False) 481 482 def handle_impl(self, query_packet): 483 if not self.is_our_message_type(query_packet): 484 return RESPONSE_NO_ACTION 485 486 self.logger.info("Received REQUEST packet, checking fields...") 487 if query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) is not None: 488 self.logger.info("REQUEST packet had a SERVER_ID option, which it " 489 "is not expected to have, discarding.") 490 return RESPONSE_NO_ACTION 491 492 if query_packet.get_option( 493 dhcp_packet.OPTION_REQUESTED_IP) is not None: 494 self.logger.info("REQUEST packet had a REQUESTED_IP_ID option, " 495 "which it is not expected to have, discarding.") 496 return RESPONSE_NO_ACTION 497 498 requested_ip = query_packet.get_field(dhcp_packet.FIELD_CLIENT_IP) 499 if requested_ip == dhcp_packet.IPV4_NULL_ADDRESS: 500 self.logger.info("REQUEST packet did not have the expected " 501 "request ip option at all, discarding.") 502 return RESPONSE_NO_ACTION 503 504 if requested_ip != self._expected_requested_ip: 505 self.emit_warning("REQUEST packet's requested IP did not match " 506 "our expectations; expected %s but got %s" % 507 (self._expected_requested_ip, requested_ip)) 508 return RESPONSE_NO_ACTION 509 510 self.logger.info("Received valid post T2 REQUEST packet, processing") 511 ret = RESPONSE_POP_HANDLER 512 if self.is_final_handler: 513 ret |= RESPONSE_TEST_SUCCEEDED 514 if self._should_respond: 515 ret |= RESPONSE_HAVE_RESPONSE 516 return ret 517 518 519class DhcpHandlingRule_AcceptRelease(DhcpHandlingRule): 520 """ 521 This handler accepts any RELEASE packet that contains an option for 522 SERVER_ID matches |expected_server_ip|. There is no response to this 523 packet. 524 """ 525 def __init__(self, 526 expected_server_ip, 527 additional_options, 528 custom_fields): 529 """ 530 All *_ip arguments are IPv4 address strings like "192.168.1.101". 531 532 |additional_options| is handled as explained by DhcpHandlingRule. 533 """ 534 super(DhcpHandlingRule_AcceptRelease, self).__init__( 535 dhcp_packet.MESSAGE_TYPE_RELEASE, additional_options, 536 custom_fields) 537 self._expected_server_ip = expected_server_ip 538 539 def handle_impl(self, query_packet): 540 if not self.is_our_message_type(query_packet): 541 return RESPONSE_NO_ACTION 542 543 self.logger.info("Received RELEASE packet, checking fields...") 544 server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) 545 if server_ip is None: 546 self.logger.info("RELEASE packet did not have the expected " 547 "options, discarding.") 548 return RESPONSE_NO_ACTION 549 550 if server_ip != self._expected_server_ip: 551 self.emit_warning("RELEASE packet's server ip did not match our " 552 "expectations; expected %s but got %s" % 553 (self._expected_server_ip, server_ip)) 554 return RESPONSE_NO_ACTION 555 556 self.logger.info("Received valid RELEASE packet, processing") 557 ret = RESPONSE_POP_HANDLER 558 if self.is_final_handler: 559 ret |= RESPONSE_TEST_SUCCEEDED 560 return ret 561 562 563class DhcpHandlingRule_RejectAndRespondToRequest( 564 DhcpHandlingRule_RespondToRequest): 565 """ 566 This handler accepts any REQUEST packet that contains options for SERVER_ID 567 and REQUESTED_IP that match |expected_server_ip| and |expected_requested_ip| 568 respectively. It responds with both an ACKNOWLEDGEMENT packet from a DHCP 569 server as well as a NAK, in order to simulate a network with two conflicting 570 servers. 571 """ 572 def __init__(self, 573 expected_requested_ip, 574 expected_server_ip, 575 additional_options, 576 custom_fields, 577 send_nak_before_ack): 578 super(DhcpHandlingRule_RejectAndRespondToRequest, self).__init__( 579 expected_requested_ip, 580 expected_server_ip, 581 additional_options, 582 custom_fields) 583 self._send_nak_before_ack = send_nak_before_ack 584 self._response_counter = 0 585 586 @property 587 def response_packet_count(self): 588 return 2 589 590 def respond(self, query_packet): 591 """ Respond to |query_packet| with a NAK then ACK or ACK then NAK. """ 592 if ((self._response_counter == 0 and self._send_nak_before_ack) or 593 (self._response_counter != 0 and not self._send_nak_before_ack)): 594 response_packet = dhcp_packet.DhcpPacket.create_nak_packet( 595 query_packet.transaction_id, query_packet.client_hw_address) 596 else: 597 response_packet = super(DhcpHandlingRule_RejectAndRespondToRequest, 598 self).respond(query_packet) 599 self._response_counter += 1 600 return response_packet 601 602 603class DhcpHandlingRule_AcceptDecline(DhcpHandlingRule): 604 """ 605 This handler accepts any DECLINE packet that contains an option for 606 SERVER_ID matches |expected_server_ip|. There is no response to this 607 packet. 608 """ 609 def __init__(self, 610 expected_server_ip, 611 additional_options, 612 custom_fields): 613 """ 614 All *_ip arguments are IPv4 address strings like "192.168.1.101". 615 616 |additional_options| is handled as explained by DhcpHandlingRule. 617 """ 618 super(DhcpHandlingRule_AcceptDecline, self).__init__( 619 dhcp_packet.MESSAGE_TYPE_DECLINE, additional_options, 620 custom_fields) 621 self._expected_server_ip = expected_server_ip 622 623 def handle_impl(self, query_packet): 624 if not self.is_our_message_type(query_packet): 625 return RESPONSE_NO_ACTION 626 627 self.logger.info("Received DECLINE packet, checking fields...") 628 server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) 629 if server_ip is None: 630 self.logger.info("DECLINE packet did not have the expected " 631 "options, discarding.") 632 return RESPONSE_NO_ACTION 633 634 if server_ip != self._expected_server_ip: 635 self.emit_warning("DECLINE packet's server ip did not match our " 636 "expectations; expected %s but got %s" % 637 (self._expected_server_ip, server_ip)) 638 return RESPONSE_NO_ACTION 639 640 self.logger.info("Received valid DECLINE packet, processing") 641 ret = RESPONSE_POP_HANDLER 642 if self.is_final_handler: 643 ret |= RESPONSE_TEST_SUCCEEDED 644 return ret 645