| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| Tools for serializing and deserializing DHCP packets. |
| |
| DhcpPacket is a class that represents a single DHCP packet and contains some |
| logic to create and parse binary strings containing on the wire DHCP packets. |
| |
| While you could call the constructor explicitly, most users should use the |
| static factories to construct packets with reasonable default values in most of |
| the fields, even if those values are zeros. |
| |
| For example: |
| |
| packet = dhcp_packet.create_offer_packet(transaction_id, |
| hwmac_addr, |
| offer_ip, |
| server_ip) |
| socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| # Sending to the broadcast address needs special permissions. |
| socket.sendto(response_packet.to_binary_string(), |
| ("255.255.255.255", 68)) |
| |
| Note that if you make changes, make sure that the tests in the bottom of this |
| file still pass. |
| """ |
| |
| import collections |
| import logging |
| import random |
| import socket |
| import struct |
| |
| |
| def CreatePacketPieceClass(super_class, field_format): |
| class PacketPiece(super_class): |
| @staticmethod |
| def pack(value): |
| return struct.pack(field_format, value) |
| |
| @staticmethod |
| def unpack(byte_string): |
| return struct.unpack(field_format, byte_string)[0] |
| return PacketPiece |
| |
| """ |
| Represents an option in a DHCP packet. Options may or may not be present in any |
| given packet, depending on the configurations of the client and the server. |
| Using namedtuples as super classes gets us the comparison operators we want to |
| use these Options in dictionaries as keys. Below, we'll subclass Option to |
| reflect that different kinds of options serialize to on the wire formats in |
| different ways. |
| |
| |name| |
| A human readable name for this option. |
| |
| |number| |
| Every DHCP option has a number that goes into the packet to indicate |
| which particular option is being encoded in the next few bytes. This |
| property returns that number for each option. |
| """ |
| Option = collections.namedtuple("Option", ["name", "number"]) |
| |
| ByteOption = CreatePacketPieceClass(Option, "!B") |
| |
| ShortOption = CreatePacketPieceClass(Option, "!H") |
| |
| IntOption = CreatePacketPieceClass(Option, "!I") |
| |
| class IpAddressOption(Option): |
| @staticmethod |
| def pack(value): |
| return socket.inet_aton(value) |
| |
| @staticmethod |
| def unpack(byte_string): |
| return socket.inet_ntoa(byte_string) |
| |
| |
| class IpListOption(Option): |
| @staticmethod |
| def pack(value): |
| return "".join([socket.inet_aton(addr) for addr in value]) |
| |
| @staticmethod |
| def unpack(byte_string): |
| return [socket.inet_ntoa(byte_string[idx:idx+4]) |
| for idx in range(0, len(byte_string), 4)] |
| |
| |
| class RawOption(Option): |
| @staticmethod |
| def pack(value): |
| return value |
| |
| @staticmethod |
| def unpack(byte_string): |
| return byte_string |
| |
| |
| class ByteListOption(Option): |
| @staticmethod |
| def pack(value): |
| return "".join(chr(v) for v in value) |
| |
| @staticmethod |
| def unpack(byte_string): |
| return [ord(c) for c in byte_string] |
| |
| |
| class ClasslessStaticRoutesOption(Option): |
| """ |
| This is a RFC 3442 compliant classless static route option parser and |
| serializer. The symbolic "value" packed and unpacked from this class |
| is a list (prefix_size, destination, router) tuples. |
| """ |
| |
| @staticmethod |
| def pack(value): |
| route_list = value |
| byte_string = "" |
| for prefix_size, destination, router in route_list: |
| byte_string += chr(prefix_size) |
| # Encode only the significant octets of the destination |
| # that fall within the prefix. |
| destination_address_count = (prefix_size + 7) / 8 |
| destination_address = socket.inet_aton(destination) |
| byte_string += destination_address[:destination_address_count] |
| byte_string += socket.inet_aton(router) |
| |
| return byte_string |
| |
| @staticmethod |
| def unpack(byte_string): |
| route_list = [] |
| offset = 0 |
| while offset < len(byte_string): |
| prefix_size = ord(byte_string[offset]) |
| destination_address_count = (prefix_size + 7) / 8 |
| entry_end = offset + 1 + destination_address_count + 4 |
| if entry_end > len(byte_string): |
| raise Exception("Classless domain list is corrupted.") |
| offset += 1 |
| destination_address_end = offset + destination_address_count |
| destination_address = byte_string[offset:destination_address_end] |
| # Pad the destination address bytes with zero byte octets to |
| # fill out an IPv4 address. |
| destination_address += '\x00' * (4 - destination_address_count) |
| router_address = byte_string[destination_address_end:entry_end] |
| route_list.append((prefix_size, |
| socket.inet_ntoa(destination_address), |
| socket.inet_ntoa(router_address))) |
| offset = entry_end |
| |
| return route_list |
| |
| |
| class DomainListOption(Option): |
| """ |
| This is a RFC 1035 compliant domain list option parser and serializer. |
| There are some clever compression optimizations that it does not implement |
| for serialization, but correctly parses. This should be sufficient for |
| testing. |
| """ |
| # Various RFC's let you finish a domain name by pointing to an existing |
| # domain name rather than repeating the same suffix. All such pointers are |
| # two bytes long, specify the offset in the byte string, and begin with |
| # |POINTER_PREFIX| to distinguish them from normal characters. |
| POINTER_PREFIX = ord("\xC0") |
| |
| @staticmethod |
| def pack(value): |
| domain_list = value |
| byte_string = "" |
| for domain in domain_list: |
| for part in domain.split("."): |
| byte_string += chr(len(part)) |
| byte_string += part |
| byte_string += "\x00" |
| return byte_string |
| |
| @staticmethod |
| def unpack(byte_string): |
| domain_list = [] |
| offset = 0 |
| try: |
| while offset < len(byte_string): |
| (new_offset, domain_parts) = DomainListOption._read_domain_name( |
| byte_string, |
| offset) |
| domain_name = ".".join(domain_parts) |
| domain_list.append(domain_name) |
| if new_offset <= offset: |
| raise Exception("Parsing logic error is letting domain " |
| "list parsing go on forever.") |
| offset = new_offset |
| except ValueError: |
| # Badly formatted packets are not necessarily test errors. |
| logging.warning("Found badly formatted DHCP domain search list") |
| return None |
| return domain_list |
| |
| @staticmethod |
| def _read_domain_name(byte_string, offset): |
| """ |
| Recursively parse a domain name from a domain name list. |
| """ |
| parts = [] |
| while True: |
| if offset >= len(byte_string): |
| raise ValueError("Domain list ended without a NULL byte.") |
| maybe_part_len = ord(byte_string[offset]) |
| offset += 1 |
| if maybe_part_len == 0: |
| # Domains are terminated with either a 0 or a pointer to a |
| # domain suffix within |byte_string|. |
| return (offset, parts) |
| elif ((maybe_part_len & DomainListOption.POINTER_PREFIX) == |
| DomainListOption.POINTER_PREFIX): |
| if offset >= len(byte_string): |
| raise ValueError("Missing second byte of domain suffix " |
| "pointer.") |
| maybe_part_len &= ~DomainListOption.POINTER_PREFIX |
| pointer_offset = ((maybe_part_len << 8) + |
| ord(byte_string[offset])) |
| offset += 1 |
| (_, more_parts) = DomainListOption._read_domain_name( |
| byte_string, |
| pointer_offset) |
| parts.extend(more_parts) |
| return (offset, parts) |
| else: |
| # That byte was actually the length of the next part, not a |
| # pointer back into the data. |
| part_len = maybe_part_len |
| if offset + part_len >= len(byte_string): |
| raise ValueError("Part of a domain goes beyond data " |
| "length.") |
| parts.append(byte_string[offset : offset + part_len]) |
| offset += part_len |
| |
| |
| """ |
| Represents a required field in a DHCP packet. Similar to Option, we'll |
| subclass Field to reflect that different fields serialize to on the wire formats |
| in different ways. |
| |
| |name| |
| A human readable name for this field. |
| |
| |offset| |
| The |offset| for a field defines the starting byte of the field in the |
| binary packet string. |offset| is used during parsing, along with |
| |size| to extract the byte string of a field. |
| |
| |size| |
| Fields in DHCP packets have a fixed size that must be respected. This |
| size property is used in parsing to indicate that |self._size| number of |
| bytes make up this field. |
| """ |
| Field = collections.namedtuple("Field", ["name", "offset", "size"]) |
| |
| ByteField = CreatePacketPieceClass(Field, "!B") |
| |
| ShortField = CreatePacketPieceClass(Field, "!H") |
| |
| IntField = CreatePacketPieceClass(Field, "!I") |
| |
| HwAddrField = CreatePacketPieceClass(Field, "!16s") |
| |
| ServerNameField = CreatePacketPieceClass(Field, "!64s") |
| |
| BootFileField = CreatePacketPieceClass(Field, "!128s") |
| |
| class IpAddressField(Field): |
| @staticmethod |
| def pack(value): |
| return socket.inet_aton(value) |
| |
| @staticmethod |
| def unpack(byte_string): |
| return socket.inet_ntoa(byte_string) |
| |
| |
| # This is per RFC 2131. The wording doesn't seem to say that the packets must |
| # be this big, but that has been the historic assumption in implementations. |
| DHCP_MIN_PACKET_SIZE = 300 |
| |
| IPV4_NULL_ADDRESS = "0.0.0.0" |
| |
| # These are required in every DHCP packet. Without these fields, the |
| # packet will not even pass DhcpPacket.is_valid |
| FIELD_OP = ByteField("op", 0, 1) |
| FIELD_HWTYPE = ByteField("htype", 1, 1) |
| FIELD_HWADDR_LEN = ByteField("hlen", 2, 1) |
| FIELD_RELAY_HOPS = ByteField("hops", 3, 1) |
| FIELD_TRANSACTION_ID = IntField("xid", 4, 4) |
| FIELD_TIME_SINCE_START = ShortField("secs", 8, 2) |
| FIELD_FLAGS = ShortField("flags", 10, 2) |
| FIELD_CLIENT_IP = IpAddressField("ciaddr", 12, 4) |
| FIELD_YOUR_IP = IpAddressField("yiaddr", 16, 4) |
| FIELD_SERVER_IP = IpAddressField("siaddr", 20, 4) |
| FIELD_GATEWAY_IP = IpAddressField("giaddr", 24, 4) |
| FIELD_CLIENT_HWADDR = HwAddrField("chaddr", 28, 16) |
| # The following two fields are considered "legacy BOOTP" fields but may |
| # sometimes be used by DHCP clients. |
| FIELD_LEGACY_SERVER_NAME = ServerNameField("servername", 44, 64); |
| FIELD_LEGACY_BOOT_FILE = BootFileField("bootfile", 108, 128); |
| FIELD_MAGIC_COOKIE = IntField("magic_cookie", 236, 4) |
| |
| OPTION_TIME_OFFSET = IntOption("time_offset", 2) |
| OPTION_ROUTERS = IpListOption("routers", 3) |
| OPTION_SUBNET_MASK = IpAddressOption("subnet_mask", 1) |
| OPTION_TIME_SERVERS = IpListOption("time_servers", 4) |
| OPTION_NAME_SERVERS = IpListOption("name_servers", 5) |
| OPTION_DNS_SERVERS = IpListOption("dns_servers", 6) |
| OPTION_LOG_SERVERS = IpListOption("log_servers", 7) |
| OPTION_COOKIE_SERVERS = IpListOption("cookie_servers", 8) |
| OPTION_LPR_SERVERS = IpListOption("lpr_servers", 9) |
| OPTION_IMPRESS_SERVERS = IpListOption("impress_servers", 10) |
| OPTION_RESOURCE_LOC_SERVERS = IpListOption("resource_loc_servers", 11) |
| OPTION_HOST_NAME = RawOption("host_name", 12) |
| OPTION_BOOT_FILE_SIZE = ShortOption("boot_file_size", 13) |
| OPTION_MERIT_DUMP_FILE = RawOption("merit_dump_file", 14) |
| OPTION_DOMAIN_NAME = RawOption("domain_name", 15) |
| OPTION_SWAP_SERVER = IpAddressOption("swap_server", 16) |
| OPTION_ROOT_PATH = RawOption("root_path", 17) |
| OPTION_EXTENSIONS = RawOption("extensions", 18) |
| OPTION_VENDOR_ENCAPSULATED_OPTIONS = RawOption( |
| "vendor_encapsulated_options", 43) |
| OPTION_REQUESTED_IP = IpAddressOption("requested_ip", 50) |
| OPTION_IP_LEASE_TIME = IntOption("ip_lease_time", 51) |
| OPTION_OPTION_OVERLOAD = ByteOption("option_overload", 52) |
| OPTION_DHCP_MESSAGE_TYPE = ByteOption("dhcp_message_type", 53) |
| OPTION_SERVER_ID = IpAddressOption("server_id", 54) |
| OPTION_PARAMETER_REQUEST_LIST = ByteListOption("parameter_request_list", 55) |
| OPTION_MESSAGE = RawOption("message", 56) |
| OPTION_MAX_DHCP_MESSAGE_SIZE = ShortOption("max_dhcp_message_size", 57) |
| OPTION_RENEWAL_T1_TIME_VALUE = IntOption("renewal_t1_time_value", 58) |
| OPTION_REBINDING_T2_TIME_VALUE = IntOption("rebinding_t2_time_value", 59) |
| OPTION_VENDOR_ID = RawOption("vendor_id", 60) |
| OPTION_CLIENT_ID = RawOption("client_id", 61) |
| OPTION_TFTP_SERVER_NAME = RawOption("tftp_server_name", 66) |
| OPTION_BOOTFILE_NAME = RawOption("bootfile_name", 67) |
| OPTION_DNS_DOMAIN_SEARCH_LIST = DomainListOption("domain_search_list", 119) |
| OPTION_CLASSLESS_STATIC_ROUTES = ClasslessStaticRoutesOption( |
| "classless_static_routes", 121) |
| OPTION_WEB_PROXY_AUTO_DISCOVERY = RawOption("wpad", 252) |
| |
| # Unlike every other option, which are tuples like: |
| # <number, length in bytes, data>, the pad and end options are just |
| # single bytes "\x00" and "\xff" (without length or data fields). |
| OPTION_PAD = 0 |
| OPTION_END = 255 |
| |
| DHCP_COMMON_FIELDS = [ |
| FIELD_OP, |
| FIELD_HWTYPE, |
| FIELD_HWADDR_LEN, |
| FIELD_RELAY_HOPS, |
| FIELD_TRANSACTION_ID, |
| FIELD_TIME_SINCE_START, |
| FIELD_FLAGS, |
| FIELD_CLIENT_IP, |
| FIELD_YOUR_IP, |
| FIELD_SERVER_IP, |
| FIELD_GATEWAY_IP, |
| FIELD_CLIENT_HWADDR, |
| ] |
| |
| DHCP_REQUIRED_FIELDS = DHCP_COMMON_FIELDS + [ |
| FIELD_MAGIC_COOKIE, |
| ] |
| |
| DHCP_ALL_FIELDS = DHCP_COMMON_FIELDS + [ |
| FIELD_LEGACY_SERVER_NAME, |
| FIELD_LEGACY_BOOT_FILE, |
| FIELD_MAGIC_COOKIE, |
| ] |
| |
| # The op field in an ipv4 packet is either 1 or 2 depending on |
| # whether the packet is from a server or from a client. |
| FIELD_VALUE_OP_CLIENT_REQUEST = 1 |
| FIELD_VALUE_OP_SERVER_RESPONSE = 2 |
| # 1 == 10mb ethernet hardware address type (aka MAC). |
| FIELD_VALUE_HWTYPE_10MB_ETH = 1 |
| # MAC addresses are still 6 bytes long. |
| FIELD_VALUE_HWADDR_LEN_10MB_ETH = 6 |
| FIELD_VALUE_MAGIC_COOKIE = 0x63825363 |
| |
| OPTIONS_START_OFFSET = 240 |
| # From RFC2132, the valid DHCP message types are: |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_DISCOVERY = 1 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_OFFER = 2 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_REQUEST = 3 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_DECLINE = 4 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_ACK = 5 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_NAK = 6 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_RELEASE = 7 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_INFORM = 8 |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_UNKNOWN = -1 |
| |
| OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = [ |
| OPTION_REQUESTED_IP.number, |
| OPTION_IP_LEASE_TIME.number, |
| OPTION_SERVER_ID.number, |
| OPTION_SUBNET_MASK.number, |
| OPTION_ROUTERS.number, |
| OPTION_DNS_SERVERS.number, |
| OPTION_HOST_NAME.number, |
| ] |
| |
| # These are possible options that may not be in every packet. |
| # Frequently, the client can include a bunch of options that indicate |
| # that it would like to receive information about time servers, routers, |
| # lpr servers, and much more, but the DHCP server can usually ignore |
| # those requests. |
| # |
| # Eventually, each option is encoded as: |
| # <option.number, option.size, [array of option.size bytes]> |
| # Unlike fields, which make up a fixed packet format, options can be in |
| # any order, except where they cannot. For instance, option 1 must |
| # follow option 3 if both are supplied. For this reason, potential |
| # options are in this list, and added to the packet in this order every |
| # time. |
| # |
| # size < 0 indicates that this is variable length field of at least |
| # abs(length) bytes in size. |
| DHCP_PACKET_OPTIONS = [ |
| OPTION_TIME_OFFSET, |
| OPTION_ROUTERS, |
| OPTION_SUBNET_MASK, |
| OPTION_TIME_SERVERS, |
| OPTION_NAME_SERVERS, |
| OPTION_DNS_SERVERS, |
| OPTION_LOG_SERVERS, |
| OPTION_COOKIE_SERVERS, |
| OPTION_LPR_SERVERS, |
| OPTION_IMPRESS_SERVERS, |
| OPTION_RESOURCE_LOC_SERVERS, |
| OPTION_HOST_NAME, |
| OPTION_BOOT_FILE_SIZE, |
| OPTION_MERIT_DUMP_FILE, |
| OPTION_SWAP_SERVER, |
| OPTION_DOMAIN_NAME, |
| OPTION_ROOT_PATH, |
| OPTION_EXTENSIONS, |
| OPTION_VENDOR_ENCAPSULATED_OPTIONS, |
| OPTION_REQUESTED_IP, |
| OPTION_IP_LEASE_TIME, |
| OPTION_OPTION_OVERLOAD, |
| OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_SERVER_ID, |
| OPTION_PARAMETER_REQUEST_LIST, |
| OPTION_MESSAGE, |
| OPTION_MAX_DHCP_MESSAGE_SIZE, |
| OPTION_RENEWAL_T1_TIME_VALUE, |
| OPTION_REBINDING_T2_TIME_VALUE, |
| OPTION_VENDOR_ID, |
| OPTION_CLIENT_ID, |
| OPTION_TFTP_SERVER_NAME, |
| OPTION_BOOTFILE_NAME, |
| OPTION_DNS_DOMAIN_SEARCH_LIST, |
| OPTION_CLASSLESS_STATIC_ROUTES, |
| OPTION_WEB_PROXY_AUTO_DISCOVERY, |
| ] |
| |
| def get_dhcp_option_by_number(number): |
| for option in DHCP_PACKET_OPTIONS: |
| if option.number == number: |
| return option |
| return None |
| |
| class DhcpPacket(object): |
| @staticmethod |
| def create_discovery_packet(hwmac_addr): |
| """ |
| Create a discovery packet. |
| |
| Fill in fields of a DHCP packet as if it were being sent from |
| |hwmac_addr|. Requests subnet masks, broadcast addresses, router |
| addresses, dns addresses, domain search lists, client host name, and NTP |
| server addresses. Note that the offer packet received in response to |
| this packet will probably not contain all of that information. |
| """ |
| # MAC addresses are actually only 6 bytes long, however, for whatever |
| # reason, DHCP allocated 12 bytes to this field. Ease the burden on |
| # developers and hide this detail. |
| while len(hwmac_addr) < 12: |
| hwmac_addr += chr(OPTION_PAD) |
| |
| packet = DhcpPacket() |
| packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST) |
| packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| packet.set_field(FIELD_RELAY_HOPS, 0) |
| packet.set_field(FIELD_TRANSACTION_ID, random.getrandbits(32)) |
| packet.set_field(FIELD_TIME_SINCE_START, 0) |
| packet.set_field(FIELD_FLAGS, 0) |
| packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_DISCOVERY) |
| return packet |
| |
| @staticmethod |
| def create_offer_packet(transaction_id, |
| hwmac_addr, |
| offer_ip, |
| server_ip): |
| """ |
| Create an offer packet, given some fields that tie the packet to a |
| particular offer. |
| """ |
| packet = DhcpPacket() |
| packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE) |
| packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| # This has something to do with relay agents |
| packet.set_field(FIELD_RELAY_HOPS, 0) |
| packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| packet.set_field(FIELD_TIME_SINCE_START, 0) |
| packet.set_field(FIELD_FLAGS, 0) |
| packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_YOUR_IP, offer_ip) |
| packet.set_field(FIELD_SERVER_IP, server_ip) |
| packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_OFFER) |
| return packet |
| |
| @staticmethod |
| def create_request_packet(transaction_id, |
| hwmac_addr): |
| packet = DhcpPacket() |
| packet.set_field(FIELD_OP, FIELD_VALUE_OP_CLIENT_REQUEST) |
| packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| # This has something to do with relay agents |
| packet.set_field(FIELD_RELAY_HOPS, 0) |
| packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| packet.set_field(FIELD_TIME_SINCE_START, 0) |
| packet.set_field(FIELD_FLAGS, 0) |
| packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_YOUR_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_SERVER_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_REQUEST) |
| return packet |
| |
| @staticmethod |
| def create_acknowledgement_packet(transaction_id, |
| hwmac_addr, |
| granted_ip, |
| server_ip): |
| packet = DhcpPacket() |
| packet.set_field(FIELD_OP, FIELD_VALUE_OP_SERVER_RESPONSE) |
| packet.set_field(FIELD_HWTYPE, FIELD_VALUE_HWTYPE_10MB_ETH) |
| packet.set_field(FIELD_HWADDR_LEN, FIELD_VALUE_HWADDR_LEN_10MB_ETH) |
| # This has something to do with relay agents |
| packet.set_field(FIELD_RELAY_HOPS, 0) |
| packet.set_field(FIELD_TRANSACTION_ID, transaction_id) |
| packet.set_field(FIELD_TIME_SINCE_START, 0) |
| packet.set_field(FIELD_FLAGS, 0) |
| packet.set_field(FIELD_CLIENT_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_YOUR_IP, granted_ip) |
| packet.set_field(FIELD_SERVER_IP, server_ip) |
| packet.set_field(FIELD_GATEWAY_IP, IPV4_NULL_ADDRESS) |
| packet.set_field(FIELD_CLIENT_HWADDR, hwmac_addr) |
| packet.set_field(FIELD_MAGIC_COOKIE, FIELD_VALUE_MAGIC_COOKIE) |
| packet.set_option(OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_ACK) |
| return packet |
| |
| def __init__(self, byte_str=None): |
| """ |
| Create a DhcpPacket, filling in fields from a byte string if given. |
| |
| Assumes that the packet starts at offset 0 in the binary string. This |
| includes the fields and options. Fields are different from options in |
| that we bother to decode these into more usable data types like |
| integers rather than keeping them as raw byte strings. Fields are also |
| required to exist, unlike options which may not. |
| |
| Each option is encoded as a tuple <option number, length, data> where |
| option number is a byte indicating the type of option, length indicates |
| the number of bytes in the data for option, and data is a length array |
| of bytes. The only exceptions to this rule are the 0 and 255 options, |
| which have 0 data length, and no length byte. These tuples are then |
| simply appended to each other. This encoding is the same as the BOOTP |
| vendor extention field encoding. |
| """ |
| super(DhcpPacket, self).__init__() |
| self._options = {} |
| self._fields = {} |
| if byte_str is None: |
| return |
| if len(byte_str) < OPTIONS_START_OFFSET + 1: |
| logging.error("Invalid byte string for packet.") |
| return |
| for field in DHCP_ALL_FIELDS: |
| self._fields[field] = field.unpack(byte_str[field.offset : |
| field.offset + |
| field.size]) |
| offset = OPTIONS_START_OFFSET |
| domain_search_list_byte_string = "" |
| while offset < len(byte_str) and ord(byte_str[offset]) != OPTION_END: |
| data_type = ord(byte_str[offset]) |
| offset += 1 |
| if data_type == OPTION_PAD: |
| continue |
| data_length = ord(byte_str[offset]) |
| offset += 1 |
| data = byte_str[offset: offset + data_length] |
| offset += data_length |
| option = get_dhcp_option_by_number(data_type) |
| if option is None: |
| logging.warning("Unsupported DHCP option found. " |
| "Option number: %d" % data_type) |
| continue |
| if option == OPTION_DNS_DOMAIN_SEARCH_LIST: |
| # In a cruel twist of fate, the server is allowed to give |
| # multiple options with this number. The client is expected to |
| # concatenate the byte strings together and use it as a single |
| # value. |
| domain_search_list_byte_string += data |
| continue |
| option_value = option.unpack(data) |
| if option == OPTION_PARAMETER_REQUEST_LIST: |
| logging.info("Requested options: %s" % str(option_value)) |
| self._options[option] = option_value |
| if domain_search_list_byte_string: |
| self._options[OPTION_DNS_DOMAIN_SEARCH_LIST] = option_value |
| |
| |
| @property |
| def client_hw_address(self): |
| return self._fields.get(FIELD_CLIENT_HWADDR) |
| |
| @property |
| def is_valid(self): |
| """ |
| Checks that we have (at a minimum) values for all the required fields, |
| and that the magic cookie is set correctly. |
| """ |
| for field in DHCP_REQUIRED_FIELDS: |
| if self._fields.get(field) is None: |
| logging.warning("Missing field %s in packet." % field) |
| return False |
| if self._fields[FIELD_MAGIC_COOKIE] != FIELD_VALUE_MAGIC_COOKIE: |
| return False |
| return True |
| |
| @property |
| def message_type(self): |
| return self._options.get(OPTION_DHCP_MESSAGE_TYPE, |
| OPTION_VALUE_DHCP_MESSAGE_TYPE_UNKNOWN) |
| |
| @property |
| def transaction_id(self): |
| return self._fields.get(FIELD_TRANSACTION_ID) |
| |
| def get_field(self, field): |
| return self._fields.get(field) |
| |
| def get_option(self, option): |
| return self._options.get(option) |
| |
| def set_field(self, field, field_value): |
| self._fields[field] = field_value |
| |
| def set_option(self, option, option_value): |
| self._options[option] = option_value |
| |
| def to_binary_string(self): |
| if not self.is_valid: |
| return None |
| # A list of byte strings to be joined into a single string at the end. |
| data = [] |
| offset = 0 |
| for field in DHCP_ALL_FIELDS: |
| if field not in self._fields: |
| continue |
| field_data = field.pack(self._fields[field]) |
| while offset < field.offset: |
| # This should only happen when we're padding the fields because |
| # we're not filling in legacy BOOTP stuff. |
| data.append("\x00") |
| offset += 1 |
| data.append(field_data) |
| offset += field.size |
| # Last field processed is the magic cookie, so we're ready for options. |
| # Have to process options |
| for option in DHCP_PACKET_OPTIONS: |
| option_value = self._options.get(option) |
| if option_value is None: |
| continue |
| serialized_value = option.pack(option_value) |
| data.append(struct.pack("BB", |
| option.number, |
| len(serialized_value))) |
| offset += 2 |
| data.append(serialized_value) |
| offset += len(serialized_value) |
| data.append(chr(OPTION_END)) |
| offset += 1 |
| while offset < DHCP_MIN_PACKET_SIZE: |
| data.append(chr(OPTION_PAD)) |
| offset += 1 |
| return "".join(data) |
| |
| def __str__(self): |
| options = [k.name + "=" + str(v) for k, v in self._options.items()] |
| fields = [k.name + "=" + str(v) for k, v in self._fields.items()] |
| return "<DhcpPacket fields=%s, options=%s>" % (fields, options) |