1046 lines
45 KiB
C
1046 lines
45 KiB
C
/*
|
|
* the dhcpv6 message layer.
|
|
*
|
|
* dhcpv6 relaying is not a header field but a containment: a relay agent
|
|
* wraps the message it received whole, as the value of a RELAY-MSG option
|
|
* inside the message it sends on. so every accessor here takes a relay level,
|
|
* and the interesting failures are all about that nesting - an inner message
|
|
* whose length disagrees with the option carrying it, an edit at one level
|
|
* that has to grow or shrink the option at every level outside it, and a
|
|
* packet that claims to nest more deeply than the protocol allows, which is
|
|
* what turns a recursive walker into a stack overflow.
|
|
*
|
|
* like t-017 this layer is pure - a buffer in, a buffer out - so all of it is
|
|
* reachable without a socket.
|
|
*/
|
|
|
|
#include <hio-dhcp.h>
|
|
#include <hio-prv.h>
|
|
#include "tap.h"
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
#define BUFCAPA 2048
|
|
|
|
static hio_uint8_t g_buf[BUFCAPA];
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* builders */
|
|
|
|
static void init_msg (hio_dhcp6_pktbuf_t* pkt, hio_uint8_t mtype)
|
|
{
|
|
OK (hio_dhcp6_init_pktbuf(pkt, g_buf, BUFCAPA) == 0, "pktbuf initialised");
|
|
pkt->hdr->msgtype = mtype;
|
|
pkt->hdr->transid[0] = 0x11;
|
|
pkt->hdr->transid[1] = 0x22;
|
|
pkt->hdr->transid[2] = 0x33;
|
|
}
|
|
|
|
/* write a message header and options into a plain buffer by hand, returning
|
|
* how many octets were used. the edit functions are not involved, so a test
|
|
* of them starts from something they did not build. */
|
|
static hio_oow_t put_hdr (hio_uint8_t* b, hio_uint8_t mtype)
|
|
{
|
|
b[0] = mtype;
|
|
b[1] = 0xaa; b[2] = 0xbb; b[3] = 0xcc;
|
|
return 4;
|
|
}
|
|
|
|
static hio_oow_t put_relay_hdr (hio_uint8_t* b, hio_uint8_t mtype, hio_uint8_t hop)
|
|
{
|
|
HIO_MEMSET (b, 0, HIO_SIZEOF(hio_dhcp6_relay_hdr_t));
|
|
b[0] = mtype;
|
|
b[1] = hop;
|
|
b[2 + 15] = 0x01; /* link address, last octet */
|
|
b[2 + 16 + 15] = 0x02; /* peer address, last octet */
|
|
return HIO_SIZEOF(hio_dhcp6_relay_hdr_t);
|
|
}
|
|
|
|
static hio_oow_t put_opt (hio_uint8_t* b, hio_uint16_t code, const void* data, hio_uint16_t dlen)
|
|
{
|
|
hio_uint16_t v;
|
|
v = hio_hton16(code); HIO_MEMCPY (&b[0], &v, 2);
|
|
v = hio_hton16(dlen); HIO_MEMCPY (&b[2], &v, 2);
|
|
if (dlen > 0) HIO_MEMCPY (&b[4], data, dlen);
|
|
return 4 + dlen;
|
|
}
|
|
|
|
/* an option's data, read out of the packed header without assuming the
|
|
* payload is aligned for a wider load */
|
|
static hio_uint16_t opt_code (const hio_dhcp6_opt_hdr_t* o) { return hio_ntoh16(o->code); }
|
|
static hio_uint16_t opt_len (const hio_dhcp6_opt_hdr_t* o) { return hio_ntoh16(o->len); }
|
|
static const hio_uint8_t* opt_data (const hio_dhcp6_opt_hdr_t* o) { return (const hio_uint8_t*)(o + 1); }
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_layout (void)
|
|
{
|
|
/* the two headers are on the wire exactly as rfc 3315 draws them, so
|
|
* their sizes are not free to differ from these */
|
|
OK (HIO_SIZEOF(hio_dhcp6_pkt_hdr_t) == 4, "message header is 4 octets");
|
|
OK (HIO_SIZEOF(hio_dhcp6_relay_hdr_t) == 34, "relay header is 34 octets");
|
|
OK (HIO_SIZEOF(hio_dhcp6_opt_hdr_t) == 4, "option header is 4 octets");
|
|
}
|
|
|
|
static void test_init_pktbuf (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
|
|
OK (hio_dhcp6_init_pktbuf(&pkt, g_buf, BUFCAPA) == 0, "init succeeds with room");
|
|
OK (pkt.len == HIO_SIZEOF(hio_dhcp6_pkt_hdr_t), "a fresh packet is just a header");
|
|
OK (pkt.capa == BUFCAPA, "capacity is recorded");
|
|
OK (pkt.hdr->msgtype == 0, "the header is cleared");
|
|
|
|
/* a buffer too small for even the fixed header cannot hold a message */
|
|
OK (hio_dhcp6_init_pktbuf(&pkt, g_buf, 3) <= -1, "init refuses a buffer under 4 octets");
|
|
}
|
|
|
|
static void test_append_and_find (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
static const hio_uint8_t duid[10] = { 0,1,0,1,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f };
|
|
hio_uint16_t elapsed = hio_hton16(0x0102);
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, duid, HIO_SIZEOF(duid));
|
|
OK (o != HIO_NULL, "clientid appended");
|
|
OK (pkt.len == 4 + 4 + 10, "the packet grew by header plus data");
|
|
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_ELAPSED_TIME, &elapsed, 2);
|
|
OK (o != HIO_NULL, "elapsed time appended");
|
|
OK (pkt.len == 4 + 14 + 6, "and again");
|
|
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL, "clientid found");
|
|
OK (opt_len(o) == 10, "with the length it was given");
|
|
OK (memcmp(opt_data(o), duid, 10) == 0, "and the data it was given");
|
|
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_ELAPSED_TIME);
|
|
OK (o != HIO_NULL, "elapsed time found");
|
|
OK (opt_len(o) == 2, "two octets of it");
|
|
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (o == HIO_NULL, "an option that was never added is not found");
|
|
}
|
|
|
|
static void test_byte_order (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
const hio_uint8_t* raw;
|
|
|
|
/* both fields of a dhcpv6 option header are 16 bit and network order, so
|
|
* the octets on the wire are fixed regardless of this machine's */
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_IA_PD, "ab", 2);
|
|
OK (o != HIO_NULL, "option 25 added");
|
|
|
|
raw = (const hio_uint8_t*)o;
|
|
OK (raw[0] == 0x00 && raw[1] == 25, "the code is big endian on the wire");
|
|
OK (raw[2] == 0x00 && raw[3] == 0x02, "so is the length");
|
|
OK (opt_code(o) == HIO_DHCP6_OPT_IA_PD, "and reads back in host order");
|
|
}
|
|
|
|
static void test_walk_every_option (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
int n = 0;
|
|
int codes[8];
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_REQUEST);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, "c", 1);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_SERVERID, "ss", 2);
|
|
/* a zero-length option is legal - rapid commit is defined as exactly that */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_RAPID_COMMIT, HIO_NULL, 0);
|
|
OK (o != HIO_NULL, "a zero-length option is accepted");
|
|
OK (opt_len(o) == 0, "and carries no data");
|
|
|
|
o = HIO_NULL;
|
|
while ((o = hio_dhcp6_get_option_after((hio_dhcp6_pktinf_t*)&pkt, 0, o)))
|
|
{
|
|
if (n < (int)HIO_COUNTOF(codes)) codes[n] = opt_code(o);
|
|
n++;
|
|
if (n > (int)HIO_COUNTOF(codes)) break; /* a walker that does not advance */
|
|
}
|
|
|
|
OK (n == 3, "the walk visits every option once");
|
|
OK (codes[0] == HIO_DHCP6_OPT_CLIENTID, "in the order they were appended (1)");
|
|
OK (codes[1] == HIO_DHCP6_OPT_SERVERID, "in the order they were appended (2)");
|
|
OK (codes[2] == HIO_DHCP6_OPT_RAPID_COMMIT, "in the order they were appended (3)");
|
|
}
|
|
|
|
static void test_find_after (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* a;
|
|
hio_dhcp6_opt_hdr_t* b;
|
|
|
|
/* several options may share a code - two IA_NAs for two addresses - so
|
|
* finding one has to be able to continue past it */
|
|
init_msg (&pkt, HIO_DHCP6_MSG_REQUEST);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_IA_NA, "one", 3);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_ELAPSED_TIME, "xx", 2);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_IA_NA, "two", 3);
|
|
|
|
a = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_IA_NA);
|
|
OK (a != HIO_NULL && memcmp(opt_data(a), "one", 3) == 0, "the first of two IA_NAs");
|
|
|
|
b = hio_dhcp6_find_option_after((hio_dhcp6_pktinf_t*)&pkt, 0, a, HIO_DHCP6_OPT_IA_NA);
|
|
OK (b != HIO_NULL && memcmp(opt_data(b), "two", 3) == 0, "and the second, skipping the option between");
|
|
|
|
OK (hio_dhcp6_find_option_after((hio_dhcp6_pktinf_t*)&pkt, 0, b, HIO_DHCP6_OPT_IA_NA) == HIO_NULL,
|
|
"and no third");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* relay nesting */
|
|
|
|
/* RELAY-FORW { RELAY-MSG: RELAY-FORW { RELAY-MSG: SOLICIT { CLIENTID } } },
|
|
* built by hand so the nesting under test is not the nesting the edit
|
|
* functions produce. returns the total length. */
|
|
static hio_oow_t build_two_level_relay (hio_uint8_t* b)
|
|
{
|
|
hio_uint8_t inner[64];
|
|
hio_uint8_t mid[128];
|
|
hio_oow_t ilen = 0, mlen = 0, olen = 0;
|
|
|
|
ilen += put_hdr(&inner[ilen], HIO_DHCP6_MSG_SOLICIT);
|
|
ilen += put_opt(&inner[ilen], HIO_DHCP6_OPT_CLIENTID, "innerduid", 9);
|
|
|
|
mlen += put_relay_hdr(&mid[mlen], HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
mlen += put_opt(&mid[mlen], HIO_DHCP6_OPT_INTERFACE_ID, "eth1", 4);
|
|
mlen += put_opt(&mid[mlen], HIO_DHCP6_OPT_RELAY_MESSAGE, inner, (hio_uint16_t)ilen);
|
|
|
|
olen += put_relay_hdr(&b[olen], HIO_DHCP6_MSG_RELAYFORW, 1);
|
|
olen += put_opt(&b[olen], HIO_DHCP6_OPT_RELAY_MESSAGE, mid, (hio_uint16_t)mlen);
|
|
olen += put_opt(&b[olen], HIO_DHCP6_OPT_INTERFACE_ID, "eth0", 4);
|
|
|
|
return olen;
|
|
}
|
|
|
|
static void test_relay_level (void)
|
|
{
|
|
hio_dhcp6_pktinf_t pkt;
|
|
hio_dhcp6_pkt_hdr_t* h;
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 2, "two relay agents means level 2");
|
|
|
|
h = hio_dhcp6_get_pkt_hdr(&pkt, 0);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_RELAYFORW, "level 0 is the outermost relay");
|
|
|
|
h = hio_dhcp6_get_pkt_hdr(&pkt, 1);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_RELAYFORW, "level 1 is the inner relay");
|
|
OK (h != HIO_NULL && ((hio_dhcp6_relay_hdr_t*)h)->hopcount == 0, "with its own hop count");
|
|
|
|
h = hio_dhcp6_get_pkt_hdr(&pkt, 2);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_SOLICIT, "level 2 is the client's own message");
|
|
|
|
h = hio_dhcp6_get_pkt_hdr(&pkt, -1);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_SOLICIT, "a negative level means the innermost");
|
|
|
|
/* a level the packet does not nest to is an error. returning the
|
|
* innermost instead would have a caller read one message believing it
|
|
* was another. */
|
|
OK (hio_dhcp6_get_pkt_hdr(&pkt, 3) == HIO_NULL, "a level past the nesting is refused");
|
|
|
|
/* a plain message is level 0 and nothing else */
|
|
pkt.len = put_hdr(g_buf, HIO_DHCP6_MSG_SOLICIT);
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 0, "an unrelayed message is level 0");
|
|
h = hio_dhcp6_get_pkt_hdr(&pkt, 0);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_SOLICIT, "and level 0 is itself");
|
|
OK (hio_dhcp6_get_pkt_hdr(&pkt, 1) == HIO_NULL, "asking it for level 1 fails");
|
|
}
|
|
|
|
static void test_find_per_level (void)
|
|
{
|
|
hio_dhcp6_pktinf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
|
|
/* the two relay levels each carry an INTERFACE_ID, and they differ. this
|
|
* is the case that catches a find that ignores its level argument. */
|
|
o = hio_dhcp6_find_option(&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (o != HIO_NULL && opt_len(o) == 4 && memcmp(opt_data(o), "eth0", 4) == 0,
|
|
"level 0 finds the outer relay's interface id");
|
|
|
|
o = hio_dhcp6_find_option(&pkt, 1, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (o != HIO_NULL && opt_len(o) == 4 && memcmp(opt_data(o), "eth1", 4) == 0,
|
|
"level 1 finds the inner relay's, not the outer one's");
|
|
|
|
o = hio_dhcp6_find_option(&pkt, 2, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "innerduid", 9) == 0,
|
|
"level 2 finds the client's own option");
|
|
|
|
o = hio_dhcp6_find_option(&pkt, -1, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "innerduid", 9) == 0,
|
|
"and so does the innermost level");
|
|
|
|
/* the client id is at level 2 only */
|
|
OK (hio_dhcp6_find_option(&pkt, 0, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"the client id is not at level 0");
|
|
OK (hio_dhcp6_find_option(&pkt, 1, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"nor at level 1");
|
|
OK (hio_dhcp6_find_option(&pkt, 3, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"and a level past the nesting finds nothing rather than the innermost");
|
|
}
|
|
|
|
static void test_malformed_relay (void)
|
|
{
|
|
hio_dhcp6_pktinf_t pkt;
|
|
hio_uint8_t inner[32];
|
|
hio_oow_t ilen = 0, olen = 0;
|
|
hio_uint16_t v;
|
|
|
|
/* a RELAY-MSG whose length runs past the end of the packet carrying it */
|
|
ilen += put_hdr(&inner[ilen], HIO_DHCP6_MSG_SOLICIT);
|
|
ilen += put_opt(&inner[ilen], HIO_DHCP6_OPT_CLIENTID, "duid", 4);
|
|
|
|
olen += put_relay_hdr(&g_buf[olen], HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_RELAY_MESSAGE, inner, (hio_uint16_t)ilen);
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = olen;
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 1, "the well-formed version nests once");
|
|
|
|
/* overstate the inner length by one octet */
|
|
v = hio_hton16((hio_uint16_t)(ilen + 1));
|
|
HIO_MEMCPY (&g_buf[HIO_SIZEOF(hio_dhcp6_relay_hdr_t) + 2], &v, 2);
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 0, "a relay message running past the packet is not followed");
|
|
OK (hio_dhcp6_find_option(&pkt, 1, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"and nothing is read out of it");
|
|
|
|
/* a relay with no RELAY-MSG option at all relays nothing */
|
|
olen = put_relay_hdr(g_buf, HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_INTERFACE_ID, "eth0", 4);
|
|
pkt.len = olen;
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 0, "a relay carrying nothing is level 0");
|
|
OK (hio_dhcp6_find_option(&pkt, -1, HIO_DHCP6_OPT_INTERFACE_ID) != HIO_NULL,
|
|
"its own options are still readable");
|
|
|
|
/* a relay header truncated mid-address */
|
|
pkt.len = HIO_SIZEOF(hio_dhcp6_relay_hdr_t) - 1;
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == 0, "a truncated relay header is not followed");
|
|
OK (hio_dhcp6_find_option(&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID) == HIO_NULL,
|
|
"and yields no options");
|
|
|
|
/* an option whose length exceeds what is left */
|
|
olen = put_hdr(g_buf, HIO_DHCP6_MSG_SOLICIT);
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_CLIENTID, "duid", 4);
|
|
pkt.len = olen - 1; /* one octet of the value is gone */
|
|
OK (hio_dhcp6_find_option(&pkt, 0, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"an option whose value is cut short is not returned");
|
|
}
|
|
|
|
static void test_nesting_bound (void)
|
|
{
|
|
hio_dhcp6_pktinf_t pkt;
|
|
hio_oow_t total;
|
|
int i;
|
|
const int levels = HIO_DHCP6_MAX_RELAY_LEVEL + 8;
|
|
hio_oow_t hdr_at[HIO_DHCP6_MAX_RELAY_LEVEL + 8];
|
|
|
|
/* a packet that nests further than the protocol permits. the traversals
|
|
* are recursive, so without the level cap this is a stack overflow driven
|
|
* by an attacker's packet rather than a parse failure.
|
|
*
|
|
* it is built outside in: write every relay header first, then close the
|
|
* RELAY-MSG lengths from the inside out. */
|
|
HIO_MEMSET (g_buf, 0, BUFCAPA);
|
|
total = 0;
|
|
for (i = 0; i < levels; i++)
|
|
{
|
|
hio_uint16_t v;
|
|
hdr_at[i] = total;
|
|
total += put_relay_hdr(&g_buf[total], HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
/* the RELAY-MSG header; its length is filled in below */
|
|
v = hio_hton16(HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
HIO_MEMCPY (&g_buf[total], &v, 2);
|
|
total += 4;
|
|
}
|
|
total += put_hdr(&g_buf[total], HIO_DHCP6_MSG_SOLICIT);
|
|
/* an option down at the bottom. a traversal that followed the nesting all
|
|
* the way would reach it; one that stops at the cap must not. */
|
|
total += put_opt(&g_buf[total], HIO_DHCP6_OPT_CLIENTID, "toodeep", 7);
|
|
|
|
for (i = levels - 1; i >= 0; i--)
|
|
{
|
|
hio_oow_t opt_at = hdr_at[i] + HIO_SIZEOF(hio_dhcp6_relay_hdr_t);
|
|
hio_uint16_t v = hio_hton16((hio_uint16_t)(total - (opt_at + 4)));
|
|
HIO_MEMCPY (&g_buf[opt_at + 2], &v, 2);
|
|
}
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = total;
|
|
|
|
OK (total <= BUFCAPA, "the deep packet fits the buffer");
|
|
OK (hio_dhcp6_get_relay_level(&pkt) == HIO_DHCP6_MAX_RELAY_LEVEL,
|
|
"the nesting is reported no deeper than the protocol allows");
|
|
|
|
/* and the walkers stop there too rather than following it down */
|
|
OK (hio_dhcp6_get_pkt_hdr(&pkt, HIO_DHCP6_MAX_RELAY_LEVEL) != HIO_NULL,
|
|
"the last permitted level is reachable");
|
|
OK (hio_dhcp6_get_pkt_hdr(&pkt, HIO_DHCP6_MAX_RELAY_LEVEL + 1) == HIO_NULL,
|
|
"one past it is not");
|
|
OK (hio_dhcp6_find_option(&pkt, HIO_DHCP6_MAX_RELAY_LEVEL + 1, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"and a find past it does not recurse");
|
|
OK (hio_dhcp6_find_option(&pkt, levels, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"the option at the bottom of an over-deep packet is not reached");
|
|
OK (hio_dhcp6_find_option(&pkt, -1, HIO_DHCP6_OPT_CLIENTID) == HIO_NULL,
|
|
"nor is it reached by asking for the innermost");
|
|
}
|
|
|
|
/* a relay message that relays nothing is as deep as its packet goes, so
|
|
* asking for the innermost level has to mean that one - the same answer
|
|
* hio_dhcp6_get_pkt_hdr() gives - rather than nothing at all. otherwise the
|
|
* two disagree about what a malformed relay's innermost message is. */
|
|
static void test_innermost_of_a_broken_chain (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_pkt_hdr_t* h;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_oow_t olen;
|
|
|
|
olen = put_relay_hdr(g_buf, HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_INTERFACE_ID, "eth0", 4);
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = olen;
|
|
pkt.capa = BUFCAPA;
|
|
|
|
h = hio_dhcp6_get_pkt_hdr((hio_dhcp6_pktinf_t*)&pkt, -1);
|
|
OK (h != HIO_NULL && h->msgtype == HIO_DHCP6_MSG_RELAYFORW, "the innermost message is the relay itself");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, -1, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (o != HIO_NULL, "and a find at the innermost level agrees");
|
|
|
|
/* and so does an edit */
|
|
o = hio_dhcp6_insert_option_at(&pkt, -1, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, "p", 1);
|
|
OK (o != HIO_NULL, "an insert at the innermost level lands there");
|
|
OK (pkt.len == olen + 5, "the packet grew");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE);
|
|
OK (o != HIO_NULL, "which for this packet is level 0");
|
|
OK (hio_dhcp6_delete_option_at(&pkt, -1, o, 1) == 0, "and a delete at the innermost level too");
|
|
OK (pkt.len == olen, "leaving it as it was");
|
|
|
|
/* a definite level the packet does not have is still refused */
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_INTERFACE_ID) == HIO_NULL,
|
|
"but naming level 1 does not settle for level 0");
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 1, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, "p", 1) == HIO_NULL,
|
|
"and neither does an insert");
|
|
OK (pkt.len == olen, "which changed nothing");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* editing */
|
|
|
|
static void test_insert_at_position (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* first;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
int n = 0;
|
|
int codes[8];
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_SERVERID, "sid", 3);
|
|
first = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (first != HIO_NULL, "an option to insert before");
|
|
|
|
/* inserting at an existing option's position puts the new one first and
|
|
* shifts that one along */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, first, 1, HIO_DHCP6_OPT_CLIENTID, "cid", 3);
|
|
OK (o != HIO_NULL, "inserted at a position rather than appended");
|
|
OK (o == first, "the new option occupies the old one's place");
|
|
|
|
o = HIO_NULL;
|
|
while ((o = hio_dhcp6_get_option_after((hio_dhcp6_pktinf_t*)&pkt, 0, o)))
|
|
{
|
|
if (n < (int)HIO_COUNTOF(codes)) codes[n] = opt_code(o);
|
|
if (++n > (int)HIO_COUNTOF(codes)) break;
|
|
}
|
|
OK (n == 2, "both options are present");
|
|
OK (codes[0] == HIO_DHCP6_OPT_CLIENTID, "the inserted one comes first");
|
|
OK (codes[1] == HIO_DHCP6_OPT_SERVERID, "and the shifted one after it");
|
|
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "sid", 3) == 0, "the shifted option kept its data");
|
|
}
|
|
|
|
static void test_insert_limits (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
static hio_uint8_t big[BUFCAPA];
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
|
|
/* the buffer bounds the packet: header plus option header plus data */
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_USER_CLASS, big, BUFCAPA) == HIO_NULL,
|
|
"an option larger than the buffer is refused");
|
|
OK (pkt.len == 4, "and the packet is left as it was");
|
|
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_USER_CLASS, big, BUFCAPA - 4 - 4) != HIO_NULL,
|
|
"one that exactly fills the buffer is accepted");
|
|
OK (pkt.len == BUFCAPA, "and fills it");
|
|
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, "p", 1) == HIO_NULL,
|
|
"nothing more fits");
|
|
OK (pkt.len == BUFCAPA, "and the failed insert changed nothing");
|
|
|
|
/* an option length field is 16 bits, so data past 65535 cannot be
|
|
* described - the length would wrap and the option would claim to carry
|
|
* a different amount than was copied in */
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
pkt.capa = 0x20000; /* pretend to a buffer large enough to reach the real limit */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_USER_CLASS, big, HIO_DHCP6_MAX_OPT_DLEN + 1);
|
|
OK (o == HIO_NULL, "data past what a 16-bit length can express is refused");
|
|
OK (pkt.len == 4, "and nothing was written");
|
|
}
|
|
|
|
static void test_insert_bad_position (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* first;
|
|
hio_uint8_t* p;
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_SOLICIT);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_SERVERID, "server", 6);
|
|
first = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (first != HIO_NULL, "a starting packet with one option");
|
|
|
|
/* a position one octet into an option is not where an option begins, and
|
|
* with checking asked for it is refused rather than splicing the packet
|
|
* into nonsense */
|
|
p = (hio_uint8_t*)first + 1;
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, (hio_dhcp6_opt_hdr_t*)p, 1, HIO_DHCP6_OPT_CLIENTID, "c", 1) == HIO_NULL,
|
|
"a position inside an option is refused");
|
|
OK (pkt.len == 4 + 10, "and the packet is untouched");
|
|
|
|
/* as is one outside the packet altogether */
|
|
p = (hio_uint8_t*)pkt.hdr + pkt.len + 8;
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, (hio_dhcp6_opt_hdr_t*)p, 1, HIO_DHCP6_OPT_CLIENTID, "c", 1) == HIO_NULL,
|
|
"a position past the packet is refused");
|
|
OK (pkt.len == 4 + 10, "and that leaves it untouched too");
|
|
|
|
/* the position just past the last option is the append point, and is
|
|
* allowed */
|
|
p = (hio_uint8_t*)pkt.hdr + pkt.len;
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 0, (hio_dhcp6_opt_hdr_t*)p, 1, HIO_DHCP6_OPT_CLIENTID, "c", 1) != HIO_NULL,
|
|
"the position just after the last option is the append point");
|
|
OK (pkt.len == 4 + 10 + 5, "and the append took");
|
|
}
|
|
|
|
static void test_delete (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_oow_t before;
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_REQUEST);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, "cid", 3);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_ELAPSED_TIME, "xx", 2);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_SERVERID, "sid", 3);
|
|
before = pkt.len;
|
|
|
|
/* deleting from the middle closes the gap */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_ELAPSED_TIME);
|
|
OK (o != HIO_NULL, "the option to delete is there");
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, o, 1) == 0, "deleted");
|
|
OK (pkt.len == before - 6, "the packet shrank by header plus data");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_ELAPSED_TIME) == HIO_NULL,
|
|
"and it is gone");
|
|
|
|
/* the options either side survived intact, which a wrong move length
|
|
* would not leave true */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && opt_len(o) == 3 && memcmp(opt_data(o), "cid", 3) == 0, "the option before it is intact");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (o != HIO_NULL && opt_len(o) == 3 && memcmp(opt_data(o), "sid", 3) == 0, "and the one after it moved down whole");
|
|
|
|
/* deleting the last one leaves a message with no options */
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, o, 1) == 0, "the last option deleted");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL, "the first is still there");
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, o, 1) == 0, "and it deletes too");
|
|
OK (pkt.len == 4, "leaving just the message header");
|
|
OK (hio_dhcp6_get_option_after((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_NULL) == HIO_NULL, "with no options at all");
|
|
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, HIO_NULL, 1) <= -1, "deleting nothing is refused");
|
|
}
|
|
|
|
static void test_delete_bad_position (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_uint8_t* p;
|
|
hio_uint16_t v;
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_REQUEST);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, "cid", 3);
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL, "one option present");
|
|
|
|
/* the append point is not an option, so there is nothing there to delete */
|
|
p = (hio_uint8_t*)pkt.hdr + pkt.len;
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, (hio_dhcp6_opt_hdr_t*)p, 1) <= -1,
|
|
"deleting at the append point is refused");
|
|
OK (pkt.len == 4 + 7, "and the packet is unchanged");
|
|
|
|
/* a position past the packet is refused with checking off as well as on:
|
|
* the move length is derived from it, and an unchecked one underflows */
|
|
p = (hio_uint8_t*)pkt.hdr + pkt.len + 16;
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, (hio_dhcp6_opt_hdr_t*)p, 0) <= -1,
|
|
"a position past the packet is refused even unchecked");
|
|
OK (pkt.len == 4 + 7, "and still unchanged");
|
|
|
|
/* nor may the option's own length field reach past the packet */
|
|
v = hio_hton16(4000);
|
|
HIO_MEMCPY ((hio_uint8_t*)o + 2, &v, 2);
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 0, o, 0) <= -1,
|
|
"an option claiming more data than the packet holds is refused");
|
|
OK (pkt.len == 4 + 7, "and changes nothing");
|
|
}
|
|
|
|
static void test_replace (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_oow_t before;
|
|
|
|
init_msg (&pkt, HIO_DHCP6_MSG_REPLY);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, "cid", 3);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, "p", 1);
|
|
hio_dhcp6_insert_option_at (&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_SERVERID, "sid", 3);
|
|
before = pkt.len;
|
|
|
|
/* same size: nothing moves */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE);
|
|
o = hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_PREFERENCE, "q", 1);
|
|
OK (o != HIO_NULL, "replaced with the same length");
|
|
OK (pkt.len == before, "the packet is the same size");
|
|
OK (opt_data(o)[0] == 'q', "with the new value");
|
|
|
|
/* larger: the tail shifts up */
|
|
o = hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_PREFERENCE, "abcdefgh", 8);
|
|
OK (o != HIO_NULL, "replaced with something longer");
|
|
OK (pkt.len == before + 7, "the packet grew by the difference");
|
|
OK (opt_len(o) == 8 && memcmp(opt_data(o), "abcdefgh", 8) == 0, "and holds the longer value");
|
|
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "sid", 3) == 0, "the option after it shifted up whole");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "cid", 3) == 0, "the one before it did not move");
|
|
|
|
/* smaller: the tail shifts down */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE);
|
|
o = hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_PREFERENCE, HIO_NULL, 0);
|
|
OK (o != HIO_NULL, "replaced with nothing at all");
|
|
OK (pkt.len == before - 1, "the packet shrank");
|
|
OK (opt_len(o) == 0, "and the option carries no data");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_SERVERID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "sid", 3) == 0, "with the tail intact again");
|
|
|
|
/* the code may change too, not only the value */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE);
|
|
o = hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_RAPID_COMMIT, HIO_NULL, 0);
|
|
OK (o != HIO_NULL, "replaced with a different code");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE) == HIO_NULL,
|
|
"the old code is gone");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RAPID_COMMIT) != HIO_NULL,
|
|
"and the new one is there");
|
|
|
|
/* a replacement that will not fit is refused, and changes nothing */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_CLIENTID);
|
|
before = pkt.len;
|
|
OK (hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_CLIENTID, g_buf, BUFCAPA) == HIO_NULL,
|
|
"a replacement past the buffer is refused");
|
|
OK (pkt.len == before, "and the packet is unchanged");
|
|
OK (hio_dhcp6_replace_option_at(&pkt, 0, HIO_NULL, 1, HIO_DHCP6_OPT_CLIENTID, "x", 1) == HIO_NULL,
|
|
"replacing nothing is refused");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* editing through a relay chain */
|
|
|
|
static void test_edit_inner_level (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_dhcp6_opt_hdr_t* rm0;
|
|
hio_dhcp6_opt_hdr_t* rm1;
|
|
hio_oow_t before;
|
|
hio_uint16_t rm0_len, rm1_len;
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
pkt.capa = BUFCAPA;
|
|
before = pkt.len;
|
|
|
|
rm0 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
rm1 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm0 != HIO_NULL && rm1 != HIO_NULL, "both relay message options found");
|
|
rm0_len = opt_len(rm0);
|
|
rm1_len = opt_len(rm1);
|
|
|
|
/* appending inside the client's own message has to grow the RELAY-MSG
|
|
* option at each of the two levels enclosing it, or the packet stops
|
|
* describing itself */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 2, HIO_NULL, 1, HIO_DHCP6_OPT_ELAPSED_TIME, "\x00\x05", 2);
|
|
OK (o != HIO_NULL, "an option appended at level 2");
|
|
OK (pkt.len == before + 6, "the whole packet grew by header plus data");
|
|
|
|
rm0 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
rm1 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm0 != HIO_NULL && opt_len(rm0) == rm0_len + 6, "the outer relay option grew to match");
|
|
OK (rm1 != HIO_NULL && opt_len(rm1) == rm1_len + 6, "and so did the inner one");
|
|
|
|
/* which is to say the nesting still parses, and everything is still
|
|
* where it was */
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 2, "the packet still nests twice");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 2, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "innerduid", 9) == 0, "the client id survived");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 2, HIO_DHCP6_OPT_ELAPSED_TIME);
|
|
OK (o != HIO_NULL && opt_len(o) == 2, "and the new option is at level 2");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_ELAPSED_TIME) == HIO_NULL,
|
|
"not at level 0");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "eth1", 4) == 0, "the inner relay's own option is intact");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "eth0", 4) == 0, "and so is the outer relay's");
|
|
|
|
/* deleting it again puts every length back */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 2, HIO_DHCP6_OPT_ELAPSED_TIME);
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 2, o, 1) == 0, "and deleted from level 2");
|
|
OK (pkt.len == before, "the packet is its original size");
|
|
rm0 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
rm1 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm0 != HIO_NULL && opt_len(rm0) == rm0_len, "the outer relay option shrank back");
|
|
OK (rm1 != HIO_NULL && opt_len(rm1) == rm1_len, "and so did the inner one");
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 2, "and it still nests twice");
|
|
|
|
/* the round trip put every octet back, not merely every length */
|
|
{
|
|
static hio_uint8_t fresh[BUFCAPA];
|
|
hio_oow_t flen = build_two_level_relay(fresh);
|
|
OK (flen == pkt.len && memcmp(fresh, g_buf, flen) == 0,
|
|
"insert then delete leaves the packet byte for byte as it was");
|
|
}
|
|
}
|
|
|
|
static void test_edit_middle_level (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_dhcp6_opt_hdr_t* rm0;
|
|
hio_uint16_t rm0_len, rm1_len;
|
|
hio_oow_t before;
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
pkt.capa = BUFCAPA;
|
|
before = pkt.len;
|
|
|
|
rm0_len = opt_len(hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE));
|
|
rm1_len = opt_len(hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_RELAY_MESSAGE));
|
|
|
|
/* an edit at level 1 grows the option outside it but not the one inside */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 1, HIO_NULL, 1, HIO_DHCP6_OPT_VENDOR_VSI, "vs", 2);
|
|
OK (o != HIO_NULL, "an option appended at level 1");
|
|
OK (pkt.len == before + 6, "the packet grew");
|
|
|
|
rm0 = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm0 != HIO_NULL && opt_len(rm0) == rm0_len + 6, "the enclosing relay option grew");
|
|
OK (opt_len(hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_RELAY_MESSAGE)) == rm1_len,
|
|
"the one it encloses did not");
|
|
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_VENDOR_VSI) != HIO_NULL,
|
|
"the new option reads back at level 1");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 2, HIO_DHCP6_OPT_CLIENTID) != HIO_NULL,
|
|
"and the client's message is still reachable through it");
|
|
|
|
/* replacing at level 0 shifts everything nested inside, so the whole
|
|
* chain has to come out the other side */
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
o = hio_dhcp6_replace_option_at(&pkt, 0, o, 1, HIO_DHCP6_OPT_INTERFACE_ID, "eth0.100", 8);
|
|
OK (o != HIO_NULL, "the outermost interface id replaced with a longer one");
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 2, "the packet still nests twice");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 2, HIO_DHCP6_OPT_CLIENTID);
|
|
OK (o != HIO_NULL && memcmp(opt_data(o), "innerduid", 9) == 0, "and the innermost option is intact");
|
|
}
|
|
|
|
/* an edit that cannot be completed must leave the packet exactly as it was.
|
|
* checking the lengths is not enough: an implementation that shifts the octets
|
|
* first and only then discovers the enclosing option cannot describe the
|
|
* result reports the failure with a packet already spliced. */
|
|
static hio_uint8_t g_snap[BUFCAPA];
|
|
static hio_oow_t g_snap_len;
|
|
|
|
static void snapshot (const hio_dhcp6_pktbuf_t* pkt)
|
|
{
|
|
g_snap_len = pkt->len;
|
|
HIO_MEMCPY (g_snap, pkt->hdr, pkt->len);
|
|
}
|
|
|
|
static int unchanged (const hio_dhcp6_pktbuf_t* pkt)
|
|
{
|
|
return pkt->len == g_snap_len && memcmp(g_snap, pkt->hdr, g_snap_len) == 0;
|
|
}
|
|
|
|
static void test_relay_length_bound (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* rm;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_uint16_t v;
|
|
|
|
/* understate what the outer relay option carries, then remove something
|
|
* from inside it. the removal cannot be subtracted from a length that
|
|
* small, and an implementation that subtracted anyway would wrap the
|
|
* field round to just under 65536.
|
|
*
|
|
* the position and the level are taken while the packet is still
|
|
* consistent, since corrupting the length is what makes level 1
|
|
* unreachable through a lookup - which is why the edit is unchecked. */
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
pkt.capa = BUFCAPA;
|
|
|
|
rm = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (rm != HIO_NULL && o != HIO_NULL, "a relay chain to edit inside");
|
|
v = hio_hton16(2);
|
|
HIO_MEMCPY ((hio_uint8_t*)rm + 2, &v, 2);
|
|
snapshot (&pkt);
|
|
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 1, o, 0) <= -1,
|
|
"a delete larger than the enclosing option claims to hold is refused");
|
|
OK (opt_len(rm) == 2, "the length is not wrapped below zero");
|
|
OK (unchanged(&pkt), "and nothing was moved");
|
|
|
|
/* a replacement that shrinks is the same problem */
|
|
OK (hio_dhcp6_replace_option_at(&pkt, 1, o, 0, HIO_DHCP6_OPT_INTERFACE_ID, HIO_NULL, 0) == HIO_NULL,
|
|
"a replacement shrinking past it is refused too");
|
|
OK (opt_len(rm) == 2, "with the length still unwrapped");
|
|
OK (unchanged(&pkt), "and the packet untouched");
|
|
}
|
|
|
|
/* the other direction needs a real packet of very nearly the size an option
|
|
* length can express - a relay option cannot be overstated and still be
|
|
* traversed, so the only way to reach the top of the range is to get there
|
|
* honestly. */
|
|
#define HUGE_INNER_DLEN (65500)
|
|
#define HUGE_CAPA (HUGE_INNER_DLEN + 512)
|
|
static hio_uint8_t g_huge[HUGE_CAPA];
|
|
static hio_uint8_t g_huge_snap[HUGE_CAPA];
|
|
|
|
static void test_enclosing_length_ceiling (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* rm;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_uint8_t* inner;
|
|
hio_oow_t ilen, olen, snap_len;
|
|
|
|
/* RELAY-FORW { RELAY-MSG: SOLICIT { USER-CLASS of 65500 octets } } */
|
|
inner = &g_huge[HIO_SIZEOF(hio_dhcp6_relay_hdr_t) + 4];
|
|
HIO_MEMSET (inner, 0x5a, HUGE_INNER_DLEN + 8);
|
|
ilen = put_hdr(inner, HIO_DHCP6_MSG_SOLICIT);
|
|
ilen += put_opt(&inner[ilen], HIO_DHCP6_OPT_USER_CLASS, HIO_NULL, 0);
|
|
{
|
|
hio_uint16_t v = hio_hton16(HUGE_INNER_DLEN);
|
|
HIO_MEMCPY (&inner[ilen - 2], &v, 2);
|
|
ilen += HUGE_INNER_DLEN;
|
|
}
|
|
OK (ilen <= HIO_DHCP6_MAX_OPT_DLEN, "the inner message fits an option length");
|
|
|
|
olen = put_relay_hdr(g_huge, HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
{
|
|
hio_uint16_t v;
|
|
v = hio_hton16(HIO_DHCP6_OPT_RELAY_MESSAGE); HIO_MEMCPY (&g_huge[olen], &v, 2);
|
|
v = hio_hton16((hio_uint16_t)ilen); HIO_MEMCPY (&g_huge[olen + 2], &v, 2);
|
|
olen += 4 + ilen;
|
|
}
|
|
/* the relay's own option, after the one it relays. an insert inside the
|
|
* relayed message has to shift this along, so a refused insert that moved
|
|
* the octets anyway is visible. */
|
|
olen += put_opt(&g_huge[olen], HIO_DHCP6_OPT_INTERFACE_ID, "eth0", 4);
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_huge;
|
|
pkt.len = olen;
|
|
pkt.capa = HUGE_CAPA;
|
|
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 1, "the large packet nests once");
|
|
rm = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm != HIO_NULL && opt_len(rm) == ilen, "and its relay option is at the top of the range");
|
|
o = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_USER_CLASS);
|
|
OK (o != HIO_NULL && opt_len(o) == HUGE_INNER_DLEN, "the large option inside it reads back");
|
|
|
|
snap_len = pkt.len;
|
|
HIO_MEMCPY (g_huge_snap, g_huge, snap_len);
|
|
|
|
/* the buffer has room; the enclosing option's length field does not */
|
|
OK (pkt.len + 4 + 64 <= pkt.capa, "the buffer could hold another option");
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 1, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, g_huge, 64) == HIO_NULL,
|
|
"but an insert the enclosing relay option could not describe is refused");
|
|
OK (opt_len(rm) == ilen, "its length did not wrap");
|
|
OK (pkt.len == snap_len && memcmp(g_huge_snap, g_huge, snap_len) == 0,
|
|
"and not one octet was moved");
|
|
|
|
/* and a replacement that grows past the same ceiling */
|
|
OK (hio_dhcp6_replace_option_at(&pkt, 1, o, 1, HIO_DHCP6_OPT_USER_CLASS, g_huge, HUGE_INNER_DLEN + 64) == HIO_NULL,
|
|
"a replacement past the ceiling is refused as well");
|
|
OK (opt_len(rm) == ilen, "with its length still intact");
|
|
OK (pkt.len == snap_len && memcmp(g_huge_snap, g_huge, snap_len) == 0, "and the packet unmoved");
|
|
|
|
/* one that stays inside it goes through, so the ceiling is a limit and
|
|
* not a blanket refusal */
|
|
OK (hio_dhcp6_insert_option_at(&pkt, 1, HIO_NULL, 1, HIO_DHCP6_OPT_PREFERENCE, g_huge, HIO_DHCP6_MAX_OPT_DLEN - (int)ilen - 4) != HIO_NULL,
|
|
"an insert that exactly reaches the ceiling is accepted");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID) != HIO_NULL,
|
|
"with the relay's own option shifted along intact");
|
|
rm = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
OK (rm != HIO_NULL && opt_len(rm) == HIO_DHCP6_MAX_OPT_DLEN, "filling the enclosing option exactly");
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 1, "and the packet still parses");
|
|
}
|
|
|
|
/* 'safe' exists because a position is just an address: nothing about it says
|
|
* which level it came from. with checking off, a caller may hand an edit at
|
|
* one level a position belonging to another - and then the option's own size
|
|
* has no relation to the enclosing relay option's length. this is the case
|
|
* where subtracting one from the other runs below zero, so it is where the
|
|
* enclosing length has to be examined before anything moves rather than after.
|
|
*/
|
|
static void test_position_from_another_level (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* rm;
|
|
hio_dhcp6_opt_hdr_t* outer_opt;
|
|
hio_oow_t olen;
|
|
|
|
/* RELAY-FORW { RELAY-MSG: a bare SOLICIT header } + a 20-octet option of
|
|
* the relay's own. the relayed message is thus far smaller than that
|
|
* option, which no position taken at the inner level could be. */
|
|
olen = put_relay_hdr(g_buf, HIO_DHCP6_MSG_RELAYFORW, 0);
|
|
{
|
|
hio_uint8_t inner[8];
|
|
hio_oow_t ilen = put_hdr(inner, HIO_DHCP6_MSG_SOLICIT);
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_RELAY_MESSAGE, inner, (hio_uint16_t)ilen);
|
|
}
|
|
olen += put_opt(&g_buf[olen], HIO_DHCP6_OPT_INTERFACE_ID, "0123456789abcdef", 16);
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = olen;
|
|
pkt.capa = BUFCAPA;
|
|
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 1, "the packet nests once");
|
|
rm = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_RELAY_MESSAGE);
|
|
outer_opt = hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID);
|
|
OK (rm != HIO_NULL && opt_len(rm) == 4, "carrying a message of four octets");
|
|
OK (outer_opt != HIO_NULL && opt_len(outer_opt) == 16, "beside an option of twenty");
|
|
snapshot (&pkt);
|
|
|
|
/* deleting the outer option as though it were at level 1 would take more
|
|
* out of the relay option than it holds */
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 1, outer_opt, 0) <= -1,
|
|
"an unchecked delete at the wrong level is refused");
|
|
OK (opt_len(rm) == 4, "the relay option's length is not wrapped below zero");
|
|
OK (unchanged(&pkt), "and the packet is not spliced");
|
|
|
|
/* and so would replacing it with something shorter */
|
|
OK (hio_dhcp6_replace_option_at(&pkt, 1, outer_opt, 0, HIO_DHCP6_OPT_INTERFACE_ID, HIO_NULL, 0) == HIO_NULL,
|
|
"an unchecked replace at the wrong level is refused as well");
|
|
OK (opt_len(rm) == 4, "with its length still intact");
|
|
OK (unchanged(&pkt), "and the packet still whole");
|
|
|
|
/* with checking on, the position is rejected for what it is */
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 1, outer_opt, 1) <= -1,
|
|
"and a checked delete refuses the position outright");
|
|
OK (unchanged(&pkt), "changing nothing either way");
|
|
}
|
|
|
|
/* the end of one level's options is not the end of the packet - level 0's own
|
|
* options come after everything nested inside it. so the position just past
|
|
* the last option of an inner level is an append point for that level and
|
|
* nothing else; treating it as an option there would edit the enclosing
|
|
* message instead. */
|
|
static void test_inner_append_point (void)
|
|
{
|
|
hio_dhcp6_pktbuf_t pkt;
|
|
hio_dhcp6_opt_hdr_t* o;
|
|
hio_dhcp6_opt_hdr_t* last;
|
|
hio_uint8_t* area_end;
|
|
hio_oow_t before;
|
|
|
|
pkt.hdr = (hio_dhcp6_pkt_hdr_t*)g_buf;
|
|
pkt.len = build_two_level_relay(g_buf);
|
|
pkt.capa = BUFCAPA;
|
|
|
|
/* walk level 1 to its last option and step past it */
|
|
last = HIO_NULL;
|
|
while ((o = hio_dhcp6_get_option_after((hio_dhcp6_pktinf_t*)&pkt, 1, last))) last = o;
|
|
OK (last != HIO_NULL, "level 1 has options to walk");
|
|
area_end = (hio_uint8_t*)last + 4 + opt_len(last);
|
|
OK (area_end < (hio_uint8_t*)pkt.hdr + pkt.len, "and its option area ends before the packet does");
|
|
|
|
/* level 0's own interface id sits at that address, so a delete which
|
|
* accepted it would take an option out of the wrong message */
|
|
before = pkt.len;
|
|
snapshot (&pkt);
|
|
OK (hio_dhcp6_delete_option_at(&pkt, 1, (hio_dhcp6_opt_hdr_t*)area_end, 1) <= -1,
|
|
"deleting at an inner level's append point is refused");
|
|
OK (unchanged(&pkt), "and the enclosing message keeps its option");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_INTERFACE_ID) != HIO_NULL,
|
|
"which is still where it was");
|
|
|
|
/* an insert there, on the other hand, is exactly how one appends to
|
|
* level 1 - and it must land at level 1, not level 0 */
|
|
o = hio_dhcp6_insert_option_at(&pkt, 1, (hio_dhcp6_opt_hdr_t*)area_end, 1, HIO_DHCP6_OPT_PREFERENCE, "p", 1);
|
|
OK (o != HIO_NULL, "inserting there appends to level 1");
|
|
OK (pkt.len == before + 5, "the packet grew");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 1, HIO_DHCP6_OPT_PREFERENCE) != HIO_NULL,
|
|
"and the new option is at level 1");
|
|
OK (hio_dhcp6_find_option((hio_dhcp6_pktinf_t*)&pkt, 0, HIO_DHCP6_OPT_PREFERENCE) == HIO_NULL,
|
|
"not at level 0");
|
|
OK (hio_dhcp6_get_relay_level((hio_dhcp6_pktinf_t*)&pkt) == 2, "and the chain still parses");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int main (void)
|
|
{
|
|
no_plan ();
|
|
|
|
test_layout ();
|
|
test_init_pktbuf ();
|
|
test_append_and_find ();
|
|
test_byte_order ();
|
|
test_walk_every_option ();
|
|
test_find_after ();
|
|
test_relay_level ();
|
|
test_find_per_level ();
|
|
test_malformed_relay ();
|
|
test_nesting_bound ();
|
|
test_innermost_of_a_broken_chain ();
|
|
test_insert_at_position ();
|
|
test_insert_limits ();
|
|
test_insert_bad_position ();
|
|
test_delete ();
|
|
test_delete_bad_position ();
|
|
test_replace ();
|
|
test_edit_inner_level ();
|
|
test_edit_middle_level ();
|
|
test_relay_length_bound ();
|
|
test_enclosing_length_ceiling ();
|
|
test_position_from_another_level ();
|
|
test_inner_append_point ();
|
|
|
|
return exit_status();
|
|
}
|