/* * 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 #include #include "tap.h" #include #include #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(); }