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