普通文本  |  630行  |  24.24 KB

# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
DHCP handling rules are ways to record expectations for a DhcpTestServer.

When a handling rule reaches the front of the DhcpTestServer handling rule
queue, the server begins to ask the rule what it should do with each incoming
DHCP packet (in the form of a DhcpPacket).  The handle() method is expected to
return a tuple (response, action) where response indicates whether the packet
should be ignored or responded to and whether the test failed, succeeded, or is
continuing.  The action part of the tuple refers to whether or not the rule
should be be removed from the test server's handling rule queue.
"""

import logging
import time

from autotest_lib.client.cros import dhcp_packet

# Drops the packet and acts like it never happened.
RESPONSE_NO_ACTION = 0
# Signals that the handler wishes to send a packet.
RESPONSE_HAVE_RESPONSE = 1 << 0
# Signals that the handler wishes to be removed from the handling queue.
# The handler will be asked to generate a packet first if the handler signalled
# that it wished to do so with RESPONSE_HAVE_RESPONSE.
RESPONSE_POP_HANDLER = 1 << 1
# Signals that the handler wants to end the test on a failure.
RESPONSE_TEST_FAILED = 1 << 2
# Signals that the handler wants to end the test because it succeeded.
# Note that the failure bit has precedence over the success bit.
RESPONSE_TEST_SUCCEEDED = 1 << 3

class DhcpHandlingRule(object):
    """
    DhcpHandlingRule defines an interface between the DhcpTestServer and
    subclasses of DhcpHandlingRule.  A handling rule at the front of the
    DhcpTestServer rule queue is first asked what should be done with a packet
    via handle().  handle() returns a bitfield as described above.  If the
    response from handle() indicates that a packet should be sent in response,
    the server asks the handling rule to construct a response packet via
    respond().
    """

    def __init__(self, message_type, additional_options, custom_fields):
        """
        |message_type| should be a MessageType, from DhcpPacket.
        |additional_options| should be a dictionary that maps from
        dhcp_packet.OPTION_* to values.  For instance:

        {dhcp_packet.OPTION_SERVER_ID : "10.10.10.1"}

        These options are injected into response packets if the client requests
        it.  See inject_options().
        """
        super(DhcpHandlingRule, self).__init__()
        self._is_final_handler = False
        self._logger = logging.getLogger("dhcp.handling_rule")
        self._options = additional_options
        self._fields = custom_fields
        self._target_time_seconds = None
        self._allowable_time_delta_seconds = 0.5
        self._force_reply_options = []
        self._message_type = message_type
        self._last_warning = None

    def __str__(self):
        if self._last_warning:
            return '%s (%s)' % (self.__class__.__name__, self._last_warning)
        else:
            return self.__class__.__name__

    @property
    def logger(self):
        return self._logger

    @property
    def is_final_handler(self):
        return self._is_final_handler

    @is_final_handler.setter
    def is_final_handler(self, value):
        self._is_final_handler = value

    @property
    def options(self):
        """
        Returns a dictionary that maps from DhcpPacket options to their values.
        """
        return self._options

    @property
    def fields(self):
        """
        Returns a dictionary that maps from DhcpPacket fields to their values.
        """
        return self._fields

    @property
    def target_time_seconds(self):
        """
        If this is not None, packets will be rejected if they don't fall within
        |self.allowable_time_delta_seconds| seconds of
        |self.target_time_seconds|.  A value of None will cause this handler to
        ignore the target packet time.

        Defaults to None.
        """
        return self._target_time_seconds

    @target_time_seconds.setter
    def target_time_seconds(self, value):
        self._target_time_seconds = value

    @property
    def allowable_time_delta_seconds(self):
        """
        A configurable fudge factor for |self.target_time_seconds|.  If a packet
        comes in at time T and:

        delta = abs(T - |self.target_time_seconds|)

        Then if delta < |self.allowable_time_delta_seconds|, we accept the
        packet.  Otherwise we either fail the test or ignore the packet,
        depending on whether this packet is before or after the window.

        Defaults to 0.5 seconds.
        """
        return self._allowable_time_delta_seconds

    @allowable_time_delta_seconds.setter
    def allowable_time_delta_seconds(self, value):
        self._allowable_time_delta_seconds = value

    @property
    def packet_is_too_late(self):
        if self.target_time_seconds is None:
            return False
        delta = time.time() - self.target_time_seconds
        logging.debug("Handler received packet %0.2f seconds from target time.",
                      delta)
        if delta > self._allowable_time_delta_seconds:
            logging.info("Packet was too late for handling (+%0.2f seconds)",
                         delta - self._allowable_time_delta_seconds)
            return True
        logging.info("Packet was not too late for handling.")
        return False

    @property
    def packet_is_too_soon(self):
        if self.target_time_seconds is None:
            return False
        delta = time.time() - self.target_time_seconds
        logging.debug("Handler received packet %0.2f seconds from target time.",
                      delta)
        if -delta > self._allowable_time_delta_seconds:
            logging.info("Packet arrived too soon for handling: "
                         "(-%0.2f seconds)",
                         -delta - self._allowable_time_delta_seconds)
            return True
        logging.info("Packet was not too soon for handling.")
        return False

    @property
    def force_reply_options(self):
        return self._force_reply_options

    @force_reply_options.setter
    def force_reply_options(self, value):
        self._force_reply_options = value

    @property
    def response_packet_count(self):
        return 1

    def emit_warning(self, warning):
        """
        Log a warning, and retain that warning as |_last_warning|.

        @param warning: The warning message
        """
        self.logger.warning(warning)
        self._last_warning = warning

    def handle(self, query_packet):
        """
        The DhcpTestServer will call this method to ask a handling rule whether
        it wants to take some action in response to a packet.  The handler
        should return some combination of RESPONSE_* bits as described above.

        |packet| is a valid DHCP packet, but the values of fields and presence
        of options is not guaranteed.
        """
        if self.packet_is_too_late:
            return RESPONSE_TEST_FAILED
        if self.packet_is_too_soon:
            return RESPONSE_NO_ACTION
        return self.handle_impl(query_packet)

    def handle_impl(self, query_packet):
        logging.error("DhcpHandlingRule.handle_impl() called.")
        return RESPONSE_TEST_FAILED

    def respond(self, query_packet):
        """
        Called by the DhcpTestServer to generate a packet to send back to the
        client.  This method is called if and only if the response returned from
        handle() had RESPONSE_HAVE_RESPONSE set.
        """
        return None

    def inject_options(self, packet, requested_parameters):
        """
        Adds options listed in the intersection of |requested_parameters| and
        |self.options| to |packet|.  Also include the options in the
        intersection of |self.force_reply_options| and |self.options|.

        |packet| is a DhcpPacket.

        |requested_parameters| is a list of options numbers as you would find in
        a DHCP_DISCOVER or DHCP_REQUEST packet after being parsed by DhcpPacket
        (e.g. [1, 121, 33, 3, 6, 12]).

        Subclassed handling rules may call this to inject options into response
        packets to the client.  This process emulates a real DHCP server which
        would have a pool of configuration settings to hand out to DHCP clients
        upon request.
        """
        for option, value in self.options.items():
            if (option.number in requested_parameters or
                option in self.force_reply_options):
                packet.set_option(option, value)

    def inject_fields(self, packet):
        """
        Adds fields listed in |self.fields| to |packet|.

        |packet| is a DhcpPacket.

        Subclassed handling rules may call this to inject fields into response
        packets to the client.  This process emulates a real DHCP server which
        would have a pool of configuration settings to hand out to DHCP clients
        upon request.
        """
        for field, value in self.fields.items():
            packet.set_field(field, value)

    def is_our_message_type(self, packet):
        """
        Checks if the Message Type DHCP Option in |packet| matches the message
        type handled by this rule. Logs a warning if the types do not match.

        @param packet: a DhcpPacket

        @returns True or False
        """
        if packet.message_type == self._message_type:
            return True
        else:
            self.emit_warning("Packet's message type was %s, not %s." % (
                              packet.message_type.name,
                              self._message_type.name))
            return False


class DhcpHandlingRule_RespondToDiscovery(DhcpHandlingRule):
    """
    This handler will accept any DISCOVER packet received by the server. In
    response to such a packet, the handler will construct an OFFER packet
    offering |intended_ip| from a server at |server_ip| (from the constructor).
    """
    def __init__(self,
                 intended_ip,
                 server_ip,
                 additional_options,
                 custom_fields,
                 should_respond=True):
        """
        |intended_ip| is an IPv4 address string like "192.168.1.100".

        |server_ip| is an IPv4 address string like "192.168.1.1".

        |additional_options| is handled as explained by DhcpHandlingRule.
        """
        super(DhcpHandlingRule_RespondToDiscovery, self).__init__(
                dhcp_packet.MESSAGE_TYPE_DISCOVERY, additional_options,
                custom_fields)
        self._intended_ip = intended_ip
        self._server_ip = server_ip
        self._should_respond = should_respond

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        self.logger.info("Received valid DISCOVERY packet.  Processing.")
        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        if self._should_respond:
            ret |= RESPONSE_HAVE_RESPONSE
        return ret

    def respond(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return None

        self.logger.info("Responding to DISCOVERY packet.")
        response_packet = dhcp_packet.DhcpPacket.create_offer_packet(
                query_packet.transaction_id,
                query_packet.client_hw_address,
                self._intended_ip,
                self._server_ip)
        requested_parameters = query_packet.get_option(
                dhcp_packet.OPTION_PARAMETER_REQUEST_LIST)
        if requested_parameters is not None:
            self.inject_options(response_packet, requested_parameters)
        self.inject_fields(response_packet)
        return response_packet


class DhcpHandlingRule_RejectRequest(DhcpHandlingRule):
    """
    This handler receives a REQUEST packet, and responds with a NAK.
    """
    def __init__(self):
        super(DhcpHandlingRule_RejectRequest, self).__init__(
                dhcp_packet.MESSAGE_TYPE_REQUEST, {}, {})
        self._should_respond = True

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        if self._should_respond:
            ret |= RESPONSE_HAVE_RESPONSE
        return ret

    def respond(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return None

        self.logger.info("NAKing the REQUEST packet.")
        response_packet = dhcp_packet.DhcpPacket.create_nak_packet(
            query_packet.transaction_id, query_packet.client_hw_address)
        return response_packet


class DhcpHandlingRule_RespondToRequest(DhcpHandlingRule):
    """
    This handler accepts any REQUEST packet that contains options for SERVER_ID
    and REQUESTED_IP that match |expected_server_ip| and |expected_requested_ip|
    respectively.  It responds with an ACKNOWLEDGEMENT packet from a DHCP server
    at |response_server_ip| granting |response_granted_ip| to a client at the
    address given in the REQUEST packet.  If |response_server_ip| or
    |response_granted_ip| are not given, then they default to
    |expected_server_ip| and |expected_requested_ip| respectively.
    """
    def __init__(self,
                 expected_requested_ip,
                 expected_server_ip,
                 additional_options,
                 custom_fields,
                 should_respond=True,
                 response_server_ip=None,
                 response_granted_ip=None,
                 expect_server_ip_set=True):
        """
        All *_ip arguments are IPv4 address strings like "192.168.1.101".

        |additional_options| is handled as explained by DhcpHandlingRule.
        """
        super(DhcpHandlingRule_RespondToRequest, self).__init__(
                dhcp_packet.MESSAGE_TYPE_REQUEST, additional_options,
                custom_fields)
        self._expected_requested_ip = expected_requested_ip
        self._expected_server_ip = expected_server_ip
        self._should_respond = should_respond
        self._granted_ip = response_granted_ip
        self._server_ip = response_server_ip
        self._expect_server_ip_set = expect_server_ip_set
        if self._granted_ip is None:
            self._granted_ip = self._expected_requested_ip
        if self._server_ip is None:
            self._server_ip = self._expected_server_ip

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        self.logger.info("Received REQUEST packet, checking fields...")
        server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID)
        requested_ip = query_packet.get_option(dhcp_packet.OPTION_REQUESTED_IP)
        server_ip_provided = server_ip is not None
        if ((server_ip_provided != self._expect_server_ip_set) or
            (requested_ip is None)):
            self.logger.info("REQUEST packet did not have the expected "
                             "options, discarding.")
            return RESPONSE_NO_ACTION

        if server_ip_provided and server_ip != self._expected_server_ip:
            self.emit_warning("REQUEST packet's server ip did not match our "
                              "expectations; expected %s but got %s" %
                              (self._expected_server_ip, server_ip))
            return RESPONSE_NO_ACTION

        if requested_ip != self._expected_requested_ip:
            self.emit_warning("REQUEST packet's requested IP did not match "
                              "our expectations; expected %s but got %s" %
                              (self._expected_requested_ip, requested_ip))
            return RESPONSE_NO_ACTION

        self.logger.info("Received valid REQUEST packet, processing")
        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        if self._should_respond:
            ret |= RESPONSE_HAVE_RESPONSE
        return ret

    def respond(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return None

        self.logger.info("Responding to REQUEST packet.")
        response_packet = dhcp_packet.DhcpPacket.create_acknowledgement_packet(
                query_packet.transaction_id,
                query_packet.client_hw_address,
                self._granted_ip,
                self._server_ip)
        requested_parameters = query_packet.get_option(
                dhcp_packet.OPTION_PARAMETER_REQUEST_LIST)
        if requested_parameters is not None:
            self.inject_options(response_packet, requested_parameters)
        self.inject_fields(response_packet)
        return response_packet


class DhcpHandlingRule_RespondToPostT2Request(
        DhcpHandlingRule_RespondToRequest):
    """
    This handler is a lot like DhcpHandlingRule_RespondToRequest except that it
    expects request packets like those sent after the T2 deadline (see RFC
    2131).  This is the only time that you can find a request packet without the
    SERVER_ID option.  It responds to packets in exactly the same way.
    """
    def __init__(self,
                 expected_requested_ip,
                 response_server_ip,
                 additional_options,
                 custom_fields,
                 should_respond=True,
                 response_granted_ip=None):
        """
        All *_ip arguments are IPv4 address strings like "192.168.1.101".

        |additional_options| is handled as explained by DhcpHandlingRule.
        """
        super(DhcpHandlingRule_RespondToPostT2Request, self).__init__(
                expected_requested_ip,
                None,
                additional_options,
                custom_fields,
                should_respond=should_respond,
                response_server_ip=response_server_ip,
                response_granted_ip=response_granted_ip)

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        self.logger.info("Received REQUEST packet, checking fields...")
        if query_packet.get_option(dhcp_packet.OPTION_SERVER_ID) is not None:
            self.logger.info("REQUEST packet had a SERVER_ID option, which it "
                             "is not expected to have, discarding.")
            return RESPONSE_NO_ACTION

        requested_ip = query_packet.get_option(dhcp_packet.OPTION_REQUESTED_IP)
        if requested_ip is None:
            self.logger.info("REQUEST packet did not have the expected "
                             "request ip option at all, discarding.")
            return RESPONSE_NO_ACTION

        if requested_ip != self._expected_requested_ip:
            self.emit_warning("REQUEST packet's requested IP did not match "
                              "our expectations; expected %s but got %s" %
                              (self._expected_requested_ip, requested_ip))
            return RESPONSE_NO_ACTION

        self.logger.info("Received valid post T2 REQUEST packet, processing")
        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        if self._should_respond:
            ret |= RESPONSE_HAVE_RESPONSE
        return ret


class DhcpHandlingRule_AcceptRelease(DhcpHandlingRule):
    """
    This handler accepts any RELEASE packet that contains an option for
    SERVER_ID matches |expected_server_ip|.  There is no response to this
    packet.
    """
    def __init__(self,
                 expected_server_ip,
                 additional_options,
                 custom_fields):
        """
        All *_ip arguments are IPv4 address strings like "192.168.1.101".

        |additional_options| is handled as explained by DhcpHandlingRule.
        """
        super(DhcpHandlingRule_AcceptRelease, self).__init__(
                dhcp_packet.MESSAGE_TYPE_RELEASE, additional_options,
                custom_fields)
        self._expected_server_ip = expected_server_ip

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        self.logger.info("Received RELEASE packet, checking fields...")
        server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID)
        if server_ip is None:
            self.logger.info("RELEASE packet did not have the expected "
                             "options, discarding.")
            return RESPONSE_NO_ACTION

        if server_ip != self._expected_server_ip:
            self.emit_warning("RELEASE packet's server ip did not match our "
                                "expectations; expected %s but got %s" %
                                (self._expected_server_ip, server_ip))
            return RESPONSE_NO_ACTION

        self.logger.info("Received valid RELEASE packet, processing")
        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        return ret


class DhcpHandlingRule_RejectAndRespondToRequest(
        DhcpHandlingRule_RespondToRequest):
    """
    This handler accepts any REQUEST packet that contains options for SERVER_ID
    and REQUESTED_IP that match |expected_server_ip| and |expected_requested_ip|
    respectively.  It responds with both an ACKNOWLEDGEMENT packet from a DHCP
    server as well as a NAK, in order to simulate a network with two conflicting
    servers.
    """
    def __init__(self,
                 expected_requested_ip,
                 expected_server_ip,
                 additional_options,
                 custom_fields,
                 send_nak_before_ack):
        super(DhcpHandlingRule_RejectAndRespondToRequest, self).__init__(
                expected_requested_ip,
                expected_server_ip,
                additional_options,
                custom_fields)
        self._send_nak_before_ack = send_nak_before_ack
        self._response_counter = 0

    @property
    def response_packet_count(self):
        return 2

    def respond(self, query_packet):
        """ Respond to |query_packet| with a NAK then ACK or ACK then NAK. """
        if ((self._response_counter == 0 and self._send_nak_before_ack) or
            (self._response_counter != 0 and not self._send_nak_before_ack)):
            response_packet = dhcp_packet.DhcpPacket.create_nak_packet(
                query_packet.transaction_id, query_packet.client_hw_address)
        else:
            response_packet = super(DhcpHandlingRule_RejectAndRespondToRequest,
                                    self).respond(query_packet)
        self._response_counter += 1
        return response_packet


class DhcpHandlingRule_AcceptDecline(DhcpHandlingRule):
    """
    This handler accepts any DECLINE packet that contains an option for
    SERVER_ID matches |expected_server_ip|.  There is no response to this
    packet.
    """
    def __init__(self,
                 expected_server_ip,
                 additional_options,
                 custom_fields):
        """
        All *_ip arguments are IPv4 address strings like "192.168.1.101".

        |additional_options| is handled as explained by DhcpHandlingRule.
        """
        super(DhcpHandlingRule_AcceptDecline, self).__init__(
                dhcp_packet.MESSAGE_TYPE_DECLINE, additional_options,
                custom_fields)
        self._expected_server_ip = expected_server_ip

    def handle_impl(self, query_packet):
        if not self.is_our_message_type(query_packet):
            return RESPONSE_NO_ACTION

        self.logger.info("Received DECLINE packet, checking fields...")
        server_ip = query_packet.get_option(dhcp_packet.OPTION_SERVER_ID)
        if server_ip is None:
            self.logger.info("DECLINE packet did not have the expected "
                             "options, discarding.")
            return RESPONSE_NO_ACTION

        if server_ip != self._expected_server_ip:
            self.emit_warning("DECLINE packet's server ip did not match our "
                                "expectations; expected %s but got %s" %
                                (self._expected_server_ip, server_ip))
            return RESPONSE_NO_ACTION

        self.logger.info("Received valid DECLINE packet, processing")
        ret = RESPONSE_POP_HANDLER
        if self.is_final_handler:
            ret |= RESPONSE_TEST_SUCCEEDED
        return ret