Derek Beckett | 3fff4b9 | 2020-10-20 08:27:06 -0700 | [diff] [blame] | 1 | # Lint as: python2, python3 |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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 | """ |
| 7 | Tools for serializing and deserializing DHCP packets. |
| 8 | |
| 9 | DhcpPacket is a class that represents a single DHCP packet and contains some |
| 10 | logic to create and parse binary strings containing on the wire DHCP packets. |
| 11 | |
| 12 | While you could call the constructor explicitly, most users should use the |
| 13 | static factories to construct packets with reasonable default values in most of |
| 14 | the fields, even if those values are zeros. |
| 15 | |
| 16 | For example: |
| 17 | |
| 18 | packet = dhcp_packet.create_offer_packet(transaction_id, |
| 19 | hwmac_addr, |
| 20 | offer_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 21 | server_ip) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 22 | socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 23 | # Sending to the broadcast address needs special permissions. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 24 | socket.sendto(response_packet.to_binary_string(), |
| 25 | ("255.255.255.255", 68)) |
| 26 | |
| 27 | Note that if you make changes, make sure that the tests in the bottom of this |
| 28 | file still pass. |
| 29 | """ |
| 30 | |
Derek Beckett | 3fff4b9 | 2020-10-20 08:27:06 -0700 | [diff] [blame] | 31 | from __future__ import absolute_import |
| 32 | from __future__ import division |
| 33 | from __future__ import print_function |
| 34 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 35 | import collections |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 36 | import logging |
| 37 | import random |
Derek Beckett | 3fff4b9 | 2020-10-20 08:27:06 -0700 | [diff] [blame] | 38 | from six.moves import range |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 39 | import socket |
| 40 | import struct |
| 41 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 42 | |
| 43 | def 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 54 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 55 | Represents an option in a DHCP packet. Options may or may not be present in any |
| 56 | given packet, depending on the configurations of the client and the server. |
| 57 | Using namedtuples as super classes gets us the comparison operators we want to |
| 58 | use these Options in dictionaries as keys. Below, we'll subclass Option to |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 59 | reflect that different kinds of options serialize to on the wire formats in |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 60 | different ways. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 61 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 62 | |name| |
| 63 | A human readable name for this option. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 64 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 65 | |number| |
| 66 | Every DHCP option has a number that goes into the packet to indicate |
| 67 | which particular option is being encoded in the next few bytes. This |
| 68 | property returns that number for each option. |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 69 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 70 | Option = collections.namedtuple("Option", ["name", "number"]) |
| 71 | |
| 72 | ByteOption = CreatePacketPieceClass(Option, "!B") |
| 73 | |
| 74 | ShortOption = CreatePacketPieceClass(Option, "!H") |
| 75 | |
| 76 | IntOption = CreatePacketPieceClass(Option, "!I") |
| 77 | |
| 78 | class 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 | |
| 88 | class 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 | |
| 99 | class 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 | |
| 109 | class 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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 117 | |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 118 | |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 119 | class 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 Beckett | 2c1c37d | 2020-10-27 07:42:25 -0700 | [diff] [blame] | 134 | destination_address_count = (prefix_size + 7) // 8 |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 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]) |
Derek Beckett | 2c1c37d | 2020-10-27 07:42:25 -0700 | [diff] [blame] | 147 | destination_address_count = (prefix_size + 7) // 8 |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 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 | |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 166 | class 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 251 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 252 | Represents a required field in a DHCP packet. Similar to Option, we'll |
| 253 | subclass Field to reflect that different fields serialize to on the wire formats |
| 254 | in different ways. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 255 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 256 | |name| |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 257 | A human readable name for this field. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 258 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 259 | |offset| |
| 260 | The |offset| for a field defines the starting byte of the field in the |
| 261 | binary packet string. |offset| is used during parsing, along with |
| 262 | |size| to extract the byte string of a field. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 263 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 264 | |size| |
| 265 | Fields in DHCP packets have a fixed size that must be respected. This |
| 266 | size property is used in parsing to indicate that |self._size| number of |
| 267 | bytes make up this field. |
| 268 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 269 | Field = collections.namedtuple("Field", ["name", "offset", "size"]) |
| 270 | |
| 271 | ByteField = CreatePacketPieceClass(Field, "!B") |
| 272 | |
| 273 | ShortField = CreatePacketPieceClass(Field, "!H") |
| 274 | |
| 275 | IntField = CreatePacketPieceClass(Field, "!I") |
| 276 | |
| 277 | HwAddrField = CreatePacketPieceClass(Field, "!16s") |
| 278 | |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 279 | ServerNameField = CreatePacketPieceClass(Field, "!64s") |
| 280 | |
| 281 | BootFileField = CreatePacketPieceClass(Field, "!128s") |
| 282 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 283 | class 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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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. |
| 295 | DHCP_MIN_PACKET_SIZE = 300 |
| 296 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 297 | IPV4_NULL_ADDRESS = "0.0.0.0" |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 298 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 299 | # These are required in every DHCP packet. Without these fields, the |
| 300 | # packet will not even pass DhcpPacket.is_valid |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 301 | FIELD_OP = ByteField("op", 0, 1) |
| 302 | FIELD_HWTYPE = ByteField("htype", 1, 1) |
| 303 | FIELD_HWADDR_LEN = ByteField("hlen", 2, 1) |
| 304 | FIELD_RELAY_HOPS = ByteField("hops", 3, 1) |
| 305 | FIELD_TRANSACTION_ID = IntField("xid", 4, 4) |
| 306 | FIELD_TIME_SINCE_START = ShortField("secs", 8, 2) |
| 307 | FIELD_FLAGS = ShortField("flags", 10, 2) |
| 308 | FIELD_CLIENT_IP = IpAddressField("ciaddr", 12, 4) |
| 309 | FIELD_YOUR_IP = IpAddressField("yiaddr", 16, 4) |
| 310 | FIELD_SERVER_IP = IpAddressField("siaddr", 20, 4) |
| 311 | FIELD_GATEWAY_IP = IpAddressField("giaddr", 24, 4) |
| 312 | FIELD_CLIENT_HWADDR = HwAddrField("chaddr", 28, 16) |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 313 | # The following two fields are considered "legacy BOOTP" fields but may |
| 314 | # sometimes be used by DHCP clients. |
| 315 | FIELD_LEGACY_SERVER_NAME = ServerNameField("servername", 44, 64); |
| 316 | FIELD_LEGACY_BOOT_FILE = BootFileField("bootfile", 108, 128); |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 317 | FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 318 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 319 | OPTION_TIME_OFFSET = IntOption("time_offset", 2) |
| 320 | OPTION_ROUTERS = IpListOption("routers", 3) |
| 321 | OPTION_SUBNET_MASK = IpAddressOption("subnet_mask", 1) |
| 322 | OPTION_TIME_SERVERS = IpListOption("time_servers", 4) |
| 323 | OPTION_NAME_SERVERS = IpListOption("name_servers", 5) |
| 324 | OPTION_DNS_SERVERS = IpListOption("dns_servers", 6) |
| 325 | OPTION_LOG_SERVERS = IpListOption("log_servers", 7) |
| 326 | OPTION_COOKIE_SERVERS = IpListOption("cookie_servers", 8) |
| 327 | OPTION_LPR_SERVERS = IpListOption("lpr_servers", 9) |
| 328 | OPTION_IMPRESS_SERVERS = IpListOption("impress_servers", 10) |
| 329 | OPTION_RESOURCE_LOC_SERVERS = IpListOption("resource_loc_servers", 11) |
| 330 | OPTION_HOST_NAME = RawOption("host_name", 12) |
| 331 | OPTION_BOOT_FILE_SIZE = ShortOption("boot_file_size", 13) |
| 332 | OPTION_MERIT_DUMP_FILE = RawOption("merit_dump_file", 14) |
| 333 | OPTION_DOMAIN_NAME = RawOption("domain_name", 15) |
| 334 | OPTION_SWAP_SERVER = IpAddressOption("swap_server", 16) |
| 335 | OPTION_ROOT_PATH = RawOption("root_path", 17) |
| 336 | OPTION_EXTENSIONS = RawOption("extensions", 18) |
Paul Stewart | 21529ce | 2015-01-26 12:04:00 -0800 | [diff] [blame] | 337 | OPTION_INTERFACE_MTU = ShortOption("interface_mtu", 26) |
Paul Stewart | 8d2348b | 2013-12-02 13:40:41 -0800 | [diff] [blame] | 338 | OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption( |
| 339 | "vendor_encapsulated_options", 43) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 340 | OPTION_REQUESTED_IP = IpAddressOption("requested_ip", 50) |
| 341 | OPTION_IP_LEASE_TIME = IntOption("ip_lease_time", 51) |
| 342 | OPTION_OPTION_OVERLOAD = ByteOption("option_overload", 52) |
| 343 | OPTION_DHCP_MESSAGE_TYPE = ByteOption("dhcp_message_type", 53) |
| 344 | OPTION_SERVER_ID = IpAddressOption("server_id", 54) |
| 345 | OPTION_PARAMETER_REQUEST_LIST = ByteListOption("parameter_request_list", 55) |
| 346 | OPTION_MESSAGE = RawOption("message", 56) |
| 347 | OPTION_MAX_DHCP_MESSAGE_SIZE = ShortOption("max_dhcp_message_size", 57) |
| 348 | OPTION_RENEWAL_T1_TIME_VALUE = IntOption("renewal_t1_time_value", 58) |
| 349 | OPTION_REBINDING_T2_TIME_VALUE = IntOption("rebinding_t2_time_value", 59) |
| 350 | OPTION_VENDOR_ID = RawOption("vendor_id", 60) |
| 351 | OPTION_CLIENT_ID = RawOption("client_id", 61) |
| 352 | OPTION_TFTP_SERVER_NAME = RawOption("tftp_server_name", 66) |
| 353 | OPTION_BOOTFILE_NAME = RawOption("bootfile_name", 67) |
Paul Stewart | c0ec32d | 2015-06-17 23:39:05 -0700 | [diff] [blame] | 354 | OPTION_FULLY_QUALIFIED_DOMAIN_NAME = RawOption("fqdn", 81) |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 355 | OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119) |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 356 | OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption( |
| 357 | "classless_static_routes", 121) |
Paul Stewart | 9616fbb | 2013-06-25 19:30:04 -0700 | [diff] [blame] | 358 | OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252) |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 359 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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). |
| 363 | OPTION_PAD = 0 |
| 364 | OPTION_END = 255 |
| 365 | |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 366 | DHCP_COMMON_FIELDS = [ |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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, |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 379 | ] |
| 380 | |
| 381 | DHCP_REQUIRED_FIELDS = DHCP_COMMON_FIELDS + [ |
| 382 | FIELD_MAGIC_COOKIE, |
| 383 | ] |
| 384 | |
| 385 | DHCP_ALL_FIELDS = DHCP_COMMON_FIELDS + [ |
| 386 | FIELD_LEGACY_SERVER_NAME, |
| 387 | FIELD_LEGACY_BOOT_FILE, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 388 | FIELD_MAGIC_COOKIE, |
| 389 | ] |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 390 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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. |
| 393 | FIELD_VALUE_OP_CLIENT_REQUEST = 1 |
| 394 | FIELD_VALUE_OP_SERVER_RESPONSE = 2 |
| 395 | # 1 == 10mb ethernet hardware address type (aka MAC). |
| 396 | FIELD_VALUE_HWTYPE_10MB_ETH = 1 |
| 397 | # MAC addresses are still 6 bytes long. |
| 398 | FIELD_VALUE_HWADDR_LEN_10MB_ETH = 6 |
| 399 | FIELD_VALUE_MAGIC_COOKIE = 0x63825363 |
| 400 | |
| 401 | OPTIONS_START_OFFSET = 240 |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 402 | |
| 403 | MessageType = collections.namedtuple('MessageType', 'name option_value') |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 404 | # From RFC2132, the valid DHCP message types are: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 405 | MESSAGE_TYPE_UNKNOWN = MessageType('UNKNOWN', 0) |
| 406 | MESSAGE_TYPE_DISCOVERY = MessageType('DISCOVERY', 1) |
| 407 | MESSAGE_TYPE_OFFER = MessageType('OFFER', 2) |
| 408 | MESSAGE_TYPE_REQUEST = MessageType('REQUEST', 3) |
| 409 | MESSAGE_TYPE_DECLINE = MessageType('DECLINE', 4) |
| 410 | MESSAGE_TYPE_ACK = MessageType('ACK', 5) |
| 411 | MESSAGE_TYPE_NAK = MessageType('NAK', 6) |
| 412 | MESSAGE_TYPE_RELEASE = MessageType('RELEASE', 7) |
| 413 | MESSAGE_TYPE_INFORM = MessageType('INFORM', 8) |
| 414 | MESSAGE_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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 425 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 426 | OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [ |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 427 | OPTION_REQUESTED_IP.number, |
| 428 | OPTION_IP_LEASE_TIME.number, |
| 429 | OPTION_SERVER_ID.number, |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 430 | OPTION_SUBNET_MASK.number, |
| 431 | OPTION_ROUTERS.number, |
| 432 | OPTION_DNS_SERVERS.number, |
| 433 | OPTION_HOST_NAME.number, |
| 434 | ] |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 435 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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. |
| 452 | DHCP_PACKET_OPTIONS = [ |
| 453 | OPTION_TIME_OFFSET, |
| 454 | OPTION_ROUTERS, |
| 455 | OPTION_SUBNET_MASK, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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, |
Paul Stewart | 21529ce | 2015-01-26 12:04:00 -0800 | [diff] [blame] | 471 | OPTION_INTERFACE_MTU, |
Paul Stewart | 8d2348b | 2013-12-02 13:40:41 -0800 | [diff] [blame] | 472 | OPTION_VENDOR_ENCAPSULATED_OPTIONS, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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, |
Paul Stewart | c0ec32d | 2015-06-17 23:39:05 -0700 | [diff] [blame] | 487 | OPTION_FULLY_QUALIFIED_DOMAIN_NAME, |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 488 | OPTION_DNS_DOMAIN_SEARCH_LIST, |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 489 | OPTION_CLASSLESS_STATIC_ROUTES, |
Paul Stewart | 9616fbb | 2013-06-25 19:30:04 -0700 | [diff] [blame] | 490 | OPTION_WEB_PROXY_AUTO_DISCOVERY, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 491 | ] |
| 492 | |
| 493 | def 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 | |
| 499 | class 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 532 | MESSAGE_TYPE_DISCOVERY.option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 533 | return packet |
| 534 | |
| 535 | @staticmethod |
| 536 | def create_offer_packet(transaction_id, |
| 537 | hwmac_addr, |
| 538 | offer_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 539 | server_ip): |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 540 | """ |
| 541 | Create an offer packet, given some fields that tie the packet to a |
| 542 | particular offer. |
| 543 | """ |
| 544 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 548 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 554 | packet.set_field(FIELD_YOUR_IP, offer_ip) |
| 555 | packet.set_field(FIELD_SERVER_IP, server_ip) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 560 | MESSAGE_TYPE_OFFER.option_value) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 561 | return packet |
| 562 | |
| 563 | @staticmethod |
| 564 | def create_request_packet(transaction_id, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 565 | hwmac_addr): |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 566 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 570 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 581 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 582 | MESSAGE_TYPE_REQUEST.option_value) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 583 | return packet |
| 584 | |
| 585 | @staticmethod |
| 586 | def create_acknowledgement_packet(transaction_id, |
| 587 | hwmac_addr, |
| 588 | granted_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 589 | server_ip): |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 590 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 594 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 600 | packet.set_field(FIELD_YOUR_IP, granted_ip) |
| 601 | packet.set_field(FIELD_SERVER_IP, server_ip) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 606 | MESSAGE_TYPE_ACK.option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 607 | return packet |
| 608 | |
mukesh agrawal | 2b680b2 | 2014-04-15 10:31:12 -0700 | [diff] [blame] | 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 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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 = {} |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 657 | if byte_str is None: |
| 658 | return |
| 659 | if len(byte_str) < OPTIONS_START_OFFSET + 1: |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 660 | logging.error("Invalid byte string for packet.") |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 661 | return |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 662 | for field in DHCP_ALL_FIELDS: |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 663 | self._fields[field] = field.unpack(byte_str[field.offset : |
| 664 | field.offset + |
| 665 | field.size]) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 666 | offset = OPTIONS_START_OFFSET |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 667 | domain_search_list_byte_string = "" |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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 |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 677 | option = get_dhcp_option_by_number(data_type) |
| 678 | if option is None: |
| 679 | logging.warning("Unsupported DHCP option found. " |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 680 | "Option number: %d", data_type) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 681 | continue |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 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 |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 689 | option_value = option.unpack(data) |
| 690 | if option == OPTION_PARAMETER_REQUEST_LIST: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 691 | logging.info("Requested options: %s", str(option_value)) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 692 | self._options[option] = option_value |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 693 | if domain_search_list_byte_string: |
Matthew Wang | 2a2dc3c | 2019-10-08 14:43:49 -0700 | [diff] [blame] | 694 | self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = \ |
| 695 | DomainListOption.unpack(domain_search_list_byte_string) |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 696 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 697 | |
| 698 | @property |
| 699 | def client_hw_address(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 700 | return self._fields.get(FIELD_CLIENT_HWADDR) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 701 | |
| 702 | @property |
| 703 | def is_valid(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 704 | """ |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 705 | Checks that we have (at a minimum) values for all the required fields, |
| 706 | and that the magic cookie is set correctly. |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 707 | """ |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 708 | for field in DHCP_REQUIRED_FIELDS: |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 709 | if self._fields.get(field) is None: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 710 | logging.warning("Missing field %s in packet.", field) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 711 | return False |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 712 | if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE: |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 713 | return False |
| 714 | return True |
| 715 | |
| 716 | @property |
| 717 | def message_type(self): |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 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 | """ |
Derek Beckett | 3fff4b9 | 2020-10-20 08:27:06 -0700 | [diff] [blame] | 726 | if (OPTION_DHCP_MESSAGE_TYPE in self._options and |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 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 |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 732 | |
| 733 | @property |
| 734 | def transaction_id(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 735 | return self._fields.get(FIELD_TRANSACTION_ID) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 736 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 737 | def get_field(self, field): |
| 738 | return self._fields.get(field) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 739 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 740 | def get_option(self, option): |
| 741 | return self._options.get(option) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 742 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 743 | def set_field(self, field, field_value): |
| 744 | self._fields[field] = field_value |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 745 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 746 | def set_option(self, option, option_value): |
| 747 | self._options[option] = option_value |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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 |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 755 | for field in DHCP_ALL_FIELDS: |
| 756 | if field not in self._fields: |
| 757 | continue |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 758 | field_data = field.pack(self._fields[field]) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 759 | while offset < field.offset: |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 760 | # This should only happen when we're padding the fields because |
| 761 | # we're not filling in legacy BOOTP stuff. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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: |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 769 | option_value = self._options.get(option) |
| 770 | if option_value is None: |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 771 | continue |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 772 | serialized_value = option.pack(option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 773 | data.append(struct.pack("BB", |
| 774 | option.number, |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 775 | len(serialized_value))) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 776 | offset += 2 |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 777 | data.append(serialized_value) |
| 778 | offset += len(serialized_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 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) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 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) |