/*
* DHCPv4 Incoming Messages
*
* This file implements the message parser object for incoming DHCP4 messages.
* It takes a linear data blob as input, and provides accessors for the message
* content.
*
* This wrapper mainly deals with the OPTIONs array. That is, in hides the
* different overload-sections the DHCP4 spec defines, it concatenates
* duplicate option fields (as described by the spec), and provides a
* consistent view to the caller.
*
* Internally, for every incoming message we linearize its OPTIONs. This means,
* we create a copy of the contents, and merge all duplicate options into a
* single option entry. We then provide accessors to the caller to easily get
* O(1) access to individual fields.
*/
#include <assert.h>
#include <c-stdaux.h>
#include <endian.h>
#include <errno.h>
#include <inttypes.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "n-dhcp4.h"
#include "n-dhcp4-private.h"
static void n_dhcp4_incoming_prefetch(NDhcp4Incoming *incoming, size_t *offset, uint8_t option, const uint8_t *raw, size_t n_raw) {
uint8_t o, l;
size_t pos;
for (pos = 0; pos < n_raw; ) {
o = raw[pos++];
if (o == N_DHCP4_OPTION_PAD)
continue;
if (o == N_DHCP4_OPTION_END)
return;
/* bail out if no remaining space for length field */
if (pos >= n_raw)
return;
/* bail out if length exceeds the available space */
l = raw[pos++];
if (l > n_raw || pos > n_raw - l)
return;
/* prefetch content if it matches @option */
if (o == option) {
memcpy((uint8_t *)&incoming->message + *offset, raw + pos, l);
*offset += l;
}
pos += l;
}
}
static void n_dhcp4_incoming_merge(NDhcp4Incoming *incoming, size_t *offset, uint8_t overload, uint8_t option) {
uint8_t *m = (uint8_t *)&incoming->message;
size_t pos;
/*
* Prefetch all options matching @option from the 3 sections,
* concatenating their content. Remember the offset and size of the
* option in our message state.
*/
pos = *offset;
/* prefetch option from OPTIONS */
n_dhcp4_incoming_prefetch(incoming, offset, option,
m + offsetof(NDhcp4Message, options),
incoming->n_message - offsetof(NDhcp4Message, options));
/* prefetch option from FILE */
if (overload & N_DHCP4_OVERLOAD_FILE)
n_dhcp4_incoming_prefetch(incoming, offset, option,
m + offsetof(NDhcp4Message, file),
sizeof(incoming->message.file));
/* prefetch option from SNAME */
if (overload & N_DHCP4_OVERLOAD_SNAME)
n_dhcp4_incoming_prefetch(incoming, offset, option,
m + offsetof(NDhcp4Message, sname),
sizeof(incoming->message.sname));
incoming->options[option].value = m + pos;
incoming->options[option].size = *offset - pos;
}
static void n_dhcp4_incoming_linearize(NDhcp4Incoming *incoming) {
uint8_t *m, o, l, overload;
size_t i, pos, end, offset;
/*
* Linearize all OPTIONs of the incoming message. We know that
* @incoming->message is preallocated to be big enough to hold the
* entire linearized message _trailing_ the original copy. All we have
* to do is walk the raw message in @incoming->message and for each
* option we find, copy it into the trailing space, concatenating all
* instances we find.
*
* Before we can copy the individual options, we must scan for the
* OVERLOAD option. This is required so our prefetcher knows which data
* arrays to scan for prefetching.
*
* So far, we require the OVERLOAD option to be present in the
* options-array (which is obvious and a given). However, if the option
* occurs multiple times outside of the options-array (i.e., SNAME or
* FILE), we silently ignore them. The specification does not allow
* multiple OVERLOAD options, anyway. Hence, this behavior only defines
* what we do when we see broken implementations, and we currently seem
* to support all styles we saw in the wild so far.
*/
m = (uint8_t *)&incoming->message;
offset = incoming->n_message;
n_dhcp4_incoming_merge(incoming, &offset, 0, N_DHCP4_OPTION_OVERLOAD);
if (incoming->options[N_DHCP4_OPTION_OVERLOAD].size >= 1)
overload = *incoming->options[N_DHCP4_OPTION_OVERLOAD].value;
else
overload = 0;
for (i = 0; i < 3; ++i) {
if (i == 0) { /* walk OPTIONS */
pos = offsetof(NDhcp4Message, options);
end = incoming->n_message;
} else if (i == 1) { /* walk FILE */
if (!(overload & N_DHCP4_OVERLOAD_FILE))
continue;
pos = offsetof(NDhcp4Message, file);
end = pos + sizeof(incoming->message.file);
} else { /* walk SNAME */
if (!(overload & N_DHCP4_OVERLOAD_SNAME))
continue;
pos = offsetof(NDhcp4Message, sname);
end = pos + sizeof(incoming->message.sname);
}
while (pos < end) {
o = m[pos++];
if (o == N_DHCP4_OPTION_PAD)
continue;
if (o == N_DHCP4_OPTION_END)
break;
if (pos >= end)
break;
l = m[pos++];
if (l > end || pos > end - l)
break;
if (!incoming->options[o].value)
n_dhcp4_incoming_merge(incoming, &offset, overload, o);
pos += l;
}
}
}
/**
* n_dhcp4_incoming_new() - Allocate new incoming message object
* @incomingp: output argument for new object
* @raw: raw message blob
* @n_raw: length of the raw message blob
*
* This function allocates a new incoming-message object to wrap a received
* message blob. It performs basic verification of the message length and
* header, and then linearizes the DHCP4 options.
*
* The incoming-message object mainly provides accessors for the option-array.
* It handles all the different quirks around parsing and concatenating the
* options array, and provides the assembled data to the caller. It does not,
* however, in any way interpret the data of the individual options. This is up
* to the caller to do.
*
* Return: 0 on success, negative error code on failure, N_DHCP4_E_MALFORMED if
* the message is not a valid DHCP4 message.
*/
int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw) {
_c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
size_t size;
if (n_raw < sizeof(NDhcp4Message) || n_raw > UINT16_MAX)
return N_DHCP4_E_MALFORMED;
/*
* Allocate enough space for book-keeping, a copy of @raw and trailing
* space for linearized options. The trailing space must be big enough
* to hold the entire options array unmodified (linearizing can only
* make it smaller). Hence, just allocate enough space to hold the raw
* message without the header.
*/
size = sizeof(*incoming) + n_raw - sizeof(NDhcp4Message);
size += n_raw - sizeof(NDhcp4Header);
incoming = calloc(1, size);
if (!incoming)
return -ENOMEM;
*incoming = (NDhcp4Incoming)N_DHCP4_INCOMING_NULL(*incoming);
incoming->n_message = n_raw;
memcpy(&incoming->message, raw, n_raw);
if (incoming->message.magic != htobe32(N_DHCP4_MESSAGE_MAGIC))
return N_DHCP4_E_MALFORMED;
/* linearize options */
n_dhcp4_incoming_linearize(incoming);
*incomingp = incoming;
incoming = NULL;
return 0;
}
/**
* n_dhcp4_incoming_free() - Deallocate message object
* @incoming: object to operate on, or NULL
*
* This deallocates and frees the given incoming-message object. If NULL is
* passed, this is a no-op.
*
* Return: NULL is returned.
*/
NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming) {
if (!incoming)
return NULL;
free(incoming);
return NULL;
}
/**
* n_dhcp4_incoming_get_header() - Return message header
* @incoming: message to operate on
*
* This returns a pointer to the message header. Note that modifications to
* this header are permanent and will affect the original message.
*
* Return: A pointer to the message header is returned.
*/
NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming) {
return &incoming->message.header;
}
/**
* n_dhcp4_incoming_get_raw() - Get access to the raw original message
* @incoming: message to operate on
* @rawp: output argument for the raw blob, or NULL
*
* This hands out a pointer to the raw message blob to the caller. This will
* point to the original message content, rather than the linearized version.
*
* Note that if the caller queried the contents of the message before, any
* modifications done by the caller will not affect the original message. It
* only affects the linearized content (which is a duplicate trailing the
* original message). However, modifications to the message header *DO* also
* appear in the original, since the message header is not duplicated.
*
* In either case, it is better to never modify the message, if you intend to
* forward it further.
*
* Return: The length of the raw message blob is returned.
*/
size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp) {
if (rawp)
*rawp = &incoming->message;
return incoming->n_message;
}
/**
* n_dhcp4_incoming_query() - Query the contents of a specific option
* @incoming: message to query
* @option: option to look for
* @datap: output argument for the option-data, or NULL
* @n_datap: output argument for the length of the option, or NULL
*
* This returns a pointer to the requested option blob in the message. It
* points to a linearized version of all respective option-fields of the same
* type. Hence, the caller is not required to deal with multiple occurrences of
* the same option.
*
* If an option was not present in the incoming message, N_DHCP4_E_UNSET is
* returned. Note that this is different from an empty option! And empty option
* will return a valid pointer and size 0.
*
* Note that the pointer to the option-blob does not point to the original
* message, but a duplicated version. Modifications to the blob will not be
* reflected in the original message, but they will be permanent regarding
* further queries through this function.
*
* Note that the original message alignment might no longer be reflected in the
* returned blob. You must not alias the content of the blob, but always copy
* it out, or consume piecemeal.
*
* This function runs in O(1).
*
* Return: 0 on success, negative error code on failure, N_DHCP4_E_UNSET if the
* option was not found,
*/
int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap) {
if (!incoming->options[option].value)
return N_DHCP4_E_UNSET;
if (datap)
*datap = incoming->options[option].value;
if (n_datap)
*n_datap = incoming->options[option].size;
return 0;
}
static int n_dhcp4_incoming_query_u8(NDhcp4Incoming *message, uint8_t option, uint8_t *u8p) {
uint8_t *data;
size_t n_data;
int r;
r = n_dhcp4_incoming_query(message, option, &data, &n_data);
if (r)
return r;
else if (n_data < sizeof(*data))
return N_DHCP4_E_MALFORMED;
*u8p = *data;
return 0;
}
static int n_dhcp4_incoming_query_u16(NDhcp4Incoming *message, uint8_t option, uint16_t *u16p) {
uint8_t *data;
size_t n_data;
uint16_t be16;
int r;
r = n_dhcp4_incoming_query(message, option, &data, &n_data);
if (r)
return r;
else if (n_data < sizeof(be16))
return N_DHCP4_E_MALFORMED;
memcpy(&be16, data, sizeof(be16));
*u16p = ntohs(be16);
return 0;
}
static int n_dhcp4_incoming_query_u32(NDhcp4Incoming *message, uint8_t option, uint32_t *u32p) {
uint8_t *data;
size_t n_data;
uint32_t be32;
int r;
r = n_dhcp4_incoming_query(message, option, &data, &n_data);
if (r)
return r;
else if (n_data < sizeof(be32))
return N_DHCP4_E_MALFORMED;
memcpy(&be32, data, sizeof(be32));
*u32p = ntohl(be32);
return 0;
}
static int n_dhcp4_incoming_query_in_addr(NDhcp4Incoming *message, uint8_t option, struct in_addr *addrp) {
uint8_t *data;
size_t n_data;
uint32_t be32;
int r;
r = n_dhcp4_incoming_query(message, option, &data, &n_data);
if (r)
return r;
else if (n_data < sizeof(be32))
return N_DHCP4_E_MALFORMED;
memcpy(&be32, data, sizeof(be32));
addrp->s_addr = be32;
return 0;
}
int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep) {
return n_dhcp4_incoming_query_u8(message, N_DHCP4_OPTION_MESSAGE_TYPE, typep);
}
int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep) {
return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetimep);
}
int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p) {
return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2p);
}
int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p) {
return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1p);
}
int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp) {
return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, idp);
}
int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep) {
return n_dhcp4_incoming_query_u16(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, max_message_sizep);
}
int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp) {
return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, requested_ipp);
}
void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp) {
NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
*xidp = header->xid;
}
void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr) {
NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
yiaddr->s_addr = header->yiaddr;
}