Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 1 | # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """ |
| 6 | Tools for serializing and deserializing DHCP packets. |
| 7 | |
| 8 | DhcpPacket is a class that represents a single DHCP packet and contains some |
| 9 | logic to create and parse binary strings containing on the wire DHCP packets. |
| 10 | |
| 11 | While you could call the constructor explicitly, most users should use the |
| 12 | static factories to construct packets with reasonable default values in most of |
| 13 | the fields, even if those values are zeros. |
| 14 | |
| 15 | For example: |
| 16 | |
| 17 | packet = dhcp_packet.create_offer_packet(transaction_id, |
| 18 | hwmac_addr, |
| 19 | offer_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 20 | server_ip) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 21 | socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 22 | # Sending to the broadcast address needs special permissions. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 23 | socket.sendto(response_packet.to_binary_string(), |
| 24 | ("255.255.255.255", 68)) |
| 25 | |
| 26 | Note that if you make changes, make sure that the tests in the bottom of this |
| 27 | file still pass. |
| 28 | """ |
| 29 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 30 | import collections |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 31 | import logging |
| 32 | import random |
| 33 | import socket |
| 34 | import struct |
| 35 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 36 | |
| 37 | def 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 48 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 49 | Represents an option in a DHCP packet. Options may or may not be present in any |
| 50 | given packet, depending on the configurations of the client and the server. |
| 51 | Using namedtuples as super classes gets us the comparison operators we want to |
| 52 | 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] | 53 | 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] | 54 | different ways. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 55 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 56 | |name| |
| 57 | A human readable name for this option. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 58 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 59 | |number| |
| 60 | Every DHCP option has a number that goes into the packet to indicate |
| 61 | which particular option is being encoded in the next few bytes. This |
| 62 | property returns that number for each option. |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 63 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 64 | Option = collections.namedtuple("Option", ["name", "number"]) |
| 65 | |
| 66 | ByteOption = CreatePacketPieceClass(Option, "!B") |
| 67 | |
| 68 | ShortOption = CreatePacketPieceClass(Option, "!H") |
| 69 | |
| 70 | IntOption = CreatePacketPieceClass(Option, "!I") |
| 71 | |
| 72 | class 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 | |
| 82 | class 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 | |
| 93 | class 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 | |
| 103 | class 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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 111 | |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 112 | |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 113 | class 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 Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 160 | class 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 245 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 246 | Represents a required field in a DHCP packet. Similar to Option, we'll |
| 247 | subclass Field to reflect that different fields serialize to on the wire formats |
| 248 | in different ways. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 249 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 250 | |name| |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 251 | A human readable name for this field. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 252 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 253 | |offset| |
| 254 | The |offset| for a field defines the starting byte of the field in the |
| 255 | binary packet string. |offset| is used during parsing, along with |
| 256 | |size| to extract the byte string of a field. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 257 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 258 | |size| |
| 259 | Fields in DHCP packets have a fixed size that must be respected. This |
| 260 | size property is used in parsing to indicate that |self._size| number of |
| 261 | bytes make up this field. |
| 262 | """ |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 263 | Field = collections.namedtuple("Field", ["name", "offset", "size"]) |
| 264 | |
| 265 | ByteField = CreatePacketPieceClass(Field, "!B") |
| 266 | |
| 267 | ShortField = CreatePacketPieceClass(Field, "!H") |
| 268 | |
| 269 | IntField = CreatePacketPieceClass(Field, "!I") |
| 270 | |
| 271 | HwAddrField = CreatePacketPieceClass(Field, "!16s") |
| 272 | |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 273 | ServerNameField = CreatePacketPieceClass(Field, "!64s") |
| 274 | |
| 275 | BootFileField = CreatePacketPieceClass(Field, "!128s") |
| 276 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 277 | class 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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 286 | |
| 287 | # This is per RFC 2131. The wording doesn't seem to say that the packets must |
| 288 | # be this big, but that has been the historic assumption in implementations. |
| 289 | DHCP_MIN_PACKET_SIZE = 300 |
| 290 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 291 | IPV4_NULL_ADDRESS = "0.0.0.0" |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 292 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 293 | # These are required in every DHCP packet. Without these fields, the |
| 294 | # packet will not even pass DhcpPacket.is_valid |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 295 | FIELD_OP = ByteField("op", 0, 1) |
| 296 | FIELD_HWTYPE = ByteField("htype", 1, 1) |
| 297 | FIELD_HWADDR_LEN = ByteField("hlen", 2, 1) |
| 298 | FIELD_RELAY_HOPS = ByteField("hops", 3, 1) |
| 299 | FIELD_TRANSACTION_ID = IntField("xid", 4, 4) |
| 300 | FIELD_TIME_SINCE_START = ShortField("secs", 8, 2) |
| 301 | FIELD_FLAGS = ShortField("flags", 10, 2) |
| 302 | FIELD_CLIENT_IP = IpAddressField("ciaddr", 12, 4) |
| 303 | FIELD_YOUR_IP = IpAddressField("yiaddr", 16, 4) |
| 304 | FIELD_SERVER_IP = IpAddressField("siaddr", 20, 4) |
| 305 | FIELD_GATEWAY_IP = IpAddressField("giaddr", 24, 4) |
| 306 | FIELD_CLIENT_HWADDR = HwAddrField("chaddr", 28, 16) |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 307 | # The following two fields are considered "legacy BOOTP" fields but may |
| 308 | # sometimes be used by DHCP clients. |
| 309 | FIELD_LEGACY_SERVER_NAME = ServerNameField("servername", 44, 64); |
| 310 | FIELD_LEGACY_BOOT_FILE = BootFileField("bootfile", 108, 128); |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 311 | FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 312 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 313 | OPTION_TIME_OFFSET = IntOption("time_offset", 2) |
| 314 | OPTION_ROUTERS = IpListOption("routers", 3) |
| 315 | OPTION_SUBNET_MASK = IpAddressOption("subnet_mask", 1) |
| 316 | OPTION_TIME_SERVERS = IpListOption("time_servers", 4) |
| 317 | OPTION_NAME_SERVERS = IpListOption("name_servers", 5) |
| 318 | OPTION_DNS_SERVERS = IpListOption("dns_servers", 6) |
| 319 | OPTION_LOG_SERVERS = IpListOption("log_servers", 7) |
| 320 | OPTION_COOKIE_SERVERS = IpListOption("cookie_servers", 8) |
| 321 | OPTION_LPR_SERVERS = IpListOption("lpr_servers", 9) |
| 322 | OPTION_IMPRESS_SERVERS = IpListOption("impress_servers", 10) |
| 323 | OPTION_RESOURCE_LOC_SERVERS = IpListOption("resource_loc_servers", 11) |
| 324 | OPTION_HOST_NAME = RawOption("host_name", 12) |
| 325 | OPTION_BOOT_FILE_SIZE = ShortOption("boot_file_size", 13) |
| 326 | OPTION_MERIT_DUMP_FILE = RawOption("merit_dump_file", 14) |
| 327 | OPTION_DOMAIN_NAME = RawOption("domain_name", 15) |
| 328 | OPTION_SWAP_SERVER = IpAddressOption("swap_server", 16) |
| 329 | OPTION_ROOT_PATH = RawOption("root_path", 17) |
| 330 | OPTION_EXTENSIONS = RawOption("extensions", 18) |
Paul Stewart | 21529ce | 2015-01-26 12:04:00 -0800 | [diff] [blame] | 331 | OPTION_INTERFACE_MTU = ShortOption("interface_mtu", 26) |
Paul Stewart | 8d2348b | 2013-12-02 13:40:41 -0800 | [diff] [blame] | 332 | OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption( |
| 333 | "vendor_encapsulated_options", 43) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 334 | OPTION_REQUESTED_IP = IpAddressOption("requested_ip", 50) |
| 335 | OPTION_IP_LEASE_TIME = IntOption("ip_lease_time", 51) |
| 336 | OPTION_OPTION_OVERLOAD = ByteOption("option_overload", 52) |
| 337 | OPTION_DHCP_MESSAGE_TYPE = ByteOption("dhcp_message_type", 53) |
| 338 | OPTION_SERVER_ID = IpAddressOption("server_id", 54) |
| 339 | OPTION_PARAMETER_REQUEST_LIST = ByteListOption("parameter_request_list", 55) |
| 340 | OPTION_MESSAGE = RawOption("message", 56) |
| 341 | OPTION_MAX_DHCP_MESSAGE_SIZE = ShortOption("max_dhcp_message_size", 57) |
| 342 | OPTION_RENEWAL_T1_TIME_VALUE = IntOption("renewal_t1_time_value", 58) |
| 343 | OPTION_REBINDING_T2_TIME_VALUE = IntOption("rebinding_t2_time_value", 59) |
| 344 | OPTION_VENDOR_ID = RawOption("vendor_id", 60) |
| 345 | OPTION_CLIENT_ID = RawOption("client_id", 61) |
| 346 | OPTION_TFTP_SERVER_NAME = RawOption("tftp_server_name", 66) |
| 347 | OPTION_BOOTFILE_NAME = RawOption("bootfile_name", 67) |
Paul Stewart | c0ec32d | 2015-06-17 23:39:05 -0700 | [diff] [blame] | 348 | OPTION_FULLY_QUALIFIED_DOMAIN_NAME = RawOption("fqdn", 81) |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 349 | OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119) |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 350 | OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption( |
| 351 | "classless_static_routes", 121) |
Paul Stewart | 9616fbb | 2013-06-25 19:30:04 -0700 | [diff] [blame] | 352 | OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252) |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 353 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 354 | # Unlike every other option, which are tuples like: |
| 355 | # <number, length in bytes, data>, the pad and end options are just |
| 356 | # single bytes "\x00" and "\xff" (without length or data fields). |
| 357 | OPTION_PAD = 0 |
| 358 | OPTION_END = 255 |
| 359 | |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 360 | DHCP_COMMON_FIELDS = [ |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 361 | FIELD_OP, |
| 362 | FIELD_HWTYPE, |
| 363 | FIELD_HWADDR_LEN, |
| 364 | FIELD_RELAY_HOPS, |
| 365 | FIELD_TRANSACTION_ID, |
| 366 | FIELD_TIME_SINCE_START, |
| 367 | FIELD_FLAGS, |
| 368 | FIELD_CLIENT_IP, |
| 369 | FIELD_YOUR_IP, |
| 370 | FIELD_SERVER_IP, |
| 371 | FIELD_GATEWAY_IP, |
| 372 | FIELD_CLIENT_HWADDR, |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 373 | ] |
| 374 | |
| 375 | DHCP_REQUIRED_FIELDS = DHCP_COMMON_FIELDS + [ |
| 376 | FIELD_MAGIC_COOKIE, |
| 377 | ] |
| 378 | |
| 379 | DHCP_ALL_FIELDS = DHCP_COMMON_FIELDS + [ |
| 380 | FIELD_LEGACY_SERVER_NAME, |
| 381 | FIELD_LEGACY_BOOT_FILE, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 382 | FIELD_MAGIC_COOKIE, |
| 383 | ] |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 384 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 385 | # The op field in an ipv4 packet is either 1 or 2 depending on |
| 386 | # whether the packet is from a server or from a client. |
| 387 | FIELD_VALUE_OP_CLIENT_REQUEST = 1 |
| 388 | FIELD_VALUE_OP_SERVER_RESPONSE = 2 |
| 389 | # 1 == 10mb ethernet hardware address type (aka MAC). |
| 390 | FIELD_VALUE_HWTYPE_10MB_ETH = 1 |
| 391 | # MAC addresses are still 6 bytes long. |
| 392 | FIELD_VALUE_HWADDR_LEN_10MB_ETH = 6 |
| 393 | FIELD_VALUE_MAGIC_COOKIE = 0x63825363 |
| 394 | |
| 395 | OPTIONS_START_OFFSET = 240 |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 396 | |
| 397 | MessageType = collections.namedtuple('MessageType', 'name option_value') |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 398 | # From RFC2132, the valid DHCP message types are: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 399 | MESSAGE_TYPE_UNKNOWN = MessageType('UNKNOWN', 0) |
| 400 | MESSAGE_TYPE_DISCOVERY = MessageType('DISCOVERY', 1) |
| 401 | MESSAGE_TYPE_OFFER = MessageType('OFFER', 2) |
| 402 | MESSAGE_TYPE_REQUEST = MessageType('REQUEST', 3) |
| 403 | MESSAGE_TYPE_DECLINE = MessageType('DECLINE', 4) |
| 404 | MESSAGE_TYPE_ACK = MessageType('ACK', 5) |
| 405 | MESSAGE_TYPE_NAK = MessageType('NAK', 6) |
| 406 | MESSAGE_TYPE_RELEASE = MessageType('RELEASE', 7) |
| 407 | MESSAGE_TYPE_INFORM = MessageType('INFORM', 8) |
| 408 | MESSAGE_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 Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 419 | |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 420 | OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [ |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 421 | OPTION_REQUESTED_IP.number, |
| 422 | OPTION_IP_LEASE_TIME.number, |
| 423 | OPTION_SERVER_ID.number, |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 424 | OPTION_SUBNET_MASK.number, |
| 425 | OPTION_ROUTERS.number, |
| 426 | OPTION_DNS_SERVERS.number, |
| 427 | OPTION_HOST_NAME.number, |
| 428 | ] |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 429 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 430 | # These are possible options that may not be in every packet. |
| 431 | # Frequently, the client can include a bunch of options that indicate |
| 432 | # that it would like to receive information about time servers, routers, |
| 433 | # lpr servers, and much more, but the DHCP server can usually ignore |
| 434 | # those requests. |
| 435 | # |
| 436 | # Eventually, each option is encoded as: |
| 437 | # <option.number, option.size, [array of option.size bytes]> |
| 438 | # Unlike fields, which make up a fixed packet format, options can be in |
| 439 | # any order, except where they cannot. For instance, option 1 must |
| 440 | # follow option 3 if both are supplied. For this reason, potential |
| 441 | # options are in this list, and added to the packet in this order every |
| 442 | # time. |
| 443 | # |
| 444 | # size < 0 indicates that this is variable length field of at least |
| 445 | # abs(length) bytes in size. |
| 446 | DHCP_PACKET_OPTIONS = [ |
| 447 | OPTION_TIME_OFFSET, |
| 448 | OPTION_ROUTERS, |
| 449 | OPTION_SUBNET_MASK, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 450 | OPTION_TIME_SERVERS, |
| 451 | OPTION_NAME_SERVERS, |
| 452 | OPTION_DNS_SERVERS, |
| 453 | OPTION_LOG_SERVERS, |
| 454 | OPTION_COOKIE_SERVERS, |
| 455 | OPTION_LPR_SERVERS, |
| 456 | OPTION_IMPRESS_SERVERS, |
| 457 | OPTION_RESOURCE_LOC_SERVERS, |
| 458 | OPTION_HOST_NAME, |
| 459 | OPTION_BOOT_FILE_SIZE, |
| 460 | OPTION_MERIT_DUMP_FILE, |
| 461 | OPTION_SWAP_SERVER, |
| 462 | OPTION_DOMAIN_NAME, |
| 463 | OPTION_ROOT_PATH, |
| 464 | OPTION_EXTENSIONS, |
Paul Stewart | 21529ce | 2015-01-26 12:04:00 -0800 | [diff] [blame] | 465 | OPTION_INTERFACE_MTU, |
Paul Stewart | 8d2348b | 2013-12-02 13:40:41 -0800 | [diff] [blame] | 466 | OPTION_VENDOR_ENCAPSULATED_OPTIONS, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 467 | OPTION_REQUESTED_IP, |
| 468 | OPTION_IP_LEASE_TIME, |
| 469 | OPTION_OPTION_OVERLOAD, |
| 470 | OPTION_DHCP_MESSAGE_TYPE, |
| 471 | OPTION_SERVER_ID, |
| 472 | OPTION_PARAMETER_REQUEST_LIST, |
| 473 | OPTION_MESSAGE, |
| 474 | OPTION_MAX_DHCP_MESSAGE_SIZE, |
| 475 | OPTION_RENEWAL_T1_TIME_VALUE, |
| 476 | OPTION_REBINDING_T2_TIME_VALUE, |
| 477 | OPTION_VENDOR_ID, |
| 478 | OPTION_CLIENT_ID, |
| 479 | OPTION_TFTP_SERVER_NAME, |
| 480 | OPTION_BOOTFILE_NAME, |
Paul Stewart | c0ec32d | 2015-06-17 23:39:05 -0700 | [diff] [blame] | 481 | OPTION_FULLY_QUALIFIED_DOMAIN_NAME, |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 482 | OPTION_DNS_DOMAIN_SEARCH_LIST, |
Paul Stewart | 584440a | 2012-11-16 09:42:04 -0800 | [diff] [blame] | 483 | OPTION_CLASSLESS_STATIC_ROUTES, |
Paul Stewart | 9616fbb | 2013-06-25 19:30:04 -0700 | [diff] [blame] | 484 | OPTION_WEB_PROXY_AUTO_DISCOVERY, |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 485 | ] |
| 486 | |
| 487 | def 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 | |
| 493 | class 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 Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 512 | packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST) |
| 513 | packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| 514 | packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| 515 | packet.set_field(FIELD_RELAY_HOPS, 0) |
| 516 | packet.set_field(FIELD_TRANSACTION_ID, random.getrandbits(32)) |
| 517 | packet.set_field(FIELD_TIME_SINCE_START, 0) |
| 518 | packet.set_field(FIELD_FLAGS, 0) |
| 519 | packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| 520 | packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS) |
| 521 | packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS) |
| 522 | packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| 523 | packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| 524 | packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| 525 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 526 | MESSAGE_TYPE_DISCOVERY.option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 527 | return packet |
| 528 | |
| 529 | @staticmethod |
| 530 | def create_offer_packet(transaction_id, |
| 531 | hwmac_addr, |
| 532 | offer_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 533 | server_ip): |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 534 | """ |
| 535 | Create an offer packet, given some fields that tie the packet to a |
| 536 | particular offer. |
| 537 | """ |
| 538 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 539 | packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE) |
| 540 | packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| 541 | packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 542 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 543 | packet.set_field(FIELD_RELAY_HOPS, 0) |
| 544 | packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| 545 | packet.set_field(FIELD_TIME_SINCE_START, 0) |
| 546 | packet.set_field(FIELD_FLAGS, 0) |
| 547 | packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 548 | packet.set_field(FIELD_YOUR_IP, offer_ip) |
| 549 | packet.set_field(FIELD_SERVER_IP, server_ip) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 550 | packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| 551 | packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| 552 | packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| 553 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 554 | MESSAGE_TYPE_OFFER.option_value) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 555 | return packet |
| 556 | |
| 557 | @staticmethod |
| 558 | def create_request_packet(transaction_id, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 559 | hwmac_addr): |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 560 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 561 | packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST) |
| 562 | packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| 563 | packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 564 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 565 | packet.set_field(FIELD_RELAY_HOPS, 0) |
| 566 | packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| 567 | packet.set_field(FIELD_TIME_SINCE_START, 0) |
| 568 | packet.set_field(FIELD_FLAGS, 0) |
| 569 | packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| 570 | packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS) |
| 571 | packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS) |
| 572 | packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| 573 | packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| 574 | packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 575 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 576 | MESSAGE_TYPE_REQUEST.option_value) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 577 | return packet |
| 578 | |
| 579 | @staticmethod |
| 580 | def create_acknowledgement_packet(transaction_id, |
| 581 | hwmac_addr, |
| 582 | granted_ip, |
Christopher Wiley | 30b095f | 2012-09-13 17:50:45 -0700 | [diff] [blame] | 583 | server_ip): |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 584 | packet = DhcpPacket() |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 585 | packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE) |
| 586 | packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| 587 | packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 588 | # This has something to do with relay agents |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 589 | packet.set_field(FIELD_RELAY_HOPS, 0) |
| 590 | packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| 591 | packet.set_field(FIELD_TIME_SINCE_START, 0) |
| 592 | packet.set_field(FIELD_FLAGS, 0) |
| 593 | packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 594 | packet.set_field(FIELD_YOUR_IP, granted_ip) |
| 595 | packet.set_field(FIELD_SERVER_IP, server_ip) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 596 | packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| 597 | packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| 598 | packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| 599 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 600 | MESSAGE_TYPE_ACK.option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 601 | return packet |
| 602 | |
mukesh agrawal | 2b680b2 | 2014-04-15 10:31:12 -0700 | [diff] [blame] | 603 | @staticmethod |
| 604 | def create_nak_packet(transaction_id, hwmac_addr): |
| 605 | """ |
| 606 | Create a negative acknowledge packet. |
| 607 | |
| 608 | @param transaction_id: The DHCP transaction ID. |
| 609 | @param hwmac_addr: The client's MAC address. |
| 610 | """ |
| 611 | packet = DhcpPacket() |
| 612 | packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE) |
| 613 | packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| 614 | packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| 615 | # This has something to do with relay agents |
| 616 | packet.set_field(FIELD_RELAY_HOPS, 0) |
| 617 | packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| 618 | packet.set_field(FIELD_TIME_SINCE_START, 0) |
| 619 | packet.set_field(FIELD_FLAGS, 0) |
| 620 | packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| 621 | packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS) |
| 622 | packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS) |
| 623 | packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| 624 | packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| 625 | packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| 626 | packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
| 627 | MESSAGE_TYPE_NAK.option_value) |
| 628 | return packet |
| 629 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 630 | def __init__(self, byte_str=None): |
| 631 | """ |
| 632 | Create a DhcpPacket, filling in fields from a byte string if given. |
| 633 | |
| 634 | Assumes that the packet starts at offset 0 in the binary string. This |
| 635 | includes the fields and options. Fields are different from options in |
| 636 | that we bother to decode these into more usable data types like |
| 637 | integers rather than keeping them as raw byte strings. Fields are also |
| 638 | required to exist, unlike options which may not. |
| 639 | |
| 640 | Each option is encoded as a tuple <option number, length, data> where |
| 641 | option number is a byte indicating the type of option, length indicates |
| 642 | the number of bytes in the data for option, and data is a length array |
| 643 | of bytes. The only exceptions to this rule are the 0 and 255 options, |
| 644 | which have 0 data length, and no length byte. These tuples are then |
| 645 | simply appended to each other. This encoding is the same as the BOOTP |
| 646 | vendor extention field encoding. |
| 647 | """ |
| 648 | super(DhcpPacket, self).__init__() |
| 649 | self._options = {} |
| 650 | self._fields = {} |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 651 | if byte_str is None: |
| 652 | return |
| 653 | if len(byte_str) < OPTIONS_START_OFFSET + 1: |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 654 | logging.error("Invalid byte string for packet.") |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 655 | return |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 656 | for field in DHCP_ALL_FIELDS: |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 657 | self._fields[field] = field.unpack(byte_str[field.offset : |
| 658 | field.offset + |
| 659 | field.size]) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 660 | offset = OPTIONS_START_OFFSET |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 661 | domain_search_list_byte_string = "" |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 662 | while offset < len(byte_str) and ord(byte_str[offset]) != OPTION_END: |
| 663 | data_type = ord(byte_str[offset]) |
| 664 | offset += 1 |
| 665 | if data_type == OPTION_PAD: |
| 666 | continue |
| 667 | data_length = ord(byte_str[offset]) |
| 668 | offset += 1 |
| 669 | data = byte_str[offset: offset + data_length] |
| 670 | offset += data_length |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 671 | option = get_dhcp_option_by_number(data_type) |
| 672 | if option is None: |
| 673 | logging.warning("Unsupported DHCP option found. " |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 674 | "Option number: %d", data_type) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 675 | continue |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 676 | if option == OPTION_DNS_DOMAIN_SEARCH_LIST: |
| 677 | # In a cruel twist of fate, the server is allowed to give |
| 678 | # multiple options with this number. The client is expected to |
| 679 | # concatenate the byte strings together and use it as a single |
| 680 | # value. |
| 681 | domain_search_list_byte_string += data |
| 682 | continue |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 683 | option_value = option.unpack(data) |
| 684 | if option == OPTION_PARAMETER_REQUEST_LIST: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 685 | logging.info("Requested options: %s", str(option_value)) |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 686 | self._options[option] = option_value |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 687 | if domain_search_list_byte_string: |
| 688 | self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = option_value |
| 689 | |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 690 | |
| 691 | @property |
| 692 | def client_hw_address(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 693 | return self._fields.get(FIELD_CLIENT_HWADDR) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 694 | |
| 695 | @property |
| 696 | def is_valid(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 697 | """ |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 698 | Checks that we have (at a minimum) values for all the required fields, |
| 699 | and that the magic cookie is set correctly. |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 700 | """ |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 701 | for field in DHCP_REQUIRED_FIELDS: |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 702 | if self._fields.get(field) is None: |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 703 | logging.warning("Missing field %s in packet.", field) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 704 | return False |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 705 | if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE: |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 706 | return False |
| 707 | return True |
| 708 | |
| 709 | @property |
| 710 | def message_type(self): |
mukesh agrawal | 8962b2d | 2014-02-07 16:50:44 -0800 | [diff] [blame] | 711 | """ |
| 712 | Gets the value of the DHCP Message Type option in this packet. |
| 713 | |
| 714 | If the option is not present, or the value of the option is not |
| 715 | recognized, returns MESSAGE_TYPE_UNKNOWN. |
| 716 | |
| 717 | @returns The MessageType for this packet, or MESSAGE_TYPE_UNKNOWN. |
| 718 | """ |
| 719 | if (self._options.has_key(OPTION_DHCP_MESSAGE_TYPE) and |
| 720 | self._options[OPTION_DHCP_MESSAGE_TYPE] > 0 and |
| 721 | self._options[OPTION_DHCP_MESSAGE_TYPE] < len(MESSAGE_TYPE_BY_NUM)): |
| 722 | return MESSAGE_TYPE_BY_NUM[self._options[OPTION_DHCP_MESSAGE_TYPE]] |
| 723 | else: |
| 724 | return MESSAGE_TYPE_UNKNOWN |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 725 | |
| 726 | @property |
| 727 | def transaction_id(self): |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 728 | return self._fields.get(FIELD_TRANSACTION_ID) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 729 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 730 | def get_field(self, field): |
| 731 | return self._fields.get(field) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 732 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 733 | def get_option(self, option): |
| 734 | return self._options.get(option) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 735 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 736 | def set_field(self, field, field_value): |
| 737 | self._fields[field] = field_value |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 738 | |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 739 | def set_option(self, option, option_value): |
| 740 | self._options[option] = option_value |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 741 | |
| 742 | def to_binary_string(self): |
| 743 | if not self.is_valid: |
| 744 | return None |
| 745 | # A list of byte strings to be joined into a single string at the end. |
| 746 | data = [] |
| 747 | offset = 0 |
Paul Stewart | 3a37ed1 | 2012-10-26 13:01:49 -0700 | [diff] [blame] | 748 | for field in DHCP_ALL_FIELDS: |
| 749 | if field not in self._fields: |
| 750 | continue |
Christopher Wiley | 90c515d | 2012-09-18 15:50:08 -0700 | [diff] [blame] | 751 | field_data = field.pack(self._fields[field]) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 752 | while offset < field.offset: |
Christopher Wiley | 19b39f6 | 2012-08-30 15:54:24 -0700 | [diff] [blame] | 753 | # This should only happen when we're padding the fields because |
| 754 | # we're not filling in legacy BOOTP stuff. |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 755 | data.append("\x00") |
| 756 | offset += 1 |
| 757 | data.append(field_data) |
| 758 | offset += field.size |
| 759 | # Last field processed is the magic cookie, so we're ready for options. |
| 760 | # Have to process options |
| 761 | for option in DHCP_PACKET_OPTIONS: |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 762 | option_value = self._options.get(option) |
| 763 | if option_value is None: |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 764 | continue |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 765 | serialized_value = option.pack(option_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 766 | data.append(struct.pack("BB", |
| 767 | option.number, |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 768 | len(serialized_value))) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 769 | offset += 2 |
Christopher Wiley | d0a6e47 | 2012-09-18 15:50:49 -0700 | [diff] [blame] | 770 | data.append(serialized_value) |
| 771 | offset += len(serialized_value) |
Christopher Wiley | 1964458 | 2012-08-16 19:32:07 -0700 | [diff] [blame] | 772 | data.append(chr(OPTION_END)) |
| 773 | offset += 1 |
| 774 | while offset < DHCP_MIN_PACKET_SIZE: |
| 775 | data.append(chr(OPTION_PAD)) |
| 776 | offset += 1 |
| 777 | return "".join(data) |
Christopher Wiley | a5f16db | 2012-09-12 17:12:42 -0700 | [diff] [blame] | 778 | |
| 779 | def __str__(self): |
| 780 | options = [k.name + "=" + str(v) for k, v in self._options.items()] |
| 781 | fields = [k.name + "=" + str(v) for k, v in self._fields.items()] |
| 782 | return "<DhcpPacket fields=%s, options=%s>" % (fields, options) |