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 requested_ip = query_packet.get_option(dhcp_packet.OPTION_REQUESTED_IP) 399 server_ip_provided = server_ip is not None 400 if ((server_ip_provided != self._expect_server_ip_set) or 401 (requested_ip is None)): 402 self.logger.info("REQUEST packet did not have the expected " 403 "options, discarding.") 404 return RESPONSE_NO_ACTION 405 406 if server_ip_provided and server_ip != self._expected_server_ip: 407 self.emit_warning("REQUEST packet's server ip did not match our " 408 "expectations; expected %s but got %s" % 409 (self._expected_server_ip, server_ip)) 410 return RESPONSE_NO_ACTION 411 412 if requested_ip != self._expected_requested_ip: 413 self.emit_warning("REQUEST packet's requested IP did not match " 414 "our expectations; expected %s but got %s" % 415 (self._expected_requested_ip, requested_ip)) 416 return RESPONSE_NO_ACTION 417 418 self.logger.info("Received valid REQUEST packet, processing") 419 ret = RESPONSE_POP_HANDLER 420 if self.is_final_handler: 421 ret |= RESPONSE_TEST_SUCCEEDED 422 if self._should_respond: 423 ret |= RESPONSE_HAVE_RESPONSE 424 return ret 425 426 def respond(self, query_packet): 427 if not self.is_our_message_type(query_packet): 428 return None 429 430 self.logger.info("Responding to REQUEST packet.") 431 response_packet = dhcp_packet.DhcpPacket.create_acknowledgement_packet( 432 query_packet.transaction_id, 433 query_packet.client_hw_address, 434 self._granted_ip, 435 self._server_ip) 436 requested_parameters = query_packet.get_option( 437 dhcp_packet.OPTION_PARAMETER_REQUEST_LIST) 438 if requested_parameters is not None: 439 self.inject_options(response_packet, requested_parameters) 440 self.inject_fields(response_packet) 441 return response_packet 442 443 444class DhcpHandlingRule_RespondToPostT2Request( 445 DhcpHandlingRule_RespondToRequest): 446 """ 447 This handler is a lot like DhcpHandlingRule_RespondToRequest except that it 448 expects request packets like those sent after the T2 deadline (see RFC 449 2131). This is the only time that you can find a request packet without the 450 SERVER_ID option. It responds to packets in exactly the same way. 451 """ 452 def __init__(self, 453 expected_requested_ip, 454 response_server_ip, 455 additional_options, 456 custom_fields, 457 should_respond=True, 458 response_granted_ip=None): 459 """ 460 All *_ip arguments are IPv4 address strings like "192.168.1.101". 461 462 |additional_options| is handled as explained by DhcpHandlingRule. 463 """ 464 super(DhcpHandlingRule_RespondToPostT2Request, self).__init__( 465 expected_requested_ip, 466 None, 467 additional_options, 468 custom_fields, 469 should_respond=should_respond, 470 response_server_ip=response_server_ip, 471 response_granted_ip=response_granted_ip) 472 473 def handle_impl(self, query_packet): 474 if not self.is_our_message_type(query_packet): 475 return RESPONSE_NO_ACTION 476 477 self.logger.info("Received REQUEST packet, checking fields...") 478 if query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) is not None: 479 self.logger.info("REQUEST packet had a SERVER_ID option, which it " 480 "is not expected to have, discarding.") 481 return RESPONSE_NO_ACTION 482 483 requested_ip = query_packet.get_option(dhcp_packet.OPTION_REQUESTED_IP) 484 if requested_ip is None: 485 self.logger.info("REQUEST packet did not have the expected " 486 "request ip option at all, discarding.") 487 return RESPONSE_NO_ACTION 488 489 if requested_ip != self._expected_requested_ip: 490 self.emit_warning("REQUEST packet's requested IP did not match " 491 "our expectations; expected %s but got %s" % 492 (self._expected_requested_ip, requested_ip)) 493 return RESPONSE_NO_ACTION 494 495 self.logger.info("Received valid post T2 REQUEST packet, processing") 496 ret = RESPONSE_POP_HANDLER 497 if self.is_final_handler: 498 ret |= RESPONSE_TEST_SUCCEEDED 499 if self._should_respond: 500 ret |= RESPONSE_HAVE_RESPONSE 501 return ret 502 503 504class DhcpHandlingRule_AcceptRelease(DhcpHandlingRule): 505 """ 506 This handler accepts any RELEASE packet that contains an option for 507 SERVER_ID matches |expected_server_ip|. There is no response to this 508 packet. 509 """ 510 def __init__(self, 511 expected_server_ip, 512 additional_options, 513 custom_fields): 514 """ 515 All *_ip arguments are IPv4 address strings like "192.168.1.101". 516 517 |additional_options| is handled as explained by DhcpHandlingRule. 518 """ 519 super(DhcpHandlingRule_AcceptRelease, self).__init__( 520 dhcp_packet.MESSAGE_TYPE_RELEASE, additional_options, 521 custom_fields) 522 self._expected_server_ip = expected_server_ip 523 524 def handle_impl(self, query_packet): 525 if not self.is_our_message_type(query_packet): 526 return RESPONSE_NO_ACTION 527 528 self.logger.info("Received RELEASE packet, checking fields...") 529 server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) 530 if server_ip is None: 531 self.logger.info("RELEASE packet did not have the expected " 532 "options, discarding.") 533 return RESPONSE_NO_ACTION 534 535 if server_ip != self._expected_server_ip: 536 self.emit_warning("RELEASE packet's server ip did not match our " 537 "expectations; expected %s but got %s" % 538 (self._expected_server_ip, server_ip)) 539 return RESPONSE_NO_ACTION 540 541 self.logger.info("Received valid RELEASE packet, processing") 542 ret = RESPONSE_POP_HANDLER 543 if self.is_final_handler: 544 ret |= RESPONSE_TEST_SUCCEEDED 545 return ret 546 547 548class DhcpHandlingRule_RejectAndRespondToRequest( 549 DhcpHandlingRule_RespondToRequest): 550 """ 551 This handler accepts any REQUEST packet that contains options for SERVER_ID 552 and REQUESTED_IP that match |expected_server_ip| and |expected_requested_ip| 553 respectively. It responds with both an ACKNOWLEDGEMENT packet from a DHCP 554 server as well as a NAK, in order to simulate a network with two conflicting 555 servers. 556 """ 557 def __init__(self, 558 expected_requested_ip, 559 expected_server_ip, 560 additional_options, 561 custom_fields, 562 send_nak_before_ack): 563 super(DhcpHandlingRule_RejectAndRespondToRequest, self).__init__( 564 expected_requested_ip, 565 expected_server_ip, 566 additional_options, 567 custom_fields) 568 self._send_nak_before_ack = send_nak_before_ack 569 self._response_counter = 0 570 571 @property 572 def response_packet_count(self): 573 return 2 574 575 def respond(self, query_packet): 576 """ Respond to |query_packet| with a NAK then ACK or ACK then NAK. """ 577 if ((self._response_counter == 0 and self._send_nak_before_ack) or 578 (self._response_counter != 0 and not self._send_nak_before_ack)): 579 response_packet = dhcp_packet.DhcpPacket.create_nak_packet( 580 query_packet.transaction_id, query_packet.client_hw_address) 581 else: 582 response_packet = super(DhcpHandlingRule_RejectAndRespondToRequest, 583 self).respond(query_packet) 584 self._response_counter += 1 585 return response_packet 586 587 588class DhcpHandlingRule_AcceptDecline(DhcpHandlingRule): 589 """ 590 This handler accepts any DECLINE packet that contains an option for 591 SERVER_ID matches |expected_server_ip|. There is no response to this 592 packet. 593 """ 594 def __init__(self, 595 expected_server_ip, 596 additional_options, 597 custom_fields): 598 """ 599 All *_ip arguments are IPv4 address strings like "192.168.1.101". 600 601 |additional_options| is handled as explained by DhcpHandlingRule. 602 """ 603 super(DhcpHandlingRule_AcceptDecline, self).__init__( 604 dhcp_packet.MESSAGE_TYPE_DECLINE, additional_options, 605 custom_fields) 606 self._expected_server_ip = expected_server_ip 607 608 def handle_impl(self, query_packet): 609 if not self.is_our_message_type(query_packet): 610 return RESPONSE_NO_ACTION 611 612 self.logger.info("Received DECLINE packet, checking fields...") 613 server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) 614 if server_ip is None: 615 self.logger.info("DECLINE packet did not have the expected " 616 "options, discarding.") 617 return RESPONSE_NO_ACTION 618 619 if server_ip != self._expected_server_ip: 620 self.emit_warning("DECLINE packet's server ip did not match our " 621 "expectations; expected %s but got %s" % 622 (self._expected_server_ip, server_ip)) 623 return RESPONSE_NO_ACTION 624 625 self.logger.info("Received valid DECLINE packet, processing") 626 ret = RESPONSE_POP_HANDLER 627 if self.is_final_handler: 628 ret |= RESPONSE_TEST_SUCCEEDED 629 return ret 630