blob: 0b591c0f2b7f1884f4821645ac9991fa6d150ecb [file] [log] [blame]
Christopher Wiley19644582012-08-16 19:32:07 -07001# 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,
Christopher Wiley30b095f2012-09-13 17:50:45 -070020 server_ip)
Christopher Wiley19644582012-08-16 19:32:07 -070021socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Christopher Wiley90c515d2012-09-18 15:50:08 -070022# Sending to the broadcast address needs special permissions.
Christopher Wiley19644582012-08-16 19:32:07 -070023socket.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
Christopher Wileya5f16db2012-09-12 17:12:42 -070030import collections
Christopher Wiley19644582012-08-16 19:32:07 -070031import logging
32import random
33import socket
34import struct
35
Christopher Wiley90c515d2012-09-18 15:50:08 -070036
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
Christopher Wileya5f16db2012-09-12 17:12:42 -070048"""
Christopher Wiley90c515d2012-09-18 15:50:08 -070049Represents 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
Christopher Wileyd0a6e472012-09-18 15:50:49 -070053reflect that different kinds of options serialize to on the wire formats in
Christopher Wiley90c515d2012-09-18 15:50:08 -070054different ways.
Christopher Wiley19644582012-08-16 19:32:07 -070055
Christopher Wileya5f16db2012-09-12 17:12:42 -070056|name|
57A human readable name for this option.
Christopher Wiley19644582012-08-16 19:32:07 -070058
Christopher Wileya5f16db2012-09-12 17:12:42 -070059|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.
Christopher Wileya5f16db2012-09-12 17:12:42 -070063"""
Christopher Wiley90c515d2012-09-18 15:50:08 -070064Option = 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]
Christopher Wiley19644582012-08-16 19:32:07 -0700111
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700112
Paul Stewart584440a2012-11-16 09:42:04 -0800113class 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
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700160class 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
Christopher Wileya5f16db2012-09-12 17:12:42 -0700245"""
Christopher Wiley90c515d2012-09-18 15:50:08 -0700246Represents 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.
Christopher Wiley19644582012-08-16 19:32:07 -0700249
Christopher Wileya5f16db2012-09-12 17:12:42 -0700250|name|
Christopher Wiley90c515d2012-09-18 15:50:08 -0700251A human readable name for this field.
Christopher Wiley19644582012-08-16 19:32:07 -0700252
Christopher Wileya5f16db2012-09-12 17:12:42 -0700253|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.
Christopher Wiley19644582012-08-16 19:32:07 -0700257
Christopher Wileya5f16db2012-09-12 17:12:42 -0700258|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"""
Christopher Wiley90c515d2012-09-18 15:50:08 -0700263Field = 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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700273ServerNameField = CreatePacketPieceClass(Field, "!64s")
274
275BootFileField = CreatePacketPieceClass(Field, "!128s")
276
Christopher Wiley90c515d2012-09-18 15:50:08 -0700277class 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
Christopher Wiley19644582012-08-16 19:32:07 -0700286
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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700291IPV4_NULL_ADDRESS = "0.0.0.0"
Christopher Wiley19b39f62012-08-30 15:54:24 -0700292
Christopher Wiley19644582012-08-16 19:32:07 -0700293# These are required in every DHCP packet. Without these fields, the
294# packet will not even pass DhcpPacket.is_valid
Christopher Wiley90c515d2012-09-18 15:50:08 -0700295FIELD_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)
Paul Stewart3a37ed12012-10-26 13:01:49 -0700307# 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);
Christopher Wiley90c515d2012-09-18 15:50:08 -0700311FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4)
Christopher Wiley19644582012-08-16 19:32:07 -0700312
Christopher Wiley90c515d2012-09-18 15:50:08 -0700313OPTION_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)
Paul Stewart21529ce2015-01-26 12:04:00 -0800331OPTION_INTERFACE_MTU = ShortOption("interface_mtu", 26)
Paul Stewart8d2348b2013-12-02 13:40:41 -0800332OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption(
333 "vendor_encapsulated_options", 43)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700334OPTION_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)
Paul Stewartc0ec32d2015-06-17 23:39:05 -0700348OPTION_FULLY_QUALIFIED_DOMAIN_NAME = RawOption("fqdn", 81)
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700349OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119)
Paul Stewart584440a2012-11-16 09:42:04 -0800350OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption(
351 "classless_static_routes", 121)
Paul Stewart9616fbb2013-06-25 19:30:04 -0700352OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252)
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700353
Christopher Wiley19644582012-08-16 19:32:07 -0700354# 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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700360DHCP_COMMON_FIELDS = [
Christopher Wiley19644582012-08-16 19:32:07 -0700361 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,
Paul Stewart3a37ed12012-10-26 13:01:49 -0700373 ]
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,
Christopher Wiley19644582012-08-16 19:32:07 -0700382 FIELD_MAGIC_COOKIE,
383 ]
Christopher Wiley90c515d2012-09-18 15:50:08 -0700384
Christopher Wiley19644582012-08-16 19:32:07 -0700385# 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
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800396
397MessageType = collections.namedtuple('MessageType', 'name option_value')
Christopher Wiley19644582012-08-16 19:32:07 -0700398# From RFC2132, the valid DHCP message types are:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800399MESSAGE_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]
Christopher Wiley19644582012-08-16 19:32:07 -0700419
Christopher Wiley90c515d2012-09-18 15:50:08 -0700420OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [
Christopher Wiley30b095f2012-09-13 17:50:45 -0700421 OPTION_REQUESTED_IP.number,
422 OPTION_IP_LEASE_TIME.number,
423 OPTION_SERVER_ID.number,
Christopher Wiley90c515d2012-09-18 15:50:08 -0700424 OPTION_SUBNET_MASK.number,
425 OPTION_ROUTERS.number,
426 OPTION_DNS_SERVERS.number,
427 OPTION_HOST_NAME.number,
428 ]
Christopher Wiley19b39f62012-08-30 15:54:24 -0700429
Christopher Wiley19644582012-08-16 19:32:07 -0700430# 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,
Christopher Wiley19644582012-08-16 19:32:07 -0700450 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,
Paul Stewart21529ce2015-01-26 12:04:00 -0800465 OPTION_INTERFACE_MTU,
Paul Stewart8d2348b2013-12-02 13:40:41 -0800466 OPTION_VENDOR_ENCAPSULATED_OPTIONS,
Christopher Wiley19644582012-08-16 19:32:07 -0700467 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,
Paul Stewartc0ec32d2015-06-17 23:39:05 -0700481 OPTION_FULLY_QUALIFIED_DOMAIN_NAME,
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700482 OPTION_DNS_DOMAIN_SEARCH_LIST,
Paul Stewart584440a2012-11-16 09:42:04 -0800483 OPTION_CLASSLESS_STATIC_ROUTES,
Paul Stewart9616fbb2013-06-25 19:30:04 -0700484 OPTION_WEB_PROXY_AUTO_DISCOVERY,
Christopher Wiley19644582012-08-16 19:32:07 -0700485 ]
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()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700512 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800526 MESSAGE_TYPE_DISCOVERY.option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700527 return packet
528
529 @staticmethod
530 def create_offer_packet(transaction_id,
531 hwmac_addr,
532 offer_ip,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700533 server_ip):
Christopher Wiley19644582012-08-16 19:32:07 -0700534 """
535 Create an offer packet, given some fields that tie the packet to a
536 particular offer.
537 """
538 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700539 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)
Christopher Wiley19644582012-08-16 19:32:07 -0700542 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700543 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)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700548 packet.set_field(FIELD_YOUR_IP, offer_ip)
549 packet.set_field(FIELD_SERVER_IP, server_ip)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700550 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800554 MESSAGE_TYPE_OFFER.option_value)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700555 return packet
556
557 @staticmethod
558 def create_request_packet(transaction_id,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700559 hwmac_addr):
Christopher Wiley19b39f62012-08-30 15:54:24 -0700560 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700561 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)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700564 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700565 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)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700575 packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800576 MESSAGE_TYPE_REQUEST.option_value)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700577 return packet
578
579 @staticmethod
580 def create_acknowledgement_packet(transaction_id,
581 hwmac_addr,
582 granted_ip,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700583 server_ip):
Christopher Wiley19b39f62012-08-30 15:54:24 -0700584 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700585 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)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700588 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700589 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)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700594 packet.set_field(FIELD_YOUR_IP, granted_ip)
595 packet.set_field(FIELD_SERVER_IP, server_ip)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700596 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800600 MESSAGE_TYPE_ACK.option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700601 return packet
602
mukesh agrawal2b680b22014-04-15 10:31:12 -0700603 @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
Christopher Wiley19644582012-08-16 19:32:07 -0700630 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 = {}
Christopher Wiley19644582012-08-16 19:32:07 -0700651 if byte_str is None:
652 return
653 if len(byte_str) < OPTIONS_START_OFFSET + 1:
Christopher Wiley90c515d2012-09-18 15:50:08 -0700654 logging.error("Invalid byte string for packet.")
Christopher Wiley19644582012-08-16 19:32:07 -0700655 return
Paul Stewart3a37ed12012-10-26 13:01:49 -0700656 for field in DHCP_ALL_FIELDS:
Christopher Wiley90c515d2012-09-18 15:50:08 -0700657 self._fields[field] = field.unpack(byte_str[field.offset :
658 field.offset +
659 field.size])
Christopher Wiley19644582012-08-16 19:32:07 -0700660 offset = OPTIONS_START_OFFSET
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700661 domain_search_list_byte_string = ""
Christopher Wiley19644582012-08-16 19:32:07 -0700662 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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700671 option = get_dhcp_option_by_number(data_type)
672 if option is None:
673 logging.warning("Unsupported DHCP option found. "
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800674 "Option number: %d", data_type)
Christopher Wiley19644582012-08-16 19:32:07 -0700675 continue
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700676 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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700683 option_value = option.unpack(data)
684 if option == OPTION_PARAMETER_REQUEST_LIST:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800685 logging.info("Requested options: %s", str(option_value))
Christopher Wiley90c515d2012-09-18 15:50:08 -0700686 self._options[option] = option_value
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700687 if domain_search_list_byte_string:
688 self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = option_value
689
Christopher Wiley19644582012-08-16 19:32:07 -0700690
691 @property
692 def client_hw_address(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700693 return self._fields.get(FIELD_CLIENT_HWADDR)
Christopher Wiley19644582012-08-16 19:32:07 -0700694
695 @property
696 def is_valid(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700697 """
Paul Stewart3a37ed12012-10-26 13:01:49 -0700698 Checks that we have (at a minimum) values for all the required fields,
699 and that the magic cookie is set correctly.
Christopher Wileya5f16db2012-09-12 17:12:42 -0700700 """
Paul Stewart3a37ed12012-10-26 13:01:49 -0700701 for field in DHCP_REQUIRED_FIELDS:
Christopher Wileya5f16db2012-09-12 17:12:42 -0700702 if self._fields.get(field) is None:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800703 logging.warning("Missing field %s in packet.", field)
Christopher Wiley19644582012-08-16 19:32:07 -0700704 return False
Christopher Wileya5f16db2012-09-12 17:12:42 -0700705 if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE:
Christopher Wiley19644582012-08-16 19:32:07 -0700706 return False
707 return True
708
709 @property
710 def message_type(self):
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800711 """
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
Christopher Wiley19644582012-08-16 19:32:07 -0700725
726 @property
727 def transaction_id(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700728 return self._fields.get(FIELD_TRANSACTION_ID)
Christopher Wiley19644582012-08-16 19:32:07 -0700729
Christopher Wileya5f16db2012-09-12 17:12:42 -0700730 def get_field(self, field):
731 return self._fields.get(field)
Christopher Wiley19644582012-08-16 19:32:07 -0700732
Christopher Wileya5f16db2012-09-12 17:12:42 -0700733 def get_option(self, option):
734 return self._options.get(option)
Christopher Wiley19644582012-08-16 19:32:07 -0700735
Christopher Wileya5f16db2012-09-12 17:12:42 -0700736 def set_field(self, field, field_value):
737 self._fields[field] = field_value
Christopher Wiley19644582012-08-16 19:32:07 -0700738
Christopher Wileya5f16db2012-09-12 17:12:42 -0700739 def set_option(self, option, option_value):
740 self._options[option] = option_value
Christopher Wiley19644582012-08-16 19:32:07 -0700741
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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700748 for field in DHCP_ALL_FIELDS:
749 if field not in self._fields:
750 continue
Christopher Wiley90c515d2012-09-18 15:50:08 -0700751 field_data = field.pack(self._fields[field])
Christopher Wiley19644582012-08-16 19:32:07 -0700752 while offset < field.offset:
Christopher Wiley19b39f62012-08-30 15:54:24 -0700753 # This should only happen when we're padding the fields because
754 # we're not filling in legacy BOOTP stuff.
Christopher Wiley19644582012-08-16 19:32:07 -0700755 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:
Christopher Wileya5f16db2012-09-12 17:12:42 -0700762 option_value = self._options.get(option)
763 if option_value is None:
Christopher Wiley19644582012-08-16 19:32:07 -0700764 continue
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700765 serialized_value = option.pack(option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700766 data.append(struct.pack("BB",
767 option.number,
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700768 len(serialized_value)))
Christopher Wiley19644582012-08-16 19:32:07 -0700769 offset += 2
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700770 data.append(serialized_value)
771 offset += len(serialized_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700772 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)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700778
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)