blob: 87f54d3178a78d648b0f1155b5e23e1d971766d5 [file] [log] [blame]
# 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,
offer_mask,
server_ip,
lease_time_seconds)
socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# I believe that 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 logging
import random
import socket
import struct
class Option(object):
"""
Represents an option in a DHCP packet. Options may or may not be present
and are not parsed into any particular format. This means that the value of
options is always in the form of a byte string.
"""
def __init__(self, name, number, size):
super(Option, self).__init__()
self._name = name
self._number = number
self._size = size
@property
def name(self):
return self._name
@property
def number(self):
"""
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.
"""
return self._number
@property
def size(self):
"""
The size property is a hint for what kind of size we might expect the
option to be. For instance, options with a size of 1 are expected to
always be 1 byte long. Negative sizes are variable length fields that
are expected to be at least abs(size) bytes long.
However, the size property is just a hint, and is not enforced or
checked in any way.
"""
return self._size
class Field(object):
"""
Represents a required field in a DHCP packet. Unlike options, we sometimes
parse fields into more meaningful data types. For instance, the hardware
type field in an IPv4 packet is parsed into an int rather than being left as
a raw byte string of length 1.
"""
def __init__(self, name, wire_format, offset, size):
super(Field, self).__init__()
self._name = name
self._wire_format = wire_format
self._offset = offset
self._size = size
@property
def name(self):
return self._name
@property
def wire_format(self):
"""
The wire format for a field defines how it will be parsed out of a DHCP
packet.
"""
return self._wire_format
@property
def offset(self):
"""
The |offset| for a field defines the starting byte of the field in the
binary packet string. |offset| is using during parsing, along with
|size| to extract the byte string of a field.
"""
return self._offset
@property
def size(self):
"""
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.
"""
return self._size
# 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 = "\x00\x00\x00\x00"
# These are required in every DHCP packet. Without these fields, the
# packet will not even pass DhcpPacket.is_valid
FIELD_OP = Field("op", "!B", 0, 1)
FIELD_HWTYPE = Field("htype", "!B", 1, 1)
FIELD_HWADDR_LEN = Field("hlen", "!B", 2, 1)
FIELD_RELAY_HOPS = Field("hops", "!B", 3, 1)
FIELD_TRANSACTION_ID = Field("xid", "!I", 4, 4)
FIELD_TIME_SINCE_START = Field("secs", "!H", 8, 2)
FIELD_FLAGS = Field("flags", "!H", 10, 2)
FIELD_CLIENT_IP = Field("ciaddr", "!4s", 12, 4)
FIELD_YOUR_IP = Field("yiaddr", "!4s", 16, 4)
FIELD_SERVER_IP = Field("siaddr", "!4s", 20, 4)
FIELD_GATEWAY_IP = Field("giaddr", "!4s", 24, 4)
FIELD_CLIENT_HWADDR = Field("chaddr", "!16s", 28, 16)
# For legacy BOOTP reasons, there are 192 octets of 0's that
# come after the chaddr.
FIELD_MAGIC_COOKIE = Field("magic_cookie", "!I", 236, 4)
OPTION_TIME_OFFSET = Option("time_offset", 2, 4)
OPTION_ROUTERS = Option("routers", 3, -4)
OPTION_SUBNET_MASK = Option("subnet_mask", 1, 4)
# These *_servers (and router) options are actually lists of IPv4
# addressesexpected to be multiples of 4 octets.
OPTION_TIME_SERVERS = Option("time_servers", 4, -4)
OPTION_NAME_SERVERS = Option("name_servers", 5, -4)
OPTION_DNS_SERVERS = Option("dns_servers", 6, -4)
OPTION_LOG_SERVERS = Option("log_servers", 7, -4)
OPTION_COOKIE_SERVERS = Option("cookie_servers", 8, -4)
OPTION_LPR_SERVERS = Option("lpr_servers", 9, -4)
OPTION_IMPRESS_SERVERS = Option("impress_servers", 10, -4)
OPTION_RESOURCE_LOC_SERVERS = Option("resource_loc_servers", 11, -4)
OPTION_HOST_NAME = Option("host_name", 12, -1)
OPTION_BOOT_FILE_SIZE = Option("boot_file_size", 13, 2)
OPTION_MERIT_DUMP_FILE = Option("merit_dump_file", 14, -1)
OPTION_SWAP_SERVER = Option("domain_name", 15, -1)
OPTION_DOMAIN_NAME = Option("swap_server", 16, 4)
OPTION_ROOT_PATH = Option("root_path", 17, -1)
OPTION_EXTENSIONS = Option("extensions", 18, -1)
# DHCP options.
OPTION_REQUESTED_IP = Option("requested_ip", 50, 4)
OPTION_IP_LEASE_TIME = Option("ip_lease_time", 51, 4)
OPTION_OPTION_OVERLOAD = Option("option_overload", 52, 1)
OPTION_DHCP_MESSAGE_TYPE = Option("dhcp_message_type", 53, 1)
OPTION_SERVER_ID = Option("server_id", 54, 4)
OPTION_PARAMETER_REQUEST_LIST = Option("parameter_request_list", 55, -1)
OPTION_MESSAGE = Option("message", 56, -1)
OPTION_MAX_DHCP_MESSAGE_SIZE = Option("max_dhcp_message_size", 57, 2)
OPTION_RENEWAL_T1_TIME_VALUE = Option("renewal_t1_time_value", 58, 4)
OPTION_REBINDING_T2_TIME_VALUE = Option("rebinding_t2_time_value", 59, 4)
OPTION_VENDOR_ID = Option("vendor_id", 60, -1)
OPTION_CLIENT_ID = Option("client_id", 61, -2)
OPTION_TFTP_SERVER_NAME = Option("tftp_server_name", 66, -1)
OPTION_BOOTFILE_NAME = Option("bootfile_name", 67, -1)
# 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
# All fields are required.
DHCP_PACKET_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,
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 = "\x01"
OPTION_VALUE_DHCP_MESSAGE_TYPE_OFFER = "\x02"
OPTION_VALUE_DHCP_MESSAGE_TYPE_REQUEST = "\x03"
OPTION_VALUE_DHCP_MESSAGE_TYPE_DECLINE = "\x04"
OPTION_VALUE_DHCP_MESSAGE_TYPE_ACK = "\x05"
OPTION_VALUE_DHCP_MESSAGE_TYPE_NAK = "\x06"
OPTION_VALUE_DHCP_MESSAGE_TYPE_RELEASE = "\x07"
OPTION_VALUE_DHCP_MESSAGE_TYPE_INFORM = "\x08"
OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT = \
chr(OPTION_SUBNET_MASK.number) + \
chr(OPTION_ROUTERS.number) + \
chr(OPTION_DNS_SERVERS.number) + \
chr(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,
# These *_servers (and router) options are actually lists of
# IPv4 addresses expected to be multiples of 4 octets.
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,
# DHCP 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,
]
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.name, FIELD_VALUE_OP_CLIENT_REQUEST)
packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
packet.set_field(FIELD_RELAY_HOPS.name, 0)
packet.set_field(FIELD_TRANSACTION_ID.name, random.getrandbits(32))
packet.set_field(FIELD_TIME_SINCE_START.name, 0)
packet.set_field(FIELD_FLAGS.name, 0)
packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_YOUR_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_SERVER_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
OPTION_VALUE_DHCP_MESSAGE_TYPE_DISCOVERY)
packet.set_option(OPTION_PARAMETER_REQUEST_LIST.name,
OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT)
return packet
@staticmethod
def create_offer_packet(transaction_id,
hwmac_addr,
offer_ip,
offer_subnet_mask,
server_ip,
lease_time_seconds):
"""
Create an offer packet, given some fields that tie the packet to a
particular offer.
"""
packet = DhcpPacket()
packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_SERVER_RESPONSE)
packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
# This has something to do with relay agents
packet.set_field(FIELD_RELAY_HOPS.name, 0)
packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
packet.set_field(FIELD_TIME_SINCE_START.name, 0)
packet.set_field(FIELD_FLAGS.name, 0)
packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_YOUR_IP.name, socket.inet_aton(offer_ip))
packet.set_field(FIELD_SERVER_IP.name, socket.inet_aton(server_ip))
packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
OPTION_VALUE_DHCP_MESSAGE_TYPE_OFFER)
packet.set_option(OPTION_SUBNET_MASK.name,
socket.inet_aton(offer_subnet_mask))
packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
packet.set_option(OPTION_IP_LEASE_TIME.name,
struct.pack("!I", int(lease_time_seconds)))
return packet
@staticmethod
def create_request_packet(transaction_id,
hwmac_addr,
requested_ip,
server_ip):
packet = DhcpPacket()
packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_CLIENT_REQUEST)
packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
# This has something to do with relay agents
packet.set_field(FIELD_RELAY_HOPS.name, 0)
packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
packet.set_field(FIELD_TIME_SINCE_START.name, 0)
packet.set_field(FIELD_FLAGS.name, 0)
packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_YOUR_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_SERVER_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
packet.set_option(OPTION_REQUESTED_IP.name,
socket.inet_aton(requested_ip))
packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
OPTION_VALUE_DHCP_MESSAGE_TYPE_REQUEST)
packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
packet.set_option(OPTION_PARAMETER_REQUEST_LIST.name,
OPTION_VALUE_PARAMETER_REQUEST_LIST_DEFAULT)
return packet
@staticmethod
def create_acknowledgement_packet(transaction_id,
hwmac_addr,
granted_ip,
granted_ip_subnet_mask,
server_ip,
lease_time_seconds):
packet = DhcpPacket()
packet.set_field(FIELD_OP.name, FIELD_VALUE_OP_SERVER_RESPONSE)
packet.set_field(FIELD_HWTYPE.name, FIELD_VALUE_HWTYPE_10MB_ETH)
packet.set_field(FIELD_HWADDR_LEN.name, FIELD_VALUE_HWADDR_LEN_10MB_ETH)
# This has something to do with relay agents
packet.set_field(FIELD_RELAY_HOPS.name, 0)
packet.set_field(FIELD_TRANSACTION_ID.name, transaction_id)
packet.set_field(FIELD_TIME_SINCE_START.name, 0)
packet.set_field(FIELD_FLAGS.name, 0)
packet.set_field(FIELD_CLIENT_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_YOUR_IP.name, socket.inet_aton(granted_ip))
packet.set_field(FIELD_SERVER_IP.name, socket.inet_aton(server_ip))
packet.set_field(FIELD_GATEWAY_IP.name, IPV4_NULL_ADDRESS)
packet.set_field(FIELD_CLIENT_HWADDR.name, hwmac_addr)
packet.set_field(FIELD_MAGIC_COOKIE.name, FIELD_VALUE_MAGIC_COOKIE)
packet.set_option(OPTION_DHCP_MESSAGE_TYPE.name,
OPTION_VALUE_DHCP_MESSAGE_TYPE_ACK)
packet.set_option(OPTION_SUBNET_MASK.name,
socket.inet_aton(granted_ip_subnet_mask))
packet.set_option(OPTION_SERVER_ID.name, socket.inet_aton(server_ip))
packet.set_option(OPTION_IP_LEASE_TIME.name,
struct.pack("!I", int(lease_time_seconds)))
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 = {}
self._logger = logging.getLogger("dhcp.packet")
if byte_str is None:
return
if len(byte_str) < OPTIONS_START_OFFSET + 1:
self._logger.error("Invalid byte string for packet.")
return
for field in DHCP_PACKET_FIELDS:
self._fields[field.name] = struct.unpack(field.wire_format,
byte_str[field.offset :
field.offset +
field.size])[0]
offset = OPTIONS_START_OFFSET
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_bunch = get_dhcp_option_by_number(data_type)
if option_bunch is None:
# Unsupported data type, of which we have many.
continue
self._options[option_bunch.name] = data
@property
def client_hw_address(self):
return self._fields["chaddr"]
@property
def is_valid(self):
for field in DHCP_PACKET_FIELDS:
if (not field.name in self._fields or
self._fields[field.name] is None):
self._logger.info("Missing field %s in packet." % field.name)
return False
if (self._fields[FIELD_MAGIC_COOKIE.name] !=
FIELD_VALUE_MAGIC_COOKIE):
return False
return True
@property
def message_type(self):
if not "dhcp_message_type" in self._options:
return -1
return self._options["dhcp_message_type"]
@property
def transaction_id(self):
return self._fields["xid"]
def get_field(self, field_name):
if field_name in self._fields:
return self._fields[field_name]
return None
def get_option(self, option_name):
if option_name in self._options:
return self._options[option_name]
return None
def set_field(self, field_name, field_value):
self._fields[field_name] = field_value
def set_option(self, option_name, option_value):
self._options[option_name] = 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_PACKET_FIELDS:
field_data = struct.pack(field.wire_format,
self._fields[field.name])
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:
if not option.name in self._options:
continue
data.append(struct.pack("BB",
option.number,
len(self._options[option.name])))
offset += 2
data.append(self._options[option.name])
offset += len(self._options[option.name])
data.append(chr(OPTION_END))
offset += 1
while offset < DHCP_MIN_PACKET_SIZE:
data.append(chr(OPTION_PAD))
offset += 1
return "".join(data)