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