/* * the dhcpv4 server service. * * the state machine is hio_svc_dhcs_process(), a function from a request to a * reply that does no I/O, so the protocol is driven here by handing it packets * rather than by standing up a network. that is the point of exposing it: the * parts of DHCP that are easy to get wrong are the state transitions and the * lease bookkeeping, and neither needs a socket to exercise. * * written against RFC 2131. the cases follow the transitions it defines rather * than the shape of the implementation. */ #include #include #include "tap.h" #include #include #include #define POOL_FIRST 0x0a000064u /* 10.0.0.100 */ #define POOL_LAST 0x0a000066u /* 10.0.0.102 - three addresses, so exhaustion is reachable */ #define SERVER_ID 0x0a000001u /* 10.0.0.1 */ #define NETMASK 0xffffff00u #define ROUTER 0x0a000001u #define LEASE_SECS 600 /* mirrors PURGE_INTERVAL_SECS in dhcp-svr.c, which is private to it */ #define PURGE_INTERVAL_SECS_FOR_TEST 60 /* a pass-through allocator that can be armed to fail once. the server records * a lease for every address it hands out, and a lease it fails to record is an * address it will not remember issuing - so what it does when an allocation * fails is part of the protocol behaviour, not an incidental detail. */ static int g_fail_next_alloc; static int g_failed_allocs; static void* fi_alloc (hio_mmgr_t* mmgr, hio_oow_t n) { if (g_fail_next_alloc) { g_fail_next_alloc = 0; g_failed_allocs++; return HIO_NULL; } return malloc(n); } static void* fi_realloc (hio_mmgr_t* mmgr, void* ptr, hio_oow_t n) { if (g_fail_next_alloc) { g_fail_next_alloc = 0; g_failed_allocs++; return HIO_NULL; } return realloc(ptr, n); } static void fi_free (hio_mmgr_t* mmgr, void* ptr) { free (ptr); } static hio_mmgr_t g_fi_mmgr = { fi_alloc, fi_realloc, fi_free, HIO_NULL }; static hio_t* g_hio = HIO_NULL; static hio_svc_dhcs_t* g_dhcs = HIO_NULL; static hio_uint8_t g_reqbuf[576]; static hio_uint8_t g_repbuf[576]; static void quiet_logging (hio_t* hio) { hio_bitmask_t mask = HIO_LOG_FATAL | HIO_LOG_ALL_TYPES; hio_setoption (hio, HIO_LOG_MASK, &mask); } static int start_server (void) { hio_svc_dhcs_cfg_t cfg; HIO_MEMSET (&cfg, 0, HIO_SIZEOF(cfg)); if (hio_bcstrtoskad(g_hio, "0.0.0.0:0", &cfg.bind_addr) <= -1) return -1; cfg.server_id = SERVER_ID; cfg.pool_first = POOL_FIRST; cfg.pool_last = POOL_LAST; cfg.netmask = NETMASK; cfg.router = ROUTER; cfg.dns1 = 0x08080808u; cfg.lease_secs = LEASE_SECS; cfg.domain = "example.test"; g_dhcs = hio_svc_dhcs_start(g_hio, &cfg); return g_dhcs? 0: -1; } static void stop_server (void) { if (g_dhcs) { hio_svc_dhcs_stop (g_dhcs); g_dhcs = HIO_NULL; } hio_exec (g_hio); } /* build a request from one client, identified by the last octet of its mac */ static void build (hio_dhcp4_pktbuf_t* pkt, hio_uint8_t mtype, hio_uint8_t client, hio_uint32_t requested, hio_uint32_t server_id, hio_uint32_t ciaddr) { hio_uint8_t mac[6] = { 0x02, 0, 0, 0, 0, 0 }; mac[5] = client; hio_dhcp4_init_pktbuf (pkt, g_reqbuf, HIO_SIZEOF(g_reqbuf)); pkt->hdr->op = HIO_DHCP4_OP_BOOTREQUEST; pkt->hdr->htype = HIO_DHCP4_HTYPE_ETHERNET; pkt->hdr->hlen = 6; HIO_MEMCPY (pkt->hdr->chaddr, mac, 6); pkt->hdr->xid = hio_hton32(0xABCD0000u | client); pkt->hdr->ciaddr = hio_hton32(ciaddr); hio_dhcp4_add_option_uint8 (pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, mtype); if (requested) hio_dhcp4_add_option_uint32 (pkt, HIO_DHCP4_OPT_REQUESTED_IPADDR, requested); if (server_id) hio_dhcp4_add_option_uint32 (pkt, HIO_DHCP4_OPT_SERVER_ID, server_id); hio_dhcp4_add_option (pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0); } /* the fields a request may carry beyond what build() takes. zero throughout * means "as build() would have made it", so a case names only what it cares * about. */ struct reqx_t { hio_uint32_t giaddr; /* non-zero: the request came through a relay */ hio_uint16_t flags; /* host order; HIO_DHCP4_FLAG_BROADCAST */ hio_uint32_t lease_req; /* option 51 in the request */ const hio_uint8_t* prl; /* option 55 */ hio_uint8_t prl_len; }; typedef struct reqx_t reqx_t; /* build(), plus the extras. the option order matters to the parameter request * list cases, so the extras go in before END like any other option. */ static void build_x (hio_dhcp4_pktbuf_t* pkt, hio_uint8_t mtype, hio_uint8_t client, hio_uint32_t requested, hio_uint32_t server_id, hio_uint32_t ciaddr, const reqx_t* x) { hio_uint8_t mac[6] = { 0x02, 0, 0, 0, 0, 0 }; mac[5] = client; hio_dhcp4_init_pktbuf (pkt, g_reqbuf, HIO_SIZEOF(g_reqbuf)); pkt->hdr->op = HIO_DHCP4_OP_BOOTREQUEST; pkt->hdr->htype = HIO_DHCP4_HTYPE_ETHERNET; pkt->hdr->hlen = 6; HIO_MEMCPY (pkt->hdr->chaddr, mac, 6); pkt->hdr->xid = hio_hton32(0xABCD0000u | client); pkt->hdr->ciaddr = hio_hton32(ciaddr); pkt->hdr->giaddr = hio_hton32(x->giaddr); pkt->hdr->flags = hio_hton16(x->flags); hio_dhcp4_add_option_uint8 (pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, mtype); if (requested) hio_dhcp4_add_option_uint32 (pkt, HIO_DHCP4_OPT_REQUESTED_IPADDR, requested); if (server_id) hio_dhcp4_add_option_uint32 (pkt, HIO_DHCP4_OPT_SERVER_ID, server_id); if (x->lease_req) hio_dhcp4_add_option_uint32 (pkt, HIO_DHCP4_OPT_LEASE_TIME, x->lease_req); if (x->prl && x->prl_len > 0) hio_dhcp4_add_option (pkt, HIO_DHCP4_OPT_PARAM_REQ, (void*)x->prl, x->prl_len); hio_dhcp4_add_option (pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0); } /* run one request and hand back the whole reply and its destination, which is * what the routing and option-list cases need to look at */ static int run_x (hio_uint8_t mtype, hio_uint8_t client, hio_uint32_t requested, hio_uint32_t server_id, hio_uint32_t ciaddr, const reqx_t* x, hio_dhcp4_pktinf_t* out_rep, hio_skad_t* out_dst) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t reqinf; hio_skad_t dst; int n; build_x (&req, mtype, client, requested, server_id, ciaddr, x); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); n = hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst); if (out_rep) { out_rep->hdr = rep.hdr; out_rep->len = rep.len; } if (out_dst) *out_dst = dst; return n; } /* the destination as a host-order address and a port, for comparison */ static void dst_of (const hio_skad_t* dst, hio_uint32_t* ip, hio_uint16_t* port) { hio_uint32_t v = 0; hio_skad_get_ipad_bytes (dst, &v, HIO_SIZEOF(v)); *ip = hio_ntoh32(v); *port = hio_skad_get_port(dst); } /* run one request through the server. returns what process() returned, and * fills in the reply's message type and offered address when there is one. */ static int run (hio_uint8_t mtype, hio_uint8_t client, hio_uint32_t requested, hio_uint32_t server_id, hio_uint32_t ciaddr, hio_uint8_t* out_mtype, hio_uint32_t* out_yiaddr) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t reqinf, repinf; hio_skad_t dst; int n; build (&req, mtype, client, requested, server_id, ciaddr); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); n = hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst); if (n == 1) { repinf.hdr = rep.hdr; repinf.len = rep.len; if (out_mtype) hio_dhcp4_get_msg_type (&repinf, out_mtype); if (out_yiaddr) *out_yiaddr = hio_ntoh32(rep.hdr->yiaddr); } return n; } /* ------------------------------------------------------------------ */ static void test_discover_offers (void) { hio_uint8_t mt = 0; hio_uint32_t ip = 0; OK (run(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &mt, &ip) == 1, "a DISCOVER is answered"); OK (mt == HIO_DHCP4_MSG_OFFER, "and the answer is an OFFER"); OK (ip >= POOL_FIRST && ip <= POOL_LAST, "carrying an address from the pool"); /* the offer is recorded, or a second client discovering before the first * requests would be offered the same address */ OK (hio_svc_dhcs_getleasecount(g_dhcs) == 1, "and the offer is recorded as a lease"); } static void test_offer_is_held_against_other_clients (void) { hio_uint32_t ip1 = 0, ip2 = 0; run (HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, HIO_NULL, &ip1); run (HIO_DHCP4_MSG_DISCOVER, 2, 0, 0, 0, HIO_NULL, &ip2); OK (ip1 != 0 && ip2 != 0 && ip1 != ip2, "two clients discovering at once are offered different addresses"); } static void test_discover_is_idempotent (void) { hio_uint32_t a = 0, b = 0; run (HIO_DHCP4_MSG_DISCOVER, 3, 0, 0, 0, HIO_NULL, &a); run (HIO_DHCP4_MSG_DISCOVER, 3, 0, 0, 0, HIO_NULL, &b); /* a client that discovers twice is not two clients, and must not consume * two addresses out of the pool */ OK (a != 0 && a == b, "a client discovering twice is offered the same address twice"); } static void test_request_acks_and_binds (void) { hio_uint8_t mt = 0; hio_uint32_t offered = 0, acked = 0; hio_svc_dhcs_lease_t lease; run (HIO_DHCP4_MSG_DISCOVER, 4, 0, 0, 0, HIO_NULL, &offered); OK (run(HIO_DHCP4_MSG_REQUEST, 4, offered, SERVER_ID, 0, &mt, &acked) == 1, "a REQUEST naming this server is answered"); OK (mt == HIO_DHCP4_MSG_ACK && acked == offered, "with an ACK for the address that was offered"); /* and the lease moves from offered to bound, which is what makes it * survive longer than the brief offer hold */ { hio_oow_t i, n = hio_svc_dhcs_getleasecount(g_dhcs); int found = 0; for (i = 0; i < n; i++) { if (hio_svc_dhcs_getlease(g_dhcs, i, &lease) == 0 && lease.ipaddr == acked && lease.state == HIO_SVC_DHCS_LEASE_BOUND) found = 1; } OK (found, "and the lease is recorded as bound"); } } static void test_request_for_another_server_is_ignored (void) { hio_uint32_t offered = 0; hio_oow_t before, after; run (HIO_DHCP4_MSG_DISCOVER, 5, 0, 0, 0, HIO_NULL, &offered); before = hio_svc_dhcs_getleasecount(g_dhcs); OK (run(HIO_DHCP4_MSG_REQUEST, 5, offered, SERVER_ID + 1, 0, HIO_NULL, HIO_NULL) == 0, "a REQUEST that selected another server draws no reply"); /* and the address we offered goes back to the pool at once rather than * being held until the offer lapses - the client has told us it went * elsewhere */ after = hio_svc_dhcs_getleasecount(g_dhcs); OK (after == before - 1, "and the address offered to it is released immediately"); } static void test_request_outside_pool_is_naked (void) { hio_uint8_t mt = 0; OK (run(HIO_DHCP4_MSG_REQUEST, 6, 0x0b000001u, SERVER_ID, 0, &mt, HIO_NULL) == 1 && mt == HIO_DHCP4_MSG_NAK, "a REQUEST for an address outside the pool is answered with a NAK"); } static void test_request_for_someone_elses_address_is_naked (void) { hio_uint8_t mt = 0; hio_uint32_t theirs = 0; /* client 7 takes an address */ run (HIO_DHCP4_MSG_DISCOVER, 7, 0, 0, 0, HIO_NULL, &theirs); run (HIO_DHCP4_MSG_REQUEST, 7, theirs, SERVER_ID, 0, HIO_NULL, HIO_NULL); /* client 8 asks for it. handing it over would put two clients on one * address, so the answer has to be no. */ OK (run(HIO_DHCP4_MSG_REQUEST, 8, theirs, SERVER_ID, 0, &mt, HIO_NULL) == 1 && mt == HIO_DHCP4_MSG_NAK, "a REQUEST for an address another client holds is answered with a NAK"); } static void test_renewal (void) { hio_uint8_t mt = 0; hio_uint32_t bound = 0, acked = 0; run (HIO_DHCP4_MSG_DISCOVER, 9, 0, 0, 0, HIO_NULL, &bound); run (HIO_DHCP4_MSG_REQUEST, 9, bound, SERVER_ID, 0, HIO_NULL, HIO_NULL); /* a renewal carries no requested-address option and no server id - just * ciaddr, because the client already has the address and is only asking * for more time on it */ OK (run(HIO_DHCP4_MSG_REQUEST, 9, 0, 0, bound, &mt, &acked) == 1 && mt == HIO_DHCP4_MSG_ACK && acked == bound, "a renewal identified only by ciaddr is acknowledged for the same address"); } static void test_release_frees_the_address (void) { hio_uint32_t ip = 0; hio_oow_t before, after; run (HIO_DHCP4_MSG_DISCOVER, 10, 0, 0, 0, HIO_NULL, &ip); run (HIO_DHCP4_MSG_REQUEST, 10, ip, SERVER_ID, 0, HIO_NULL, HIO_NULL); before = hio_svc_dhcs_getleasecount(g_dhcs); OK (run(HIO_DHCP4_MSG_RELEASE, 10, 0, 0, ip, HIO_NULL, HIO_NULL) == 0, "a RELEASE draws no reply"); after = hio_svc_dhcs_getleasecount(g_dhcs); OK (after == before - 1, "and gives the address back"); } static void test_decline_takes_the_address_out_of_service (void) { hio_uint32_t ip = 0; hio_svc_dhcs_lease_t lease; hio_oow_t i, n; int declined = 0; run (HIO_DHCP4_MSG_DISCOVER, 11, 0, 0, 0, HIO_NULL, &ip); OK (run(HIO_DHCP4_MSG_DECLINE, 11, ip, SERVER_ID, 0, HIO_NULL, HIO_NULL) == 0, "a DECLINE draws no reply"); n = hio_svc_dhcs_getleasecount(g_dhcs); for (i = 0; i < n; i++) { if (hio_svc_dhcs_getlease(g_dhcs, i, &lease) == 0 && lease.ipaddr == ip && lease.state == HIO_SVC_DHCS_LEASE_DECLINED) declined = 1; } OK (declined, "and the address is marked declined rather than freed"); /* the point of that distinction: freeing it would hand the same address * to the next client, which would find the same conflict */ { hio_uint32_t other = 0; run (HIO_DHCP4_MSG_DISCOVER, 12, 0, 0, 0, HIO_NULL, &other); OK (other != ip, "so the next client is not offered it"); } } static void test_inform_returns_options_without_a_lease (void) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t reqinf, repinf; hio_skad_t dst; hio_uint8_t mt = 0; hio_uint32_t nm = 0; hio_oow_t before; before = hio_svc_dhcs_getleasecount(g_dhcs); build (&req, HIO_DHCP4_MSG_INFORM, 13, 0, 0, 0x0a0000c8u); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); OK (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) == 1, "an INFORM is answered"); repinf.hdr = rep.hdr; repinf.len = rep.len; OK (hio_dhcp4_get_msg_type(&repinf, &mt) == 0 && mt == HIO_DHCP4_MSG_ACK, "with an ACK"); OK (hio_dhcp4_get_option_uint32(&repinf, HIO_DHCP4_OPT_SUBNET, &nm) == 0 && nm == NETMASK, "carrying the configured options"); /* an INFORM is a client that already has an address by other means, so * there is nothing to lease and yiaddr stays empty */ OK (rep.hdr->yiaddr == 0, "but no address"); OK (hio_dhcp4_get_option_uint32(&repinf, HIO_DHCP4_OPT_LEASE_TIME, &nm) <= -1, "and no lease time"); OK (hio_svc_dhcs_getleasecount(g_dhcs) == before, "and no lease is recorded"); } static void test_offer_and_ack_agree (void) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t reqinf, repinf; hio_skad_t dst; hio_uint32_t off_nm = 0, off_rt = 0, off_lt = 0, offered = 0; hio_uint32_t ack_nm = 0, ack_rt = 0, ack_lt = 0; /* an offer that promises something the acknowledgement then contradicts * is a bug a client experiences as a misconfigured network, so the two * are compared field by field */ build (&req, HIO_DHCP4_MSG_DISCOVER, 14, 0, 0, 0); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); if (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) != 1) { skip ("no offer", 1); return; } repinf.hdr = rep.hdr; repinf.len = rep.len; offered = hio_ntoh32(rep.hdr->yiaddr); hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_SUBNET, &off_nm); hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_ROUTER, &off_rt); hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_LEASE_TIME, &off_lt); build (&req, HIO_DHCP4_MSG_REQUEST, 14, offered, SERVER_ID, 0); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); if (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) != 1) { skip ("no ack", 1); return; } repinf.hdr = rep.hdr; repinf.len = rep.len; hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_SUBNET, &ack_nm); hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_ROUTER, &ack_rt); hio_dhcp4_get_option_uint32 (&repinf, HIO_DHCP4_OPT_LEASE_TIME, &ack_lt); OK (off_nm == ack_nm && off_rt == ack_rt && off_lt == ack_lt && off_nm == NETMASK && off_rt == ROUTER && off_lt == LEASE_SECS, "an OFFER and the ACK that follows it describe the same network"); } static void test_pool_exhaustion_is_silence (void) { hio_uint8_t c; hio_uint32_t ip = 0; int offers = 0; /* the pool holds three addresses. binding all of them and then asking * for a fourth must not produce a fourth address, and must not produce * an error either - another server on the segment may be able to help, * so silence is the correct answer. */ for (c = 20; c < 23; c++) { if (run(HIO_DHCP4_MSG_DISCOVER, c, 0, 0, 0, HIO_NULL, &ip) == 1 && ip != 0) { run (HIO_DHCP4_MSG_REQUEST, c, ip, SERVER_ID, 0, HIO_NULL, HIO_NULL); offers++; } } OK (offers == 3, "every address in the pool can be bound"); OK (run(HIO_DHCP4_MSG_DISCOVER, 23, 0, 0, 0, HIO_NULL, HIO_NULL) == 0, "and a client arriving at an exhausted pool is answered with silence"); } static void test_malformed_and_foreign_packets (void) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t reqinf; hio_skad_t dst; rep.hdr = (hio_dhcp4_pkt_hdr_t*)g_repbuf; rep.len = 0; rep.capa = HIO_SIZEOF(g_repbuf); /* a truncated datagram. a dhcp server is exposed to whatever is on the * segment, so this has to be an error it reports rather than one it * reads past. */ build (&req, HIO_DHCP4_MSG_DISCOVER, 30, 0, 0, 0); reqinf.hdr = req.hdr; reqinf.len = 4; OK (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) <= -1, "a truncated packet is refused"); /* a reply, not a request - this server's own words coming back at it * because it is bound to a broadcast address */ build (&req, HIO_DHCP4_MSG_OFFER, 30, 0, 0, 0); req.hdr->op = HIO_DHCP4_OP_BOOTREPLY; reqinf.hdr = req.hdr; reqinf.len = req.len; rep.len = 0; OK (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) == 0, "and a packet marked as a reply is passed over in silence"); /* no option 53 at all is bootp, which this server does not serve */ hio_dhcp4_init_pktbuf (&req, g_reqbuf, HIO_SIZEOF(g_reqbuf)); req.hdr->op = HIO_DHCP4_OP_BOOTREQUEST; req.hdr->htype = HIO_DHCP4_HTYPE_ETHERNET; req.hdr->hlen = 6; hio_dhcp4_add_option_uint8 (&req, HIO_DHCP4_OPT_IP_TTL, 64); reqinf.hdr = req.hdr; reqinf.len = req.len; rep.len = 0; OK (hio_svc_dhcs_process(g_dhcs, &reqinf, &rep, &dst) == 0, "and a bootp packet with no message type is passed over too"); } static void test_config_refusals (void) { hio_svc_dhcs_cfg_t cfg; hio_svc_dhcs_t* d; HIO_MEMSET (&cfg, 0, HIO_SIZEOF(cfg)); hio_bcstrtoskad (g_hio, "0.0.0.0:0", &cfg.bind_addr); cfg.server_id = SERVER_ID; cfg.pool_first = POOL_LAST; cfg.pool_last = POOL_FIRST; /* inverted */ d = hio_svc_dhcs_start(g_hio, &cfg); OK (!d && hio_geterrnum(g_hio) == HIO_EINVAL, "an inverted pool is refused"); if (d) hio_svc_dhcs_stop (d); /* a pool the size of the address space would be an allocation nobody * intended, so the bound is a refusal rather than a clamp */ cfg.pool_first = 0x0a000000u; cfg.pool_last = 0x0affffffu; d = hio_svc_dhcs_start(g_hio, &cfg); OK (!d && hio_geterrnum(g_hio) == HIO_EINVAL, "and so is one larger than the bound"); if (d) hio_svc_dhcs_stop (d); /* without option 54 a client cannot tell this server's replies from * another's, so it is required rather than defaulted */ cfg.pool_first = POOL_FIRST; cfg.pool_last = POOL_LAST; cfg.server_id = 0; d = hio_svc_dhcs_start(g_hio, &cfg); OK (!d && hio_geterrnum(g_hio) == HIO_EINVAL, "and a missing server identifier is refused"); if (d) hio_svc_dhcs_stop (d); /* dhcpv6 is a different protocol, not this one over another family */ cfg.server_id = SERVER_ID; hio_bcstrtoskad (g_hio, "[::]:0", &cfg.bind_addr); d = hio_svc_dhcs_start(g_hio, &cfg); OK (!d, "and an ipv6 bind address is refused by the v4 server"); if (d) hio_svc_dhcs_stop (d); } static void test_lease_allocation_failure (void) { hio_uint8_t mt = 0; hio_uint32_t ip = 0; hio_oow_t before; int rc; before = hio_svc_dhcs_getleasecount(g_dhcs); /* the first allocation the DISCOVER path reaches is the copy of the * client identifier that the lease will own. with that refused, the * server must not offer an address: an address offered but not recorded * would be handed to the next client as well. */ g_failed_allocs = 0; g_fail_next_alloc = 1; rc = run(HIO_DHCP4_MSG_DISCOVER, 40, 0, 0, 0, &mt, &ip); g_fail_next_alloc = 0; OK (g_failed_allocs == 1, "the injected allocation failure was reached"); OK (rc <= -1, "a DISCOVER whose lease cannot be recorded is reported as an error"); OK (hio_svc_dhcs_getleasecount(g_dhcs) == before, "and records no lease rather than a half-built one"); /* and the server is still serving afterwards - a failed allocation is not * allowed to leave it in a state where nothing works */ OK (run(HIO_DHCP4_MSG_DISCOVER, 41, 0, 0, 0, &mt, &ip) == 1 && mt == HIO_DHCP4_MSG_OFFER && ip != 0, "and the next client is served normally"); } static void test_request_allocation_failure_naks (void) { hio_uint8_t mt = 0; int rc; /* a REQUEST is different from a DISCOVER: the client is waiting for a * yes or a no, and silence would leave it retrying. so when the lease * cannot be recorded the answer is a NAK - which tells the client to * start over - rather than an error the client never hears about. */ g_failed_allocs = 0; g_fail_next_alloc = 1; rc = run(HIO_DHCP4_MSG_REQUEST, 42, POOL_FIRST, SERVER_ID, 0, &mt, HIO_NULL); g_fail_next_alloc = 0; OK (g_failed_allocs == 1, "the injected failure was reached on the request path"); OK (rc == 1 && mt == HIO_DHCP4_MSG_NAK, "a REQUEST whose lease cannot be recorded is answered with a NAK, not silence"); } static void test_start_allocation_failure (void) { hio_svc_dhcs_cfg_t cfg; hio_svc_dhcs_t* d; HIO_MEMSET (&cfg, 0, HIO_SIZEOF(cfg)); hio_bcstrtoskad (g_hio, "0.0.0.0:0", &cfg.bind_addr); cfg.server_id = SERVER_ID; cfg.pool_first = POOL_FIRST; cfg.pool_last = POOL_LAST; cfg.domain = "example.test"; /* starting the service allocates the service, a copy of the domain name * and a socket. failing the first of those must not leave anything * behind - which is what the leak checker over this test really decides. */ g_failed_allocs = 0; g_fail_next_alloc = 1; d = hio_svc_dhcs_start(g_hio, &cfg); g_fail_next_alloc = 0; OK (g_failed_allocs == 1 && !d, "a service whose first allocation fails does not start"); if (d) hio_svc_dhcs_stop (d); /* and starting normally afterwards still works */ d = hio_svc_dhcs_start(g_hio, &cfg); OK (d != HIO_NULL, "and the next attempt starts normally"); if (d) hio_svc_dhcs_stop (d); } /* ------------------------------------------------------------------ */ /* each group starts from a clean server so one group's leases cannot decide * another group's outcome */ /* RFC 2131 section 4.3.2: a NAK to a request that arrived through a relay goes * back to the relay, not onto a segment this server cannot reach - and with * the broadcast bit set, since the client must not be unicast at an address * the NAK is about to tell it not to use. */ static void test_relayed_nak_returns_via_the_relay (void) { reqx_t x; hio_dhcp4_pktinf_t rep; hio_skad_t dst; hio_uint32_t ip = 0; hio_uint16_t port = 0; hio_uint8_t mt = 0; HIO_MEMSET (&x, 0, HIO_SIZEOF(x)); /* first without a relay, for the contrast */ OK (run_x(HIO_DHCP4_MSG_REQUEST, 1, 0x0a00000Bu, 0, 0, &x, &rep, &dst) == 1, "an out-of-pool request is answered"); hio_dhcp4_get_msg_type (&rep, &mt); OK (mt == HIO_DHCP4_MSG_NAK, "with a nak"); dst_of (&dst, &ip, &port); OK (ip == 0xFFFFFFFFu && port == HIO_DHCP4_CLIENT_PORT, "broadcast to the client port when no relay is involved"); /* now through one */ x.giaddr = 0x0a000009u; OK (run_x(HIO_DHCP4_MSG_REQUEST, 2, 0x0a00000Bu, 0, 0, &x, &rep, &dst) == 1, "a relayed out-of-pool request is answered too"); hio_dhcp4_get_msg_type (&rep, &mt); OK (mt == HIO_DHCP4_MSG_NAK, "also with a nak"); dst_of (&dst, &ip, &port); OK (ip == 0x0a000009u, "sent to the relay, not to the segment"); OK (port == HIO_DHCP4_SERVER_PORT, "and to its server port, not the client port"); OK ((hio_ntoh16(rep.hdr->flags) & HIO_DHCP4_FLAG_BROADCAST) != 0, "with the broadcast bit set for the relay to act on"); OK (rep.hdr->giaddr == hio_hton32(0x0a000009u), "and giaddr echoed so the relay can route it"); } /* a client that cannot receive a unicast says so in the flags, and that * overrides ciaddr as the place to send the reply */ static void test_broadcast_flag_overrides_ciaddr (void) { reqx_t x; hio_skad_t dst; hio_uint32_t ip = 0; hio_uint16_t port = 0; HIO_MEMSET (&x, 0, HIO_SIZEOF(x)); /* bind an address first, so the renewal below is of something real */ OK (run_x(HIO_DHCP4_MSG_REQUEST, 3, POOL_FIRST, 0, 0, &x, HIO_NULL, HIO_NULL) == 1, "an address is bound"); /* renewing from that address, without the flag: unicast to it */ OK (run_x(HIO_DHCP4_MSG_REQUEST, 3, 0, 0, POOL_FIRST, &x, HIO_NULL, &dst) == 1, "the renewal is acked"); dst_of (&dst, &ip, &port); OK (ip == POOL_FIRST && port == HIO_DHCP4_CLIENT_PORT, "and unicast to the address it renewed"); /* the same renewal with the flag set: broadcast instead */ x.flags = HIO_DHCP4_FLAG_BROADCAST; OK (run_x(HIO_DHCP4_MSG_REQUEST, 3, 0, 0, POOL_FIRST, &x, HIO_NULL, &dst) == 1, "and again with the flag"); dst_of (&dst, &ip, &port); OK (ip == 0xFFFFFFFFu, "broadcast, because the client said it cannot take a unicast"); OK (port == HIO_DHCP4_CLIENT_PORT, "still to the client port"); } /* the codes a reply carries, in the order it carries them. the walker takes * no context argument, so the result lands here. */ static int g_codes[64]; static int g_ncodes; static int collect_code (hio_dhcp4_opt_hdr_t* o) { if (g_ncodes < (int)HIO_COUNTOF(g_codes)) g_codes[g_ncodes] = o->code; g_ncodes++; return 1; /* non-zero continues the walk */ } static void walk_reply (const hio_dhcp4_pktinf_t* rep) { g_ncodes = 0; hio_dhcp4_walk_options (rep, collect_code); } static int count_option (int code) { int i, n = 0; for (i = 0; i < g_ncodes; i++) if (g_codes[i] == code) n++; return n; } /* where 'code' first appears in the reply, or -1 */ static int index_of_option (int code) { int i; for (i = 0; i < g_ncodes; i++) if (g_codes[i] == code) return i; return -1; } /* RFC 2132 section 9.8: the client lists what it wants and the server returns * what of that it can supply. what it must send regardless - the server * identifier and the lease times - is not the client's to ask for. */ static void test_parameter_request_list (void) { static const hio_uint8_t two[] = { HIO_DHCP4_OPT_ROUTER, HIO_DHCP4_OPT_SUBNET }; static const hio_uint8_t dup[] = { HIO_DHCP4_OPT_ROUTER, HIO_DHCP4_OPT_ROUTER, HIO_DHCP4_OPT_ROUTER }; static const hio_uint8_t unknown[] = { 0xF0, 0xF1 }; /* one client throughout: a repeat discover from the same client is * idempotent, so these cases exercise the option list and not the pool, * which holds only three addresses. */ reqx_t x; hio_dhcp4_pktinf_t rep; hio_uint32_t v = 0; /* no list: everything configured comes back */ HIO_MEMSET (&x, 0, HIO_SIZEOF(x)); OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover with no list"); walk_reply (&rep); OK (count_option(HIO_DHCP4_OPT_SUBNET) == 1, "the netmask is offered"); OK (count_option(HIO_DHCP4_OPT_ROUTER) == 1, "and the router"); OK (count_option(HIO_DHCP4_OPT_SERVER_ID) == 1, "and the server identifier"); OK (count_option(HIO_DHCP4_OPT_LEASE_TIME) == 1, "and the lease time"); /* a list of two: those two, and not the rest */ x.prl = two; x.prl_len = (hio_uint8_t)HIO_COUNTOF(two); OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover naming two options"); walk_reply (&rep); OK (count_option(HIO_DHCP4_OPT_ROUTER) == 1, "the router is there"); OK (count_option(HIO_DHCP4_OPT_SUBNET) == 1, "and the netmask"); OK (count_option(HIO_DHCP4_OPT_DNS_SERVER) == 0, "the dns servers are not, having not been asked for"); OK (count_option(HIO_DHCP4_OPT_DOMAIN_NAME) == 0, "nor the domain name"); OK (count_option(HIO_DHCP4_OPT_SERVER_ID) == 1, "the server identifier comes regardless"); OK (count_option(HIO_DHCP4_OPT_LEASE_TIME) == 1, "as does the lease time"); /* and in the order asked: router before netmask, which is not the order * the server would have chosen on its own */ OK (index_of_option(HIO_DHCP4_OPT_ROUTER) >= 0, "the router option is found"); OK (index_of_option(HIO_DHCP4_OPT_ROUTER) < index_of_option(HIO_DHCP4_OPT_SUBNET), "and the netmask comes after it, in the order the client listed rather than the server's own"); /* a code named three times is sent once: a reply carrying it twice is * ambiguous, since clients differ on which one wins */ x.prl = dup; x.prl_len = (hio_uint8_t)HIO_COUNTOF(dup); OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover naming one option three times"); walk_reply (&rep); OK (count_option(HIO_DHCP4_OPT_ROUTER) == 1, "gets it once"); /* codes nothing is configured for are skipped, not answered with zeros */ x.prl = unknown; x.prl_len = (hio_uint8_t)HIO_COUNTOF(unknown); OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover naming options this server has no value for"); walk_reply (&rep); OK (count_option(0xF0) == 0 && count_option(0xF1) == 0, "which are simply absent"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_SERVER_ID, &v) == 0 && v == SERVER_ID, "while the reply is still usable"); } /* a client may ask for a shorter lease than the server's, and gets it. it may * not ask for a longer one - the configuration is a limit. */ static void test_requested_lease_time (void) { reqx_t x; hio_dhcp4_pktinf_t rep; hio_uint32_t secs = 0, t1 = 0, t2 = 0, mine = 0; hio_svc_dhcs_lease_t lease; hio_ntime_t now; hio_ntime_t left; /* nothing asked: the configured lease */ HIO_MEMSET (&x, 0, HIO_SIZEOF(x)); OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover asking for no particular lease"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_LEASE_TIME, &secs) == 0 && secs == LEASE_SECS, "is offered the configured lease"); /* shorter: honoured, and T1/T2 follow it rather than the configured one */ x.lease_req = 60; OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover asking for a minute"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_LEASE_TIME, &secs) == 0 && secs == 60, "gets a minute"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_T1, &t1) == 0 && t1 == 30, "with T1 at half of it"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_T2, &t2) == 0 && t2 == (60 / 8) * 7, "and T2 at seven eighths"); /* longer than allowed: capped at the configuration */ x.lease_req = 999999; OK (run_x(HIO_DHCP4_MSG_DISCOVER, 1, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a discover asking for far too long"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_LEASE_TIME, &secs) == 0 && secs == LEASE_SECS, "is held to the configured lease"); /* and what is recorded matches what was advertised, which is the point of * settling the duration once. the address requested is the one this * client was offered, not a guess at which the pool would hand out. */ x.lease_req = 60; OK (run_x(HIO_DHCP4_MSG_DISCOVER, 2, 0, 0, 0, &x, &rep, HIO_NULL) == 1, "a second client discovers"); mine = hio_ntoh32(rep.hdr->yiaddr); OK (mine != 0, "and is offered an address"); OK (run_x(HIO_DHCP4_MSG_REQUEST, 2, mine, 0, 0, &x, &rep, HIO_NULL) == 1, "then requests it, asking for a minute"); OK (hio_dhcp4_get_option_uint32(&rep, HIO_DHCP4_OPT_LEASE_TIME, &secs) == 0 && secs == 60, "and is acked for a minute"); /* found by address, not by index: the discovers above left offered leases * of their own, and index 0 is one of those */ { hio_oow_t i, n = hio_svc_dhcs_getleasecount(g_dhcs); int found = 0; for (i = 0; i < n; i++) { if (hio_svc_dhcs_getlease(g_dhcs, i, &lease) == 0 && lease.ipaddr == mine && lease.state == HIO_SVC_DHCS_LEASE_BOUND) { found = 1; break; } } OK (found, "with a bound lease recorded against it"); if (found) { hio_gettime (g_hio, &now); HIO_SUB_NTIME (&left, &lease.expiry, &now); OK (left.sec <= 60 && left.sec >= 55, "expiring a minute out, not ten - what was recorded is what the client was told"); } else OK (0, "(no lease to check the expiry of)"); } } /* the sweep that keeps a pool nobody exhausts from remembering every client it * has ever seen. fired with an explicit clock rather than waited for. */ static void test_purge_timer (void) { hio_ntime_t due, ahead; hio_oow_t before, fired = 0; before = g_hio->tmr.size; OK (before >= 1, "starting the service scheduled a sweep"); OK (before == 1, "exactly one, so jobs[0] below is it"); /* fire it at exactly its due moment, not past it. * * hio_firetmrjobs() drains every job whose time has come, in an uncapped * loop, so a handler that reschedules to a moment at or before the clock * it was fired at spins there forever. firing at the due moment leaves * the reschedule a hair ahead whatever the handler measures from, so the * loop always exits and the interval can be inspected instead - which * makes a regression a failed assertion rather than a hung suite. */ due = g_hio->tmr.jobs[0].when; hio_firetmrjobs (g_hio, &due, &fired); OK (fired == 1, "the sweep fired once"); OK (g_hio->tmr.size == before, "and left another scheduled behind"); /* a full interval ahead of the clock it was fired at. a handler that * measured from the real clock instead would leave one due almost * immediately, which is drift in production and a spin under a simulated * clock. */ HIO_SUB_NTIME (&ahead, &g_hio->tmr.jobs[0].when, &due); OK (ahead.sec >= PURGE_INTERVAL_SECS_FOR_TEST - 5, "the next sweep is a full interval ahead, measured from the clock the handler was given"); } static void with_fresh_server (void (*fn)(void), int nassert) { if (start_server() <= -1) { skip ("cannot start the dhcp server", nassert); return; } fn (); stop_server (); } int main (void) { hio_errinf_t errinf; g_hio = hio_open(&g_fi_mmgr, 0, HIO_NULL, HIO_FEATURE_ALL, 16, &errinf); if (!g_hio) { no_plan (); bail_out ("unable to open hio"); return -1; } quiet_logging (g_hio); no_plan (); with_fresh_server (test_discover_offers, 4); with_fresh_server (test_offer_is_held_against_other_clients, 1); with_fresh_server (test_discover_is_idempotent, 1); with_fresh_server (test_request_acks_and_binds, 3); with_fresh_server (test_request_for_another_server_is_ignored, 2); with_fresh_server (test_request_outside_pool_is_naked, 1); with_fresh_server (test_request_for_someone_elses_address_is_naked, 1); with_fresh_server (test_renewal, 1); with_fresh_server (test_release_frees_the_address, 2); with_fresh_server (test_decline_takes_the_address_out_of_service, 3); with_fresh_server (test_inform_returns_options_without_a_lease, 6); with_fresh_server (test_offer_and_ack_agree, 1); with_fresh_server (test_pool_exhaustion_is_silence, 2); with_fresh_server (test_malformed_and_foreign_packets, 3); with_fresh_server (test_lease_allocation_failure, 4); with_fresh_server (test_request_allocation_failure_naks, 2); with_fresh_server (test_relayed_nak_returns_via_the_relay, 8); with_fresh_server (test_broadcast_flag_overrides_ciaddr, 6); with_fresh_server (test_parameter_request_list, 18); with_fresh_server (test_requested_lease_time, 10); with_fresh_server (test_purge_timer, 3); test_config_refusals (); test_start_allocation_failure (); hio_close (g_hio); return exit_status(); }