xref: /aosp_15_r20/external/autotest/client/cros/dhcp_packet.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Tools for serializing and deserializing DHCP packets.
8
9DhcpPacket is a class that represents a single DHCP packet and contains some
10logic to create and parse binary strings containing on the wire DHCP packets.
11
12While you could call the constructor explicitly, most users should use the
13static factories to construct packets with reasonable default values in most of
14the fields, even if those values are zeros.
15
16For example:
17
18packet = dhcp_packet.create_offer_packet(transaction_id,
19                                         hwmac_addr,
20                                         offer_ip,
21                                         server_ip)
22socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
23# Sending to the broadcast address needs special permissions.
24socket.sendto(response_packet.to_binary_string(),
25              ("255.255.255.255", 68))
26
27Note that if you make changes, make sure that the tests in the bottom of this
28file still pass.
29"""
30
31from __future__ import absolute_import
32from __future__ import division
33from __future__ import print_function
34
35import collections
36import logging
37import random
38import six
39from six.moves import range
40import socket
41import struct
42
43
44def get_ord(value):
45    """
46    Helper method for getting the ordinal value of a character in a byte string
47    during the Python 2 to Python 3 migration.
48
49    In Python 2, the function ord() provides the ordinal value of a character.
50    In Python 3, the byte is its own ordinal value.
51    """
52    if six.PY2:
53        return ord(value)
54    return value
55
56
57def get_bytes(value):
58    """
59    Helper method for converting a string into a byte string during the Python 2
60    to Python 3 migration.
61    """
62    if six.PY2:
63        return value
64    return value.encode('ISO-8859-1')
65
66
67def get_string(value):
68    """
69    Helper method for converting a byte string into a string during the Python 2
70    to Python 3 migration.
71    """
72
73    if six.PY2:
74        return value
75    return value.decode('ISO-8859-1')
76
77
78def CreatePacketPieceClass(super_class, field_format):
79    class PacketPiece(super_class):
80        @staticmethod
81        def pack(value):
82            return struct.pack(field_format, value)
83
84        @staticmethod
85        def unpack(byte_string):
86            return struct.unpack(field_format, byte_string)[0]
87    return PacketPiece
88
89"""
90Represents an option in a DHCP packet.  Options may or may not be present in any
91given packet, depending on the configurations of the client and the server.
92Using namedtuples as super classes gets us the comparison operators we want to
93use these Options in dictionaries as keys.  Below, we'll subclass Option to
94reflect that different kinds of options serialize to on the wire formats in
95different ways.
96
97|name|
98A human readable name for this option.
99
100|number|
101Every DHCP option has a number that goes into the packet to indicate
102which particular option is being encoded in the next few bytes.  This
103property returns that number for each option.
104"""
105Option = collections.namedtuple("Option", ["name", "number"])
106
107ByteOption = CreatePacketPieceClass(Option, "!B")
108
109ShortOption = CreatePacketPieceClass(Option, "!H")
110
111IntOption = CreatePacketPieceClass(Option, "!I")
112
113class IpAddressOption(Option):
114    @staticmethod
115    def pack(value):
116        return socket.inet_aton(value)
117
118    @staticmethod
119    def unpack(byte_string):
120        return socket.inet_ntoa(byte_string)
121
122
123class IpListOption(Option):
124    @staticmethod
125    def pack(value):
126        return b"".join([socket.inet_aton(addr) for addr in value])
127
128    @staticmethod
129    def unpack(byte_string):
130        return [socket.inet_ntoa(byte_string[idx:idx+4])
131                for idx in range(0, len(byte_string), 4)]
132
133
134class RawOption(Option):
135    @staticmethod
136    def pack(value):
137        return get_bytes(value)
138
139    @staticmethod
140    def unpack(byte_string):
141        return get_string(byte_string)
142
143
144class ByteListOption(Option):
145    @staticmethod
146    def pack(value):
147        if six.PY2:
148            return "".join(chr(v) for v in value)
149        return bytes(value)
150
151    @staticmethod
152    def unpack(byte_string):
153        if six.PY2:
154            return [ord(c) for c in byte_string]
155        return byte_string
156
157
158class ClasslessStaticRoutesOption(Option):
159    """
160    This is a RFC 3442 compliant classless static route option parser and
161    serializer.  The symbolic "value" packed and unpacked from this class
162    is a list (prefix_size, destination, router) tuples.
163    """
164
165    @staticmethod
166    def pack(value):
167        route_list = value
168        byte_string = b""
169        for prefix_size, destination, router in route_list:
170            byte_string += get_bytes(chr(prefix_size))
171            # Encode only the significant octets of the destination
172            # that fall within the prefix.
173            destination_address_count = (prefix_size + 7) // 8
174            destination_address = socket.inet_aton(destination)
175            byte_string += destination_address[:destination_address_count]
176            byte_string += socket.inet_aton(router)
177
178        return byte_string
179
180    @staticmethod
181    def unpack(byte_string):
182        route_list = []
183        offset = 0
184        while offset < len(byte_string):
185            prefix_size = get_ord(byte_string[offset])
186            destination_address_count = (prefix_size + 7) // 8
187            entry_end = offset + 1 + destination_address_count + 4
188            if entry_end > len(byte_string):
189                raise Exception("Classless domain list is corrupted.")
190            offset += 1
191            destination_address_end = offset + destination_address_count
192            destination_address = byte_string[offset:destination_address_end]
193            # Pad the destination address bytes with zero byte octets to
194            # fill out an IPv4 address.
195            destination_address += b'\x00' * (4 - destination_address_count)
196            router_address = byte_string[destination_address_end:entry_end]
197            route_list.append((prefix_size,
198                               socket.inet_ntoa(destination_address),
199                               socket.inet_ntoa(router_address)))
200            offset = entry_end
201
202        return route_list
203
204
205class DomainListOption(Option):
206    """
207    This is a RFC 1035 compliant domain list option parser and serializer.
208    There are some clever compression optimizations that it does not implement
209    for serialization, but correctly parses.  This should be sufficient for
210    testing.
211    """
212    # Various RFC's let you finish a domain name by pointing to an existing
213    # domain name rather than repeating the same suffix.  All such pointers are
214    # two bytes long, specify the offset in the byte string, and begin with
215    # |POINTER_PREFIX| to distinguish them from normal characters.
216    POINTER_PREFIX = ord("\xC0")
217
218    @staticmethod
219    def pack(value):
220        domain_list = value
221        byte_string = b""
222        for domain in domain_list:
223            for part in domain.split("."):
224                byte_string += get_bytes(chr(len(part)))
225                byte_string += get_bytes(part)
226            byte_string += b"\x00"
227        return byte_string
228
229    @staticmethod
230    def unpack(byte_string):
231        if six.PY3:
232            byte_string = byte_string.decode('ISO-8859-1')
233        domain_list = []
234        offset = 0
235        try:
236            while offset < len(byte_string):
237                (new_offset, domain_parts) = DomainListOption._read_domain_name(
238                        byte_string,
239                        offset)
240                domain_name = ".".join(domain_parts)
241                domain_list.append(domain_name)
242                if new_offset <= offset:
243                    raise Exception("Parsing logic error is letting domain "
244                                    "list parsing go on forever.")
245                offset = new_offset
246        except ValueError:
247            # Badly formatted packets are not necessarily test errors.
248            logging.warning("Found badly formatted DHCP domain search list")
249            return None
250        return domain_list
251
252    @staticmethod
253    def _read_domain_name(byte_string, offset):
254        """
255        Recursively parse a domain name from a domain name list.
256        """
257        parts = []
258        while True:
259            if offset >= len(byte_string):
260                raise ValueError("Domain list ended without a NULL byte.")
261            maybe_part_len = ord(byte_string[offset])
262            offset += 1
263            if maybe_part_len == 0:
264                # Domains are terminated with either a 0 or a pointer to a
265                # domain suffix within |byte_string|.
266                return (offset, parts)
267            elif ((maybe_part_len & DomainListOption.POINTER_PREFIX) ==
268                  DomainListOption.POINTER_PREFIX):
269                if offset >= len(byte_string):
270                    raise ValueError("Missing second byte of domain suffix "
271                                     "pointer.")
272                maybe_part_len &= ~DomainListOption.POINTER_PREFIX
273                pointer_offset = ((maybe_part_len << 8) +
274                                  ord(byte_string[offset]))
275                offset += 1
276                (_, more_parts) = DomainListOption._read_domain_name(
277                        byte_string,
278                        pointer_offset)
279                parts.extend(more_parts)
280                return (offset, parts)
281            else:
282                # That byte was actually the length of the next part, not a
283                # pointer back into the data.
284                part_len = maybe_part_len
285                if offset + part_len >= len(byte_string):
286                    raise ValueError("Part of a domain goes beyond data "
287                                     "length.")
288                parts.append(byte_string[offset : offset + part_len])
289                offset += part_len
290
291
292"""
293Represents a required field in a DHCP packet.  Similar to Option, we'll
294subclass Field to reflect that different fields serialize to on the wire formats
295in different ways.
296
297|name|
298A human readable name for this field.
299
300|offset|
301The |offset| for a field defines the starting byte of the field in the
302binary packet string.  |offset| is used during parsing, along with
303|size| to extract the byte string of a field.
304
305|size|
306Fields in DHCP packets have a fixed size that must be respected.  This
307size property is used in parsing to indicate that |self._size| number of
308bytes make up this field.
309"""
310Field = collections.namedtuple("Field", ["name", "offset", "size"])
311
312ByteField = CreatePacketPieceClass(Field, "!B")
313
314ShortField = CreatePacketPieceClass(Field, "!H")
315
316IntField = CreatePacketPieceClass(Field, "!I")
317
318HwAddrField = CreatePacketPieceClass(Field, "!16s")
319
320ServerNameField = CreatePacketPieceClass(Field, "!64s")
321
322BootFileField = CreatePacketPieceClass(Field, "!128s")
323
324class IpAddressField(Field):
325    @staticmethod
326    def pack(value):
327        return socket.inet_aton(value)
328
329    @staticmethod
330    def unpack(byte_string):
331        return socket.inet_ntoa(byte_string)
332
333
334# This is per RFC 2131.  The wording doesn't seem to say that the packets must
335# be this big, but that has been the historic assumption in implementations.
336DHCP_MIN_PACKET_SIZE = 300
337
338IPV4_NULL_ADDRESS = "0.0.0.0"
339
340# These are required in every DHCP packet.  Without these fields, the
341# packet will not even pass DhcpPacket.is_valid
342FIELD_OP = ByteField("op", 0, 1)
343FIELD_HWTYPE = ByteField("htype", 1, 1)
344FIELD_HWADDR_LEN = ByteField("hlen", 2, 1)
345FIELD_RELAY_HOPS = ByteField("hops", 3, 1)
346FIELD_TRANSACTION_ID = IntField("xid", 4, 4)
347FIELD_TIME_SINCE_START = ShortField("secs", 8, 2)
348FIELD_FLAGS = ShortField("flags", 10, 2)
349FIELD_CLIENT_IP = IpAddressField("ciaddr", 12, 4)
350FIELD_YOUR_IP = IpAddressField("yiaddr", 16, 4)
351FIELD_SERVER_IP = IpAddressField("siaddr", 20, 4)
352FIELD_GATEWAY_IP = IpAddressField("giaddr", 24, 4)
353FIELD_CLIENT_HWADDR = HwAddrField("chaddr", 28, 16)
354# The following two fields are considered "legacy BOOTP" fields but may
355# sometimes be used by DHCP clients.
356FIELD_LEGACY_SERVER_NAME = ServerNameField("servername", 44, 64);
357FIELD_LEGACY_BOOT_FILE = BootFileField("bootfile", 108, 128);
358FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4)
359
360OPTION_TIME_OFFSET = IntOption("time_offset", 2)
361OPTION_ROUTERS = IpListOption("routers", 3)
362OPTION_SUBNET_MASK = IpAddressOption("subnet_mask", 1)
363OPTION_TIME_SERVERS = IpListOption("time_servers", 4)
364OPTION_NAME_SERVERS = IpListOption("name_servers", 5)
365OPTION_DNS_SERVERS = IpListOption("dns_servers", 6)
366OPTION_LOG_SERVERS = IpListOption("log_servers", 7)
367OPTION_COOKIE_SERVERS = IpListOption("cookie_servers", 8)
368OPTION_LPR_SERVERS = IpListOption("lpr_servers", 9)
369OPTION_IMPRESS_SERVERS = IpListOption("impress_servers", 10)
370OPTION_RESOURCE_LOC_SERVERS = IpListOption("resource_loc_servers", 11)
371OPTION_HOST_NAME = RawOption("host_name", 12)
372OPTION_BOOT_FILE_SIZE = ShortOption("boot_file_size", 13)
373OPTION_MERIT_DUMP_FILE = RawOption("merit_dump_file", 14)
374OPTION_DOMAIN_NAME = RawOption("domain_name", 15)
375OPTION_SWAP_SERVER = IpAddressOption("swap_server", 16)
376OPTION_ROOT_PATH = RawOption("root_path", 17)
377OPTION_EXTENSIONS = RawOption("extensions", 18)
378OPTION_INTERFACE_MTU = ShortOption("interface_mtu", 26)
379OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption(
380        "vendor_encapsulated_options", 43)
381OPTION_REQUESTED_IP = IpAddressOption("requested_ip", 50)
382OPTION_IP_LEASE_TIME = IntOption("ip_lease_time", 51)
383OPTION_OPTION_OVERLOAD = ByteOption("option_overload", 52)
384OPTION_DHCP_MESSAGE_TYPE = ByteOption("dhcp_message_type", 53)
385OPTION_SERVER_ID = IpAddressOption("server_id", 54)
386OPTION_PARAMETER_REQUEST_LIST = ByteListOption("parameter_request_list", 55)
387OPTION_MESSAGE = RawOption("message", 56)
388OPTION_MAX_DHCP_MESSAGE_SIZE = ShortOption("max_dhcp_message_size", 57)
389OPTION_RENEWAL_T1_TIME_VALUE = IntOption("renewal_t1_time_value", 58)
390OPTION_REBINDING_T2_TIME_VALUE = IntOption("rebinding_t2_time_value", 59)
391OPTION_VENDOR_ID = RawOption("vendor_id", 60)
392OPTION_CLIENT_ID = RawOption("client_id", 61)
393OPTION_TFTP_SERVER_NAME = RawOption("tftp_server_name", 66)
394OPTION_BOOTFILE_NAME = RawOption("bootfile_name", 67)
395OPTION_FULLY_QUALIFIED_DOMAIN_NAME = RawOption("fqdn", 81)
396OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119)
397OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption(
398        "classless_static_routes", 121)
399OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252)
400
401# Unlike every other option, which are tuples like:
402# <number, length in bytes, data>, the pad and end options are just
403# single bytes "\x00" and "\xff" (without length or data fields).
404OPTION_PAD = 0
405OPTION_END = 255
406
407DHCP_COMMON_FIELDS = [
408        FIELD_OP,
409        FIELD_HWTYPE,
410        FIELD_HWADDR_LEN,
411        FIELD_RELAY_HOPS,
412        FIELD_TRANSACTION_ID,
413        FIELD_TIME_SINCE_START,
414        FIELD_FLAGS,
415        FIELD_CLIENT_IP,
416        FIELD_YOUR_IP,
417        FIELD_SERVER_IP,
418        FIELD_GATEWAY_IP,
419        FIELD_CLIENT_HWADDR,
420        ]
421
422DHCP_REQUIRED_FIELDS = DHCP_COMMON_FIELDS + [
423        FIELD_MAGIC_COOKIE,
424        ]
425
426DHCP_ALL_FIELDS = DHCP_COMMON_FIELDS + [
427        FIELD_LEGACY_SERVER_NAME,
428        FIELD_LEGACY_BOOT_FILE,
429        FIELD_MAGIC_COOKIE,
430        ]
431
432# The op field in an ipv4 packet is either 1 or 2 depending on
433# whether the packet is from a server or from a client.
434FIELD_VALUE_OP_CLIENT_REQUEST = 1
435FIELD_VALUE_OP_SERVER_RESPONSE = 2
436# 1 == 10mb ethernet hardware address type (aka MAC).
437FIELD_VALUE_HWTYPE_10MB_ETH = 1
438# MAC addresses are still 6 bytes long.
439FIELD_VALUE_HWADDR_LEN_10MB_ETH = 6
440FIELD_VALUE_MAGIC_COOKIE = 0x63825363
441
442OPTIONS_START_OFFSET = 240
443
444MessageType = collections.namedtuple('MessageType', 'name option_value')
445# From RFC2132, the valid DHCP message types are:
446MESSAGE_TYPE_UNKNOWN = MessageType('UNKNOWN', 0)
447MESSAGE_TYPE_DISCOVERY = MessageType('DISCOVERY', 1)
448MESSAGE_TYPE_OFFER = MessageType('OFFER', 2)
449MESSAGE_TYPE_REQUEST = MessageType('REQUEST', 3)
450MESSAGE_TYPE_DECLINE = MessageType('DECLINE', 4)
451MESSAGE_TYPE_ACK = MessageType('ACK', 5)
452MESSAGE_TYPE_NAK = MessageType('NAK', 6)
453MESSAGE_TYPE_RELEASE = MessageType('RELEASE', 7)
454MESSAGE_TYPE_INFORM = MessageType('INFORM', 8)
455MESSAGE_TYPE_BY_NUM = [
456    None,
457    MESSAGE_TYPE_DISCOVERY,
458    MESSAGE_TYPE_OFFER,
459    MESSAGE_TYPE_REQUEST,
460    MESSAGE_TYPE_DECLINE,
461    MESSAGE_TYPE_ACK,
462    MESSAGE_TYPE_NAK,
463    MESSAGE_TYPE_RELEASE,
464    MESSAGE_TYPE_INFORM
465]
466
467OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [
468        OPTION_REQUESTED_IP.number,
469        OPTION_IP_LEASE_TIME.number,
470        OPTION_SERVER_ID.number,
471        OPTION_SUBNET_MASK.number,
472        OPTION_ROUTERS.number,
473        OPTION_DNS_SERVERS.number,
474        OPTION_HOST_NAME.number,
475        ]
476
477# These are possible options that may not be in every packet.
478# Frequently, the client can include a bunch of options that indicate
479# that it would like to receive information about time servers, routers,
480# lpr servers, and much more, but the DHCP server can usually ignore
481# those requests.
482#
483# Eventually, each option is encoded as:
484#     <option.number, option.size, [array of option.size bytes]>
485# Unlike fields, which make up a fixed packet format, options can be in
486# any order, except where they cannot.  For instance, option 1 must
487# follow option 3 if both are supplied.  For this reason, potential
488# options are in this list, and added to the packet in this order every
489# time.
490#
491# size < 0 indicates that this is variable length field of at least
492# abs(length) bytes in size.
493DHCP_PACKET_OPTIONS = [
494        OPTION_TIME_OFFSET,
495        OPTION_ROUTERS,
496        OPTION_SUBNET_MASK,
497        OPTION_TIME_SERVERS,
498        OPTION_NAME_SERVERS,
499        OPTION_DNS_SERVERS,
500        OPTION_LOG_SERVERS,
501        OPTION_COOKIE_SERVERS,
502        OPTION_LPR_SERVERS,
503        OPTION_IMPRESS_SERVERS,
504        OPTION_RESOURCE_LOC_SERVERS,
505        OPTION_HOST_NAME,
506        OPTION_BOOT_FILE_SIZE,
507        OPTION_MERIT_DUMP_FILE,
508        OPTION_SWAP_SERVER,
509        OPTION_DOMAIN_NAME,
510        OPTION_ROOT_PATH,
511        OPTION_EXTENSIONS,
512        OPTION_INTERFACE_MTU,
513        OPTION_VENDOR_ENCAPSULATED_OPTIONS,
514        OPTION_REQUESTED_IP,
515        OPTION_IP_LEASE_TIME,
516        OPTION_OPTION_OVERLOAD,
517        OPTION_DHCP_MESSAGE_TYPE,
518        OPTION_SERVER_ID,
519        OPTION_PARAMETER_REQUEST_LIST,
520        OPTION_MESSAGE,
521        OPTION_MAX_DHCP_MESSAGE_SIZE,
522        OPTION_RENEWAL_T1_TIME_VALUE,
523        OPTION_REBINDING_T2_TIME_VALUE,
524        OPTION_VENDOR_ID,
525        OPTION_CLIENT_ID,
526        OPTION_TFTP_SERVER_NAME,
527        OPTION_BOOTFILE_NAME,
528        OPTION_FULLY_QUALIFIED_DOMAIN_NAME,
529        OPTION_DNS_DOMAIN_SEARCH_LIST,
530        OPTION_CLASSLESS_STATIC_ROUTES,
531        OPTION_WEB_PROXY_AUTO_DISCOVERY,
532        ]
533
534def get_dhcp_option_by_number(number):
535    for option in DHCP_PACKET_OPTIONS:
536        if option.number == number:
537            return option
538    return None
539
540class DhcpPacket(object):
541    @staticmethod
542    def create_discovery_packet(hwmac_addr):
543        """
544        Create a discovery packet.
545
546        Fill in fields of a DHCP packet as if it were being sent from
547        |hwmac_addr|.  Requests subnet masks, broadcast addresses, router
548        addresses, dns addresses, domain search lists, client host name, and NTP
549        server addresses.  Note that the offer packet received in response to
550        this packet will probably not contain all of that information.
551        """
552        # MAC addresses are actually only 6 bytes long, however, for whatever
553        # reason, DHCP allocated 12 bytes to this field.  Ease the burden on
554        # developers and hide this detail.
555        while len(hwmac_addr) < 12:
556            hwmac_addr += get_bytes(chr(OPTION_PAD))
557
558        packet = DhcpPacket()
559        packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST)
560        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
561        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
562        packet.set_field(FIELD_RELAY_HOPS, 0)
563        packet.set_field(FIELD_TRANSACTION_ID, random.getrandbits(32))
564        packet.set_field(FIELD_TIME_SINCE_START, 0)
565        packet.set_field(FIELD_FLAGS, 0)
566        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
567        packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS)
568        packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS)
569        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
570        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
571        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
572        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
573                          MESSAGE_TYPE_DISCOVERY.option_value)
574        return packet
575
576    @staticmethod
577    def create_offer_packet(transaction_id,
578                            hwmac_addr,
579                            offer_ip,
580                            server_ip):
581        """
582        Create an offer packet, given some fields that tie the packet to a
583        particular offer.
584        """
585        packet = DhcpPacket()
586        packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE)
587        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
588        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
589        # This has something to do with relay agents
590        packet.set_field(FIELD_RELAY_HOPS, 0)
591        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
592        packet.set_field(FIELD_TIME_SINCE_START, 0)
593        packet.set_field(FIELD_FLAGS, 0)
594        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
595        packet.set_field(FIELD_YOUR_IP, offer_ip)
596        packet.set_field(FIELD_SERVER_IP, server_ip)
597        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
598        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
599        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
600        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
601                          MESSAGE_TYPE_OFFER.option_value)
602        return packet
603
604    @staticmethod
605    def create_request_packet(transaction_id,
606                              hwmac_addr):
607        packet = DhcpPacket()
608        packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST)
609        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
610        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
611        # This has something to do with relay agents
612        packet.set_field(FIELD_RELAY_HOPS, 0)
613        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
614        packet.set_field(FIELD_TIME_SINCE_START, 0)
615        packet.set_field(FIELD_FLAGS, 0)
616        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
617        packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS)
618        packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS)
619        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
620        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
621        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
622        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
623                          MESSAGE_TYPE_REQUEST.option_value)
624        return packet
625
626    @staticmethod
627    def create_acknowledgement_packet(transaction_id,
628                                      hwmac_addr,
629                                      granted_ip,
630                                      server_ip):
631        packet = DhcpPacket()
632        packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE)
633        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
634        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
635        # This has something to do with relay agents
636        packet.set_field(FIELD_RELAY_HOPS, 0)
637        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
638        packet.set_field(FIELD_TIME_SINCE_START, 0)
639        packet.set_field(FIELD_FLAGS, 0)
640        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
641        packet.set_field(FIELD_YOUR_IP, granted_ip)
642        packet.set_field(FIELD_SERVER_IP, server_ip)
643        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
644        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
645        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
646        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
647                          MESSAGE_TYPE_ACK.option_value)
648        return packet
649
650    @staticmethod
651    def create_nak_packet(transaction_id, hwmac_addr):
652        """
653        Create a negative acknowledge packet.
654
655        @param transaction_id: The DHCP transaction ID.
656        @param hwmac_addr: The client's MAC address.
657        """
658        packet = DhcpPacket()
659        packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE)
660        packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH)
661        packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
662        # This has something to do with relay agents
663        packet.set_field(FIELD_RELAY_HOPS, 0)
664        packet.set_field(FIELD_TRANSACTION_ID, transaction_id)
665        packet.set_field(FIELD_TIME_SINCE_START, 0)
666        packet.set_field(FIELD_FLAGS, 0)
667        packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS)
668        packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS)
669        packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS)
670        packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS)
671        packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr)
672        packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE)
673        packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
674                          MESSAGE_TYPE_NAK.option_value)
675        return packet
676
677    def __init__(self, byte_str=None):
678        """
679        Create a DhcpPacket, filling in fields from a byte string if given.
680
681        Assumes that the packet starts at offset 0 in the binary string.  This
682        includes the fields and options.  Fields are different from options in
683        that we bother to decode these into more usable data types like
684        integers rather than keeping them as raw byte strings.  Fields are also
685        required to exist, unlike options which may not.
686
687        Each option is encoded as a tuple <option number, length, data> where
688        option number is a byte indicating the type of option, length indicates
689        the number of bytes in the data for option, and data is a length array
690        of bytes.  The only exceptions to this rule are the 0 and 255 options,
691        which have 0 data length, and no length byte.  These tuples are then
692        simply appended to each other.  This encoding is the same as the BOOTP
693        vendor extention field encoding.
694        """
695        super(DhcpPacket, self).__init__()
696        self._options = {}
697        self._fields = {}
698        if byte_str is None:
699            return
700        if len(byte_str) < OPTIONS_START_OFFSET + 1:
701            logging.error("Invalid byte string for packet.")
702            return
703        for field in DHCP_ALL_FIELDS:
704            self._fields[field] = field.unpack(byte_str[field.offset :
705                                                        field.offset +
706                                                        field.size])
707        offset = OPTIONS_START_OFFSET
708        domain_search_list_byte_string = b""
709        while offset < len(byte_str) and get_ord(
710                byte_str[offset]) != OPTION_END:
711            data_type = get_ord(byte_str[offset])
712            offset += 1
713            if data_type == OPTION_PAD:
714                continue
715            data_length = get_ord(byte_str[offset])
716            offset += 1
717            data = byte_str[offset: offset + data_length]
718            offset += data_length
719            option = get_dhcp_option_by_number(data_type)
720            if option is None:
721                logging.warning("Unsupported DHCP option found.  "
722                                "Option number: %d", data_type)
723                continue
724            if option == OPTION_DNS_DOMAIN_SEARCH_LIST:
725                # In a cruel twist of fate, the server is allowed to give
726                # multiple options with this number.  The client is expected to
727                # concatenate the byte strings together and use it as a single
728                # value.
729                domain_search_list_byte_string += data
730                continue
731            option_value = option.unpack(data)
732            if option == OPTION_PARAMETER_REQUEST_LIST:
733                logging.info("Requested options: %s", str(option_value))
734            self._options[option] = option_value
735        if domain_search_list_byte_string:
736            self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = \
737                    DomainListOption.unpack(domain_search_list_byte_string)
738
739
740    @property
741    def client_hw_address(self):
742        return self._fields.get(FIELD_CLIENT_HWADDR)
743
744    @property
745    def is_valid(self):
746        """
747        Checks that we have (at a minimum) values for all the required fields,
748        and that the magic cookie is set correctly.
749        """
750        for field in DHCP_REQUIRED_FIELDS:
751            if self._fields.get(field) is None:
752                logging.warning("Missing field %s in packet.", field)
753                return False
754        if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE:
755            return False
756        return True
757
758    @property
759    def message_type(self):
760        """
761        Gets the value of the DHCP Message Type option in this packet.
762
763        If the option is not present, or the value of the option is not
764        recognized, returns MESSAGE_TYPE_UNKNOWN.
765
766        @returns The MessageType for this packet, or MESSAGE_TYPE_UNKNOWN.
767        """
768        if (OPTION_DHCP_MESSAGE_TYPE in self._options and
769            self._options[OPTION_DHCP_MESSAGE_TYPE] > 0 and
770            self._options[OPTION_DHCP_MESSAGE_TYPE] < len(MESSAGE_TYPE_BY_NUM)):
771            return MESSAGE_TYPE_BY_NUM[self._options[OPTION_DHCP_MESSAGE_TYPE]]
772        else:
773            return MESSAGE_TYPE_UNKNOWN
774
775    @property
776    def transaction_id(self):
777        return self._fields.get(FIELD_TRANSACTION_ID)
778
779    def get_field(self, field):
780        return self._fields.get(field)
781
782    def get_option(self, option):
783        return self._options.get(option)
784
785    def set_field(self, field, field_value):
786        self._fields[field] = field_value
787
788    def set_option(self, option, option_value):
789        self._options[option] = option_value
790
791    def to_binary_string(self):
792        if not self.is_valid:
793            return None
794        # A list of byte strings to be joined into a single string at the end.
795        data = b""
796        offset = 0
797        for field in DHCP_ALL_FIELDS:
798            if field not in self._fields:
799                continue
800            field_data = field.pack(self._fields[field])
801            while offset < field.offset:
802                # This should only happen when we're padding the fields because
803                # we're not filling in legacy BOOTP stuff.
804                data += b"\x00"
805                offset += 1
806            data += field_data
807            offset += field.size
808        # Last field processed is the magic cookie, so we're ready for options.
809        # Have to process options
810        for option in DHCP_PACKET_OPTIONS:
811            option_value = self._options.get(option)
812            if option_value is None:
813                continue
814            serialized_value = option.pack(option_value)
815            data += struct.pack("BB", option.number, len(serialized_value))
816            offset += 2
817            data += serialized_value
818            offset += len(serialized_value)
819        data += get_bytes(chr(OPTION_END))
820        offset += 1
821        while offset < DHCP_MIN_PACKET_SIZE:
822            data += get_bytes(chr(OPTION_PAD))
823            offset += 1
824        return data
825
826    def __str__(self):
827        options = [k.name + "=" + str(v) for k, v in self._options.items()]
828        fields = [k.name + "=" + str(v) for k, v in self._fields.items()]
829        return "<DhcpPacket fields=%s, options=%s>" % (fields, options)
830