blob: 1facaa8493b96863f440462b7237c6774f27e7a5 [file] [log] [blame]
Derek Beckett3fff4b92020-10-20 08:27:06 -07001# Lint as: python2, python3
Christopher Wiley19644582012-08-16 19:32:07 -07002# 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,
Christopher Wiley30b095f2012-09-13 17:50:45 -070021 server_ip)
Christopher Wiley19644582012-08-16 19:32:07 -070022socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Christopher Wiley90c515d2012-09-18 15:50:08 -070023# Sending to the broadcast address needs special permissions.
Christopher Wiley19644582012-08-16 19:32:07 -070024socket.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
Derek Beckett3fff4b92020-10-20 08:27:06 -070031from __future__ import absolute_import
32from __future__ import division
33from __future__ import print_function
34
Christopher Wileya5f16db2012-09-12 17:12:42 -070035import collections
Christopher Wiley19644582012-08-16 19:32:07 -070036import logging
37import random
Derek Beckett3fff4b92020-10-20 08:27:06 -070038from six.moves import range
Christopher Wiley19644582012-08-16 19:32:07 -070039import socket
40import struct
41
Christopher Wiley90c515d2012-09-18 15:50:08 -070042
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
Christopher Wileya5f16db2012-09-12 17:12:42 -070054"""
Christopher Wiley90c515d2012-09-18 15:50:08 -070055Represents 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
Christopher Wileyd0a6e472012-09-18 15:50:49 -070059reflect that different kinds of options serialize to on the wire formats in
Christopher Wiley90c515d2012-09-18 15:50:08 -070060different ways.
Christopher Wiley19644582012-08-16 19:32:07 -070061
Christopher Wileya5f16db2012-09-12 17:12:42 -070062|name|
63A human readable name for this option.
Christopher Wiley19644582012-08-16 19:32:07 -070064
Christopher Wileya5f16db2012-09-12 17:12:42 -070065|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.
Christopher Wileya5f16db2012-09-12 17:12:42 -070069"""
Christopher Wiley90c515d2012-09-18 15:50:08 -070070Option = 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]
Christopher Wiley19644582012-08-16 19:32:07 -0700117
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700118
Paul Stewart584440a2012-11-16 09:42:04 -0800119class 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.
Derek Beckett2c1c37d2020-10-27 07:42:25 -0700134 destination_address_count = (prefix_size + 7) // 8
Paul Stewart584440a2012-11-16 09:42:04 -0800135 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])
Derek Beckett2c1c37d2020-10-27 07:42:25 -0700147 destination_address_count = (prefix_size + 7) // 8
Paul Stewart584440a2012-11-16 09:42:04 -0800148 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
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700166class 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
Christopher Wileya5f16db2012-09-12 17:12:42 -0700251"""
Christopher Wiley90c515d2012-09-18 15:50:08 -0700252Represents 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.
Christopher Wiley19644582012-08-16 19:32:07 -0700255
Christopher Wileya5f16db2012-09-12 17:12:42 -0700256|name|
Christopher Wiley90c515d2012-09-18 15:50:08 -0700257A human readable name for this field.
Christopher Wiley19644582012-08-16 19:32:07 -0700258
Christopher Wileya5f16db2012-09-12 17:12:42 -0700259|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.
Christopher Wiley19644582012-08-16 19:32:07 -0700263
Christopher Wileya5f16db2012-09-12 17:12:42 -0700264|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"""
Christopher Wiley90c515d2012-09-18 15:50:08 -0700269Field = 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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700279ServerNameField = CreatePacketPieceClass(Field, "!64s")
280
281BootFileField = CreatePacketPieceClass(Field, "!128s")
282
Christopher Wiley90c515d2012-09-18 15:50:08 -0700283class 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
Christopher Wiley19644582012-08-16 19:32:07 -0700292
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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700297IPV4_NULL_ADDRESS = "0.0.0.0"
Christopher Wiley19b39f62012-08-30 15:54:24 -0700298
Christopher Wiley19644582012-08-16 19:32:07 -0700299# These are required in every DHCP packet. Without these fields, the
300# packet will not even pass DhcpPacket.is_valid
Christopher Wiley90c515d2012-09-18 15:50:08 -0700301FIELD_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)
Paul Stewart3a37ed12012-10-26 13:01:49 -0700313# 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);
Christopher Wiley90c515d2012-09-18 15:50:08 -0700317FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4)
Christopher Wiley19644582012-08-16 19:32:07 -0700318
Christopher Wiley90c515d2012-09-18 15:50:08 -0700319OPTION_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)
Paul Stewart21529ce2015-01-26 12:04:00 -0800337OPTION_INTERFACE_MTU = ShortOption("interface_mtu", 26)
Paul Stewart8d2348b2013-12-02 13:40:41 -0800338OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption(
339 "vendor_encapsulated_options", 43)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700340OPTION_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)
Paul Stewartc0ec32d2015-06-17 23:39:05 -0700354OPTION_FULLY_QUALIFIED_DOMAIN_NAME = RawOption("fqdn", 81)
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700355OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119)
Paul Stewart584440a2012-11-16 09:42:04 -0800356OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption(
357 "classless_static_routes", 121)
Paul Stewart9616fbb2013-06-25 19:30:04 -0700358OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252)
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700359
Christopher Wiley19644582012-08-16 19:32:07 -0700360# 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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700366DHCP_COMMON_FIELDS = [
Christopher Wiley19644582012-08-16 19:32:07 -0700367 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,
Paul Stewart3a37ed12012-10-26 13:01:49 -0700379 ]
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,
Christopher Wiley19644582012-08-16 19:32:07 -0700388 FIELD_MAGIC_COOKIE,
389 ]
Christopher Wiley90c515d2012-09-18 15:50:08 -0700390
Christopher Wiley19644582012-08-16 19:32:07 -0700391# 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
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800402
403MessageType = collections.namedtuple('MessageType', 'name option_value')
Christopher Wiley19644582012-08-16 19:32:07 -0700404# From RFC2132, the valid DHCP message types are:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800405MESSAGE_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]
Christopher Wiley19644582012-08-16 19:32:07 -0700425
Christopher Wiley90c515d2012-09-18 15:50:08 -0700426OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [
Christopher Wiley30b095f2012-09-13 17:50:45 -0700427 OPTION_REQUESTED_IP.number,
428 OPTION_IP_LEASE_TIME.number,
429 OPTION_SERVER_ID.number,
Christopher Wiley90c515d2012-09-18 15:50:08 -0700430 OPTION_SUBNET_MASK.number,
431 OPTION_ROUTERS.number,
432 OPTION_DNS_SERVERS.number,
433 OPTION_HOST_NAME.number,
434 ]
Christopher Wiley19b39f62012-08-30 15:54:24 -0700435
Christopher Wiley19644582012-08-16 19:32:07 -0700436# 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,
Christopher Wiley19644582012-08-16 19:32:07 -0700456 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,
Paul Stewart21529ce2015-01-26 12:04:00 -0800471 OPTION_INTERFACE_MTU,
Paul Stewart8d2348b2013-12-02 13:40:41 -0800472 OPTION_VENDOR_ENCAPSULATED_OPTIONS,
Christopher Wiley19644582012-08-16 19:32:07 -0700473 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,
Paul Stewartc0ec32d2015-06-17 23:39:05 -0700487 OPTION_FULLY_QUALIFIED_DOMAIN_NAME,
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700488 OPTION_DNS_DOMAIN_SEARCH_LIST,
Paul Stewart584440a2012-11-16 09:42:04 -0800489 OPTION_CLASSLESS_STATIC_ROUTES,
Paul Stewart9616fbb2013-06-25 19:30:04 -0700490 OPTION_WEB_PROXY_AUTO_DISCOVERY,
Christopher Wiley19644582012-08-16 19:32:07 -0700491 ]
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()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700518 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800532 MESSAGE_TYPE_DISCOVERY.option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700533 return packet
534
535 @staticmethod
536 def create_offer_packet(transaction_id,
537 hwmac_addr,
538 offer_ip,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700539 server_ip):
Christopher Wiley19644582012-08-16 19:32:07 -0700540 """
541 Create an offer packet, given some fields that tie the packet to a
542 particular offer.
543 """
544 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700545 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)
Christopher Wiley19644582012-08-16 19:32:07 -0700548 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700549 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)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700554 packet.set_field(FIELD_YOUR_IP, offer_ip)
555 packet.set_field(FIELD_SERVER_IP, server_ip)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700556 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800560 MESSAGE_TYPE_OFFER.option_value)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700561 return packet
562
563 @staticmethod
564 def create_request_packet(transaction_id,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700565 hwmac_addr):
Christopher Wiley19b39f62012-08-30 15:54:24 -0700566 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700567 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)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700570 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700571 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)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700581 packet.set_option(OPTION_DHCP_MESSAGE_TYPE,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800582 MESSAGE_TYPE_REQUEST.option_value)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700583 return packet
584
585 @staticmethod
586 def create_acknowledgement_packet(transaction_id,
587 hwmac_addr,
588 granted_ip,
Christopher Wiley30b095f2012-09-13 17:50:45 -0700589 server_ip):
Christopher Wiley19b39f62012-08-30 15:54:24 -0700590 packet = DhcpPacket()
Christopher Wileya5f16db2012-09-12 17:12:42 -0700591 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)
Christopher Wiley19b39f62012-08-30 15:54:24 -0700594 # This has something to do with relay agents
Christopher Wileya5f16db2012-09-12 17:12:42 -0700595 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)
Christopher Wiley90c515d2012-09-18 15:50:08 -0700600 packet.set_field(FIELD_YOUR_IP, granted_ip)
601 packet.set_field(FIELD_SERVER_IP, server_ip)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700602 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,
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800606 MESSAGE_TYPE_ACK.option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700607 return packet
608
mukesh agrawal2b680b22014-04-15 10:31:12 -0700609 @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
Christopher Wiley19644582012-08-16 19:32:07 -0700636 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 = {}
Christopher Wiley19644582012-08-16 19:32:07 -0700657 if byte_str is None:
658 return
659 if len(byte_str) < OPTIONS_START_OFFSET + 1:
Christopher Wiley90c515d2012-09-18 15:50:08 -0700660 logging.error("Invalid byte string for packet.")
Christopher Wiley19644582012-08-16 19:32:07 -0700661 return
Paul Stewart3a37ed12012-10-26 13:01:49 -0700662 for field in DHCP_ALL_FIELDS:
Christopher Wiley90c515d2012-09-18 15:50:08 -0700663 self._fields[field] = field.unpack(byte_str[field.offset :
664 field.offset +
665 field.size])
Christopher Wiley19644582012-08-16 19:32:07 -0700666 offset = OPTIONS_START_OFFSET
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700667 domain_search_list_byte_string = ""
Christopher Wiley19644582012-08-16 19:32:07 -0700668 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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700677 option = get_dhcp_option_by_number(data_type)
678 if option is None:
679 logging.warning("Unsupported DHCP option found. "
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800680 "Option number: %d", data_type)
Christopher Wiley19644582012-08-16 19:32:07 -0700681 continue
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700682 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
Christopher Wiley90c515d2012-09-18 15:50:08 -0700689 option_value = option.unpack(data)
690 if option == OPTION_PARAMETER_REQUEST_LIST:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800691 logging.info("Requested options: %s", str(option_value))
Christopher Wiley90c515d2012-09-18 15:50:08 -0700692 self._options[option] = option_value
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700693 if domain_search_list_byte_string:
Matthew Wang2a2dc3c2019-10-08 14:43:49 -0700694 self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = \
695 DomainListOption.unpack(domain_search_list_byte_string)
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700696
Christopher Wiley19644582012-08-16 19:32:07 -0700697
698 @property
699 def client_hw_address(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700700 return self._fields.get(FIELD_CLIENT_HWADDR)
Christopher Wiley19644582012-08-16 19:32:07 -0700701
702 @property
703 def is_valid(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700704 """
Paul Stewart3a37ed12012-10-26 13:01:49 -0700705 Checks that we have (at a minimum) values for all the required fields,
706 and that the magic cookie is set correctly.
Christopher Wileya5f16db2012-09-12 17:12:42 -0700707 """
Paul Stewart3a37ed12012-10-26 13:01:49 -0700708 for field in DHCP_REQUIRED_FIELDS:
Christopher Wileya5f16db2012-09-12 17:12:42 -0700709 if self._fields.get(field) is None:
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800710 logging.warning("Missing field %s in packet.", field)
Christopher Wiley19644582012-08-16 19:32:07 -0700711 return False
Christopher Wileya5f16db2012-09-12 17:12:42 -0700712 if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE:
Christopher Wiley19644582012-08-16 19:32:07 -0700713 return False
714 return True
715
716 @property
717 def message_type(self):
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800718 """
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 """
Derek Beckett3fff4b92020-10-20 08:27:06 -0700726 if (OPTION_DHCP_MESSAGE_TYPE in self._options and
mukesh agrawal8962b2d2014-02-07 16:50:44 -0800727 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
Christopher Wiley19644582012-08-16 19:32:07 -0700732
733 @property
734 def transaction_id(self):
Christopher Wileya5f16db2012-09-12 17:12:42 -0700735 return self._fields.get(FIELD_TRANSACTION_ID)
Christopher Wiley19644582012-08-16 19:32:07 -0700736
Christopher Wileya5f16db2012-09-12 17:12:42 -0700737 def get_field(self, field):
738 return self._fields.get(field)
Christopher Wiley19644582012-08-16 19:32:07 -0700739
Christopher Wileya5f16db2012-09-12 17:12:42 -0700740 def get_option(self, option):
741 return self._options.get(option)
Christopher Wiley19644582012-08-16 19:32:07 -0700742
Christopher Wileya5f16db2012-09-12 17:12:42 -0700743 def set_field(self, field, field_value):
744 self._fields[field] = field_value
Christopher Wiley19644582012-08-16 19:32:07 -0700745
Christopher Wileya5f16db2012-09-12 17:12:42 -0700746 def set_option(self, option, option_value):
747 self._options[option] = option_value
Christopher Wiley19644582012-08-16 19:32:07 -0700748
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
Paul Stewart3a37ed12012-10-26 13:01:49 -0700755 for field in DHCP_ALL_FIELDS:
756 if field not in self._fields:
757 continue
Christopher Wiley90c515d2012-09-18 15:50:08 -0700758 field_data = field.pack(self._fields[field])
Christopher Wiley19644582012-08-16 19:32:07 -0700759 while offset < field.offset:
Christopher Wiley19b39f62012-08-30 15:54:24 -0700760 # This should only happen when we're padding the fields because
761 # we're not filling in legacy BOOTP stuff.
Christopher Wiley19644582012-08-16 19:32:07 -0700762 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:
Christopher Wileya5f16db2012-09-12 17:12:42 -0700769 option_value = self._options.get(option)
770 if option_value is None:
Christopher Wiley19644582012-08-16 19:32:07 -0700771 continue
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700772 serialized_value = option.pack(option_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700773 data.append(struct.pack("BB",
774 option.number,
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700775 len(serialized_value)))
Christopher Wiley19644582012-08-16 19:32:07 -0700776 offset += 2
Christopher Wileyd0a6e472012-09-18 15:50:49 -0700777 data.append(serialized_value)
778 offset += len(serialized_value)
Christopher Wiley19644582012-08-16 19:32:07 -0700779 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)
Christopher Wileya5f16db2012-09-12 17:12:42 -0700785
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)