Files

742 lines
30 KiB
C

/*
* the dhcpv4 message layer.
*
* this is the part both the server and the client stand on, so it is tested on
* its own first: it is pure - a buffer in, a buffer out, no sockets and no
* timing - and every one of its failure modes is reachable from a unit test.
*
* the cases that matter most are the ones a hand-written parser gets wrong:
* an option payload carries no alignment, a length field that disagrees with
* the option's definition is a malformed packet rather than a value to
* salvage, and a hardware address length is an index into a fixed field that a
* hostile packet can overrun.
*/
#include <hio-dhcp.h>
#include <hio-prv.h>
#include "tap.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFCAPA 512
static hio_uint8_t g_buf[BUFCAPA];
/* the walker takes no context argument, so a count has to live out here */
static int g_walked;
static int g_walk_codes[32];
static int count_opt (hio_dhcp4_opt_hdr_t* opt)
{
if (g_walked < (int)HIO_COUNTOF(g_walk_codes)) g_walk_codes[g_walked] = opt->code;
g_walked++;
return 1; /* non-zero continues; 0 would stop the walk after the first */
}
/* a request as a client would send it, for the reply and client-id cases */
static void make_request (hio_dhcp4_pktbuf_t* pkt, hio_uint8_t mtype)
{
static const hio_uint8_t mac[6] = { 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef };
hio_dhcp4_init_pktbuf (pkt, g_buf, BUFCAPA);
pkt->hdr->op = HIO_DHCP4_OP_BOOTREQUEST;
pkt->hdr->htype = HIO_DHCP4_HTYPE_ETHERNET;
pkt->hdr->hlen = 6;
HIO_MEMCPY (pkt->hdr->chaddr, mac, 6);
pkt->hdr->xid = hio_hton32(0x11223344);
pkt->hdr->flags = hio_hton16(0x8000); /* broadcast */
pkt->hdr->giaddr = hio_hton32(0x0a000001);
hio_dhcp4_add_option_uint8 (pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, mtype);
}
static void as_pktinf (hio_dhcp4_pktinf_t* inf, const hio_dhcp4_pktbuf_t* pkt)
{
inf->hdr = pkt->hdr;
inf->len = pkt->len;
}
/* ------------------------------------------------------------------ */
static void test_cookie_and_layout (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_uint32_t cookie;
OK (hio_dhcp4_init_pktbuf(&pkt, g_buf, BUFCAPA) == 0,
"a packet buffer initialises to just the fixed header");
OK (pkt.len == HIO_SIZEOF(hio_dhcp4_pkt_hdr_t),
"and its length is the header alone until an option is added");
OK (hio_dhcp4_add_option_uint8(&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER) == 0,
"an option can be added");
/* the cookie has to be there, and in network order, or nothing downstream
* will recognise the packet as dhcp at all */
HIO_MEMCPY (&cookie, &g_buf[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t)], 4);
OK (cookie == HIO_CONST_HTON32(HIO_DHCP4_MAGIC_COOKIE),
"and adding the first one writes the magic cookie ahead of it");
/* header + cookie + (code,len,value) */
OK (pkt.len == HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 4 + 3,
"and the length accounts for the cookie, the option header and its payload");
}
static void test_byte_order (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
const hio_uint8_t* p;
hio_uint8_t l;
hio_uint16_t v16;
hio_uint32_t v32;
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
OK (hio_dhcp4_add_option_uint16(&pkt, HIO_DHCP4_OPT_MAX_SIZE, 0x0102) == 0 &&
hio_dhcp4_add_option_uint32(&pkt, HIO_DHCP4_OPT_LEASE_TIME, 0x01020304) == 0,
"16- and 32-bit options can be added");
as_pktinf (&inf, &pkt);
/* on the wire, most significant octet first - the whole reason these
* helpers exist rather than callers writing the bytes themselves */
OK (hio_dhcp4_get_option_data(&inf, HIO_DHCP4_OPT_MAX_SIZE, &p, &l) == 0 &&
l == 2 && p[0] == 0x01 && p[1] == 0x02,
"and a 16-bit option is written most significant octet first");
OK (hio_dhcp4_get_option_data(&inf, HIO_DHCP4_OPT_LEASE_TIME, &p, &l) == 0 &&
l == 4 && p[0] == 0x01 && p[1] == 0x02 && p[2] == 0x03 && p[3] == 0x04,
"and so is a 32-bit one");
OK (hio_dhcp4_get_option_uint16(&inf, HIO_DHCP4_OPT_MAX_SIZE, &v16) == 0 && v16 == 0x0102,
"and reading it back gives the host-order value again");
OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) == 0 && v32 == 0x01020304,
"for both widths");
}
static void test_unaligned_payload (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint32_t v32;
/* an option payload starts wherever the options before it ended, so its
* alignment is whatever happens to fall out. a one-octet option ahead of
* the 32-bit one guarantees the latter is misaligned - which is the case
* that reading through a cast gets wrong, silently on x86 and fatally
* elsewhere. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64);
hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 0xDEADBEEF);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) == 0 && v32 == 0xDEADBEEF,
"a 32-bit option is read correctly from an unaligned offset");
}
static void test_wrong_width_is_refused (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t two[2] = { 0x12, 0x34 };
hio_uint32_t v32;
hio_uint16_t v16;
/* a lease time is four octets by definition. two is a malformed packet,
* and the answer to that is a refusal rather than whichever two octets
* happen to follow in the buffer. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_LEASE_TIME, two, 2);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) <= -1,
"an option whose length disagrees with its definition is refused");
OK (hio_dhcp4_get_option_uint16(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v16) == 0 && v16 == 0x1234,
"and is readable at the width it actually has");
/* an option that is not there is not an error to be papered over either */
OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_T1, &v32) <= -1,
"and an absent option reports absence rather than a value");
}
static void test_msg_type (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t mtype;
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_REQUEST);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_msg_type(&inf, &mtype) == 0 && mtype == HIO_DHCP4_MSG_REQUEST,
"the message type is read from option 53");
/* no option 53 is what distinguishes bootp from dhcp, so it must not be
* mistaken for a message type of zero */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1,
"a packet without one is refused rather than read as type zero");
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, 0);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1, "and nor is a type of zero accepted");
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, 99);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1, "nor one this implementation does not know");
}
static void test_client_id (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
const hio_uint8_t* p;
hio_uint8_t l;
static const hio_uint8_t cid[5] = { 0xff, 'a', 'b', 'c', 'd' };
/* with no option 61, the hardware address is the identity */
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_client_id(&inf, &p, &l) == 0 && l == 6 &&
p[0] == 0x02 && p[5] == 0xef,
"with no client-id option, the hardware address identifies the client");
/* with one, it wins - and it is opaque, so it is returned as it came */
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_CLIENT_ID, (void*)cid, 5);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_client_id(&inf, &p, &l) == 0 && l == 5 && HIO_MEMCMP(p, cid, 5) == 0,
"and a client-id option takes precedence over it");
/* a packet may claim a hardware address longer than the field that holds
* it. trusting hlen there is how a parser reads past the header. */
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
pkt.hdr->hlen = 200;
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_get_client_id(&inf, &p, &l) <= -1,
"and a hardware address longer than the field is refused rather than trusted");
}
static void test_check_pkt (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_check_pkt(&inf) == 0, "a well-formed packet passes the shape check");
/* short of the fixed header there is nothing to read at all */
as_pktinf (&inf, &pkt);
inf.len = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) - 1;
OK (hio_dhcp4_check_pkt(&inf) <= -1, "one shorter than the fixed header is refused");
/* and with no room for the cookie there are no options */
as_pktinf (&inf, &pkt);
inf.len = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 2;
OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one with no room for the cookie is refused");
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
g_buf[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t)] ^= 0xFF; /* break the cookie */
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one whose cookie is wrong is refused");
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
pkt.hdr->hlen = 200;
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one claiming an oversized hardware address is refused");
}
static void test_reply_echoes_the_request (void)
{
hio_dhcp4_pktbuf_t req, rep;
hio_dhcp4_pktinf_t inf;
hio_uint8_t reqbuf[BUFCAPA], repbuf[BUFCAPA];
hio_dhcp4_pktbuf_t tmp;
/* build the request in its own buffer so the reply does not overwrite it */
make_request (&tmp, HIO_DHCP4_MSG_DISCOVER);
HIO_MEMCPY (reqbuf, g_buf, tmp.len);
req = tmp;
req.hdr = (hio_dhcp4_pkt_hdr_t*)reqbuf;
as_pktinf (&inf, &req);
OK (hio_dhcp4_init_reply_pktbuf(&rep, repbuf, BUFCAPA, &inf) == 0,
"a reply can be started from a request");
OK (rep.hdr->op == HIO_DHCP4_OP_BOOTREPLY, "and is marked as a reply");
/* these four are what let the reply reach the right client and be
* recognised by it */
OK (rep.hdr->xid == req.hdr->xid, "and echoes the transaction id");
OK (rep.hdr->flags == req.hdr->flags, "and the broadcast flag");
OK (rep.hdr->giaddr == req.hdr->giaddr, "and the relay address");
OK (rep.hdr->hlen == req.hdr->hlen && rep.hdr->htype == req.hdr->htype &&
HIO_MEMCMP(rep.hdr->chaddr, req.hdr->chaddr, req.hdr->hlen) == 0,
"and the client's hardware address");
}
static void test_option_editing (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t v8;
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_OFFER);
hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64);
as_pktinf (&inf, &pkt);
g_walked = 0;
OK (hio_dhcp4_walk_options(&inf, count_opt) == 0,
"the options of a packet that fills its buffer walk to the end");
OK (g_walked == 3 && g_walk_codes[0] == HIO_DHCP4_OPT_MESSAGE_TYPE &&
g_walk_codes[2] == HIO_DHCP4_OPT_IP_TTL,
"walking the options visits each one in the order written");
OK (hio_dhcp4_delete_option(&pkt, HIO_DHCP4_OPT_LEASE_TIME) == 0,
"an option can be deleted");
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_LEASE_TIME) == HIO_NULL,
"and is then no longer found");
OK (hio_dhcp4_get_option_uint8(&inf, HIO_DHCP4_OPT_IP_TTL, &v8) == 0 && v8 == 64,
"while the options around it survive intact");
}
/* the one-octet options. PADDING and END carry no length field, and a
* traversal that does not account for that goes wrong in two separate ways -
* both of which every conforming packet reaches, because every conforming
* packet ends with an END option. */
static void test_one_octet_options (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
/* a packet whose final octet is a lone END. this is what this library's
* own reply builder emits, so a walk that rejects it rejects almost
* everything. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK);
hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0);
as_pktinf (&inf, &pkt);
g_walked = 0;
OK (hio_dhcp4_walk_options(&inf, count_opt) == 0 && g_walked == 2,
"a packet ending in a lone END option walks cleanly");
/* a padding octet between two options. padding is one octet, so advancing
* by two lands the cursor inside the following option and everything read
* after it is misaligned. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_PADDING, HIO_NULL, 0);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64);
hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0);
as_pktinf (&inf, &pkt);
g_walked = 0;
OK (hio_dhcp4_walk_options(&inf, count_opt) == 0 && g_walked == 3 &&
g_walk_codes[0] == HIO_DHCP4_OPT_MESSAGE_TYPE &&
g_walk_codes[1] == HIO_DHCP4_OPT_IP_TTL &&
g_walk_codes[2] == HIO_DHCP4_OPT_LEASE_TIME,
"and interior padding is stepped over one octet at a time");
/* and find_option must agree with the walk about the same packet - they
* are two traversals of one format, and they disagreed before */
{
hio_uint32_t lt = 0;
OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &lt) == 0 && lt == 3600,
"and find_option reads the option after the padding too");
}
}
static void test_overlong_option_still_refused (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t* lenfield;
/* the bounds check above was loosened by one octet to stop it rejecting
* the last option of a well-formed packet. this is the other side of that:
* a length that genuinely runs past the end must still be refused, or the
* loosening would have opened a read past the buffer. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK);
hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600);
as_pktinf (&inf, &pkt);
/* overstate the last option's length by one */
lenfield = &g_buf[pkt.len - 4 - 1];
OK (*lenfield == 4, "the length field is where the test expects it");
*lenfield = 5;
g_walked = 0;
OK (hio_dhcp4_walk_options(&inf, count_opt) <= -1,
"an option length that runs past the end of the packet is still refused");
}
static void test_capacity (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_uint8_t small[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 8];
hio_uint8_t payload[16];
OK (hio_dhcp4_init_pktbuf(&pkt, small, HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) - 1) <= -1,
"a buffer too small for the header is refused outright");
/* room for the cookie and a little else, so a large option must not be
* written past the end - the failure a fixed-size datagram buffer makes
* reachable from the network */
HIO_MEMSET (payload, 'x', HIO_SIZEOF(payload));
OK (hio_dhcp4_init_pktbuf(&pkt, small, HIO_SIZEOF(small)) == 0,
"a buffer with room for the header alone initialises");
OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_HOST_NAME, payload, HIO_SIZEOF(payload)) <= -1,
"and an option that would not fit is refused rather than overrunning it");
OK (pkt.len <= HIO_SIZEOF(small), "leaving the length within the buffer");
}
/* ------------------------------------------------------------------ */
/* the relay agent information option, RFC 3046 */
/* option 82 carries suboptions inside its own value, in the same
* code/length/value shape as options themselves but one level down. what makes
* it worth testing is the arithmetic: adding a suboption grows an option whose
* length field holds only 255, inside a packet buffer that is also finite -
* two limits that a length computed in an octet cannot express. */
static void test_relay_suboptions (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t* v;
hio_uint8_t vlen = 0;
static const hio_uint8_t circuit[4] = { 'e', 't', 'h', '0' };
static const hio_uint8_t remote[3] = { 'r', 'i', 'd' };
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER);
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, circuit, 4) == 0,
"a relay suboption can be added to a packet with no relay option");
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY) != HIO_NULL,
"and the relay option comes into being to hold it");
v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen);
OK (v && vlen == 4 && HIO_MEMCMP(v, circuit, 4) == 0,
"and it is found again with its value intact");
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID, remote, 3) == 0,
"a second suboption is appended to the option that exists");
as_pktinf (&inf, &pkt);
v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_REMOTE_ID, &vlen);
OK (v && vlen == 3 && HIO_MEMCMP(v, remote, 3) == 0, "and is found by its own code");
v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen);
OK (v && vlen == 4 && HIO_MEMCMP(v, circuit, 4) == 0, "while the first is still there");
OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) == 0,
"one suboption can be deleted");
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen) == HIO_NULL,
"and is then gone");
v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_REMOTE_ID, &vlen);
OK (v && vlen == 3 && HIO_MEMCMP(v, remote, 3) == 0, "while the other survives it");
/* deleting the last one takes the option with it rather than leaving an
* empty option behind for something else to trip over */
OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID) == 0,
"the last suboption can be deleted too");
as_pktinf (&inf, &pkt);
OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY) == HIO_NULL,
"and the now-empty relay option goes with it");
{
hio_uint8_t mt = 0;
OK (hio_dhcp4_get_msg_type(&inf, &mt) == 0 && mt == HIO_DHCP4_MSG_DISCOVER,
"and the options around it are undisturbed by the shuffling");
}
OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) <= -1,
"deleting a suboption that is not there reports failure");
}
static void test_relay_suboption_limits (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t big[255];
hio_uint8_t small[8];
HIO_MEMSET (big, 'x', HIO_SIZEOF(big));
HIO_MEMSET (small, 'y', HIO_SIZEOF(small));
/* a suboption costs two octets of header on top of its value, and an
* option length field holds 255 - so 254 does not fit. computed in an
* octet that sum wraps, and a huge suboption becomes a zero-length option:
* worse than a refusal, because nothing reports it. */
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 254) <= -1,
"a suboption too large for an option length field is refused");
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 253) == 0,
"and the largest one that does fit is accepted");
/* the same arithmetic one level up: appending to an option already at its
* limit would take its length past 255 */
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID, small, 8) <= -1,
"a suboption that would take the option past 255 octets is refused");
as_pktinf (&inf, &pkt);
{
hio_dhcp4_opt_hdr_t* o = hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY);
OK (o && o->len == 255, "leaving the option at its true length, not a wrapped one");
}
/* and the packet buffer is a limit of its own */
{
hio_uint8_t tiny[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 16];
hio_dhcp4_pktbuf_t tp;
hio_dhcp4_init_pktbuf (&tp, tiny, HIO_SIZEOF(tiny));
hio_dhcp4_add_option_uint8 (&tp, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER);
OK (hio_dhcp4_add_relay_suboption(&tp, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 100) <= -1,
"and one that would not fit the packet buffer is refused");
OK (tp.len <= HIO_SIZEOF(tiny), "leaving the length within the buffer");
}
}
static void test_relay_suboption_bounds (void)
{
hio_uint8_t raw[8];
hio_uint8_t* v;
hio_uint8_t vlen = 99;
/* a suboption claiming more than the enclosing option holds. the value
* pointer returned for it would be read that far by the caller - past the
* packet, when the relay option is the last one in it. */
raw[0] = HIO_DHCP4_OPT_RELAY_CIRCUIT_ID;
raw[1] = 200;
raw[2] = 'a';
raw[3] = 'b';
v = hio_dhcp4_get_relay_suboption_value(raw, 4, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen);
OK (v == HIO_NULL, "a suboption whose length overruns its option is refused");
raw[0] = HIO_DHCP4_OPT_RELAY_CIRCUIT_ID;
v = hio_dhcp4_get_relay_suboption_value(raw, 1, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen);
OK (v == HIO_NULL, "and so is a truncated suboption header");
/* the length pointer is optional - a caller may only want the value */
raw[0] = HIO_DHCP4_OPT_RELAY_REMOTE_ID;
raw[1] = 2;
raw[2] = 'z'; raw[3] = 'z';
v = hio_dhcp4_get_relay_suboption_value(raw, 4, HIO_DHCP4_OPT_RELAY_REMOTE_ID, HIO_NULL);
OK (v != HIO_NULL && v[0] == 'z', "and the length pointer may be omitted");
}
static void test_option_value_accessor (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_dhcp4_pktinf_t inf;
hio_uint8_t* v;
hio_uint8_t vlen = 0;
static const hio_bch_t name[] = "host.example";
hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA);
hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER);
hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_HOST_NAME, (void*)name, HIO_SIZEOF(name) - 1);
as_pktinf (&inf, &pkt);
v = hio_dhcp4_get_option_value(&inf, HIO_DHCP4_OPT_HOST_NAME, &vlen);
OK (v && vlen == HIO_SIZEOF(name) - 1 && HIO_MEMCMP(v, name, vlen) == 0,
"an option's value is reachable without the caller stepping over its header");
OK (hio_dhcp4_get_option_value(&inf, HIO_DHCP4_OPT_ROOT_PATH, &vlen) == HIO_NULL,
"and an absent option yields nothing rather than a header pointer");
}
/* a code is one octet on the wire, but the argument carrying it is an int, and
* it reaches the packet through a plain assignment. so a code past 255 was
* truncated: adding 'suboption 257' wrote suboption 1 - an Agent Circuit ID
* nobody asked for - and reported success, leaving data in the packet that the
* code the caller passed could never find again. */
static void test_code_must_fit_an_octet (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_uint8_t olen;
hio_oow_t before;
/* --- the option level --- */
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
before = pkt.len;
OK (hio_dhcp4_add_option(&pkt, 0x101, "x", 1) <= -1, "an option code past one octet is refused");
OK (hio_dhcp4_add_option(&pkt, -1, "x", 1) <= -1, "and a negative one");
OK (pkt.len == before, "neither wrote anything");
OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_SUBNET) == HIO_NULL,
"and option 1 was not created by the truncation of 0x101");
/* the ends of the octet range are legitimate codes: PADDING is 0 and
* END is 255, so the bound is the octet, not a narrower guess */
OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_PADDING, HIO_NULL, 0) == 0, "padding still adds");
OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0) == 0, "and so does END");
OK (pkt.len == before + 2, "one octet each");
/* --- the suboption level --- */
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
before = pkt.len;
OK (hio_dhcp4_add_relay_suboption(&pkt, 0x101, (const hio_uint8_t*)"secret", 6) <= -1,
"a suboption code past one octet is refused");
OK (pkt.len == before, "nothing was added");
OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY) == HIO_NULL,
"no relay option was created");
olen = 0xff;
OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &olen) == HIO_NULL,
"and in particular no circuit id, which 0x101 truncates to");
/* 0 is refused on the way in as well as on the way out: a suboption
* stored under it could never be looked up or deleted again, since this
* api spends 0 as its 'no suboption wanted' sentinel */
OK (hio_dhcp4_add_relay_suboption(&pkt, 0, (const hio_uint8_t*)"zero", 4) <= -1,
"a suboption code of 0 is refused when adding");
OK (pkt.len == before, "leaving the packet as it was");
/* and the whole legitimate range still works at both ends */
OK (hio_dhcp4_add_relay_suboption(&pkt, 1, (const hio_uint8_t*)"a", 1) == 0, "code 1 adds");
OK (hio_dhcp4_delete_relay_suboption(&pkt, 1) == 0, "and deletes");
OK (hio_dhcp4_add_relay_suboption(&pkt, 255, (const hio_uint8_t*)"b", 1) == 0, "code 255 adds");
OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 255, &olen) != HIO_NULL,
"and is findable by its own code");
OK (hio_dhcp4_delete_relay_suboption(&pkt, 255) == 0, "and deletes too");
OK (pkt.len == before, "back where it started");
}
/* dhcp4_find_option() only looks inside a relay option for a suboption when it
* is asked for one - a code above zero - and otherwise returns the relay option
* having written neither the value pointer nor the value length. the two
* suboption entry points go on to use both, so a code of zero or below has to
* be refused before the lookup rather than after it. there is no suboption 0
* to ask for in the first place. */
static void test_suboption_code_zero (void)
{
hio_dhcp4_pktbuf_t pkt;
hio_uint8_t olen;
hio_uint8_t* v;
hio_oow_t before;
make_request (&pkt, HIO_DHCP4_MSG_DISCOVER);
OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, (const hio_uint8_t*)"eth0", 4) == 0,
"a relay option with one suboption");
before = pkt.len;
/* the suboption that is there reads back */
olen = 0xff;
v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &olen);
OK (v != HIO_NULL && olen == 4 && HIO_MEMCMP(v, "eth0", 4) == 0, "and it is found by its own code");
/* code 0 is not a suboption. the relay option is present, so a lookup
* that forgot to check would find it and hand back a value pointer that
* was never written. */
olen = 0xff;
v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 0, &olen);
OK (v == HIO_NULL, "a suboption code of 0 finds nothing");
OK (olen == 0xff, "and the caller's length is left alone rather than half-written");
v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, -1, &olen);
OK (v == HIO_NULL, "nor does a negative one");
/* and nothing above an octet can name a suboption either */
olen = 0xff;
OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 256, &olen) == HIO_NULL,
"a code past one octet finds nothing");
OK (olen == 0xff, "with the caller's length untouched");
OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 0x101, &olen) == HIO_NULL,
"and one that would truncate to a real code finds nothing");
OK (hio_dhcp4_delete_relay_suboption(&pkt, 256) <= -1, "nor can such a code delete");
OK (hio_dhcp4_delete_relay_suboption(&pkt, 0x101) <= -1, "including one that would truncate");
OK (pkt.len == before, "and the packet is untouched by any of it");
OK (hio_dhcp4_delete_relay_suboption(&pkt, 0) <= -1, "deleting suboption 0 is refused");
OK (pkt.len == before, "and the packet is untouched");
OK (hio_dhcp4_delete_relay_suboption(&pkt, -1) <= -1, "as is a negative code");
OK (pkt.len == before, "leaving it untouched too");
/* and the real one still deletes, so the guard did not break the path.
* it was the option's only suboption, so what goes is the whole relay
* option: its own two-octet header plus the six the suboption occupied. */
OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) == 0,
"the suboption that is there still deletes");
OK (pkt.len == before - 8, "taking the now-empty relay option with it");
OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY) == HIO_NULL,
"so no relay option is left");
}
/* an option area ending exactly with {OVERLOAD, len=1} and no value octet.
* the overload branch reads that value to learn which fields are overloaded,
* and it runs for an option the search did not match - so the read has to be
* bounded by the option's length check, not by the match.
*
* the packet is heap-allocated at exactly its own size so that a read one
* octet past it is a heap overflow rather than a quiet touch of the next
* global, which is what let this sit unnoticed in three separate trees. */
static void test_overload_at_the_very_end (void)
{
hio_oow_t plen = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 4 + 2;
hio_uint8_t* buf = (hio_uint8_t*)malloc(plen);
hio_uint32_t cookie = HIO_CONST_HTON32(HIO_DHCP4_MAGIC_COOKIE);
hio_dhcp4_pktinf_t pkt;
OK (buf != HIO_NULL, "a packet sized to the octet");
memset (buf, 0, plen);
HIO_MEMCPY (buf + HIO_SIZEOF(hio_dhcp4_pkt_hdr_t), &cookie, 4);
buf[plen - 2] = HIO_DHCP4_OPT_OVERLOAD;
buf[plen - 1] = 1; /* claims a value octet that is not there */
pkt.hdr = (hio_dhcp4_pkt_hdr_t*)buf;
pkt.len = plen;
/* searching for something else reaches the overload option through the
* path that does not match it */
OK (hio_dhcp4_find_option(&pkt, HIO_DHCP4_OPT_SUBNET) == HIO_NULL,
"an option area ending in a truncated overload yields nothing");
OK (hio_dhcp4_find_option(&pkt, HIO_DHCP4_OPT_OVERLOAD) == HIO_NULL,
"and the truncated overload option is not returned either");
/* the walker reaches it too, and must refuse the packet rather than
* reading the octet that is not there */
g_walked = 0;
OK (hio_dhcp4_walk_options(&pkt, count_opt) <= -1, "the walker refuses it");
OK (g_walked == 0, "having visited nothing");
free (buf);
}
/* ------------------------------------------------------------------ */
int main (void)
{
no_plan ();
test_cookie_and_layout ();
test_byte_order ();
test_unaligned_payload ();
test_wrong_width_is_refused ();
test_msg_type ();
test_client_id ();
test_check_pkt ();
test_reply_echoes_the_request ();
test_option_editing ();
test_one_octet_options ();
test_overlong_option_still_refused ();
test_capacity ();
test_relay_suboptions ();
test_relay_suboption_limits ();
test_relay_suboption_bounds ();
test_option_value_accessor ();
test_suboption_code_zero ();
test_code_must_fit_an_octet ();
test_overload_at_the_very_end ();
return exit_status();
}