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