diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d61834331..9e6af70de 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -71,6 +71,7 @@ jobs: libibverbs-dev libasan8 libcmocka-dev libedit-dev libarchive-dev \ libevent-dev libsmartcols-dev libmnl-dev libnuma-dev python3-pyelftools \ socat tcpdump traceroute graphviz iproute2 iputils-ping ndisc6 jq \ + dnsmasq \ "linux-modules-extra-$(uname -r)" if echo $MESON_EXTRA_OPTS | grep -q frr=enabled ; then sudo apt-get install -qy --no-install-recommends \ diff --git a/docs/graph.svg b/docs/graph.svg index 2d1fa2e39..acc300e5a 100644 --- a/docs/graph.svg +++ b/docs/graph.svg @@ -4,1036 +4,1060 @@ - - + + grout - + bond_output - -bond_output + +bond_output port_output - -port_output + +port_output bond_output->port_output - - + + port_tx - -port_tx + +port_tx - + port_output->port_tx - - + + control_input - -control_input + +control_input - + control_input->port_output - - + + lacp_output - -lacp_output + +lacp_output control_input->lacp_output - - + + loopback_input - -loopback_input + +loopback_input control_input->loopback_input - - + + arp_output_request - -arp_output_request + +arp_output_request control_input->arp_output_request - - + + icmp_local_send - -icmp_local_send + +icmp_local_send control_input->icmp_local_send - - + + icmp6_local_send - -icmp6_local_send + +icmp6_local_send control_input->icmp6_local_send - - + + ndp_na_output - -ndp_na_output + +ndp_na_output control_input->ndp_na_output - - + + ndp_ns_output - -ndp_ns_output + +ndp_ns_output control_input->ndp_ns_output - - + + - + +eth_output + +eth_output + + + +control_input->eth_output + + + + + ip6_output - -ip6_output + +ip6_output - + control_input->ip6_output - - + + - + ip_output - -ip_output + +ip_output - + control_input->ip_output - - + + - + arp_output_reply - -arp_output_reply + +arp_output_reply - + control_input->arp_output_reply - - - - - -eth_output - -eth_output + + - + lacp_output->eth_output - - + + - + ip_input - -ip_input + +ip_input - + loopback_input->ip_input - - + + - + ip6_input - -ip6_input + +ip6_input - + loopback_input->ip6_input - - + + - + arp_output_request->eth_output - - + + icmp_output - -icmp_output + +icmp_output - + icmp_local_send->icmp_output - - + + icmp6_output - -icmp6_output + +icmp6_output - + icmp6_local_send->icmp6_output - - + + - + ndp_na_output->icmp6_output - - + + - + ndp_ns_output->icmp6_output - - + + + + + +eth_output->bond_output + + + + + +eth_output->port_output + + - + ip6_output->eth_output - - + + loop_xvrf - -loop_xvrf + +loop_xvrf - + ip6_output->loop_xvrf - - + + sr6_output - -sr6_output + +sr6_output - + ip6_output->sr6_output - - + + ip6_hold - -ip6_hold + +ip6_hold - + ip6_output->ip6_hold - - + + ip6_error_dest_unreach - -ip6_error_dest_unreach + +ip6_error_dest_unreach - + ip6_output->ip6_error_dest_unreach - - + + ip6_loadbalance - -ip6_loadbalance + +ip6_loadbalance - + ip6_output->ip6_loadbalance - - + + - + ip_output->eth_output - - + + - + ip_output->loop_xvrf - - + + ip_fragment - -ip_fragment + +ip_fragment - + ip_output->ip_fragment - - + + ip_hold - -ip_hold + +ip_hold - + ip_output->ip_hold - - + + ip_error_dest_unreach - -ip_error_dest_unreach + +ip_error_dest_unreach - + ip_output->ip_error_dest_unreach - - + + ip_loadbalance - -ip_loadbalance + +ip_loadbalance - + ip_output->ip_loadbalance - - + + ip_error_frag_needed - -ip_error_frag_needed + +ip_error_frag_needed - + ip_output->ip_error_frag_needed - - + + ipip_output - -ipip_output + +ipip_output - + ip_output->ipip_output - - + + - + ip_output->sr6_output - - + + - + arp_output_reply->eth_output - - + + - + control_output - -control_output + +control_output - + eth_input - -eth_input + +eth_input - + snap_input - -snap_input + +snap_input - + eth_input->snap_input - - + + - + lacp_input - -lacp_input + +lacp_input - + eth_input->lacp_input - - + + - + arp_input - -arp_input + +arp_input - + eth_input->arp_input - - + + - + eth_input->ip_input - - + + - + eth_input->ip6_input - - + + l2_redirect - -l2_redirect + +l2_redirect - + snap_input->l2_redirect - - + + - + lacp_input->control_output - - + + arp_input_request - -arp_input_request + +arp_input_request - + arp_input->arp_input_request - - + + arp_input_reply - -arp_input_reply + +arp_input_reply - + arp_input->arp_input_reply - - + + - + ip_input->ip_output - - + + ip_forward - -ip_forward + +ip_forward - + ip_input->ip_forward - - + + dnat44_dynamic - -dnat44_dynamic + +dnat44_dynamic - + ip_input->dnat44_dynamic - - + + ip_input_local - -ip_input_local + +ip_input_local - + ip_input->ip_input_local - - + + - + ip_input->ip_error_dest_unreach - - + + dnat44_static - -dnat44_static + +dnat44_static - + ip_input->dnat44_static - - + + - + ip6_input->ip6_output - - + + ip6_forward - -ip6_forward + +ip6_forward - + ip6_input->ip6_forward - - + + ip6_input_local - -ip6_input_local + +ip6_input_local - + ip6_input->ip6_input_local - - + + - + ip6_input->ip6_error_dest_unreach - - + + sr6_local - -sr6_local + +sr6_local - + ip6_input->sr6_local - - - - - -eth_output->bond_output - - - - - -eth_output->port_output - - + + l1_xconnect - -l1_xconnect + +l1_xconnect - + l1_xconnect->port_output - - + + - + l2_redirect->control_output - - + + loopback_output - -loopback_output + +loopback_output - + loopback_output->control_output - - + + - + loop_xvrf->ip_input - - + + - + loop_xvrf->ip6_input - - + + port_rx - -port_rx + +port_rx - + port_rx->eth_input - - + + - + port_rx->l1_xconnect - - + + - + arp_input_request->control_output - - + + - + arp_input_reply->control_output - - + + icmp_input - -icmp_input + +icmp_input - + icmp_input->control_output - - + + - + icmp_input->icmp_output - - + + - + icmp_output->ip_output - - + + - + ip_forward->ip_output - - + + ip_error_ttl_exceeded - -ip_error_ttl_exceeded + +ip_error_ttl_exceeded - + ip_forward->ip_error_ttl_exceeded - - + + - + ip_fragment->ip_output - - + + - + ip_hold->control_output - - + + - + dnat44_dynamic->ip_forward - - + + - + dnat44_dynamic->ip_input_local - - + + - + dnat44_dynamic->ip_error_dest_unreach - - + + - + ip_input_local->icmp_input - - + + ipip_input - -ipip_input + +ipip_input - + ip_input_local->ipip_input - - + + l4_input_local - -l4_input_local + +l4_input_local - + ip_input_local->l4_input_local - - + + ospf_redirect - -ospf_redirect + +ospf_redirect - + ip_input_local->ospf_redirect - - + + - + dnat44_static->ip_forward - - + + - + dnat44_static->ip_input_local - - + + - + dnat44_static->ip_error_dest_unreach - - + + - + ip_loadbalance->ip_output - - + + - + ipip_input->ip_input - - + + l4_loopback_output - -l4_loopback_output + +l4_loopback_output - + l4_input_local->l4_loopback_output - - + + - + + +dhcp_input + +dhcp_input + + +l4_input_local->dhcp_input + + + + + ospf_redirect->l2_redirect - - + + - + ipip_output->ip_output - - + + - + sr6_output->ip6_output - - + + icmp6_input - -icmp6_input + +icmp6_input - + icmp6_input->control_output - - + + - + icmp6_input->icmp6_output - - + + ndp_ns_input - -ndp_ns_input + +ndp_ns_input - + icmp6_input->ndp_ns_input - - + + ndp_na_input - -ndp_na_input + +ndp_na_input - + icmp6_input->ndp_na_input - - + + ndp_rs_input - -ndp_rs_input + +ndp_rs_input - + icmp6_input->ndp_rs_input - - + + - + icmp6_output->ip6_output - - + + - + ndp_ns_input->control_output - - + + - + ndp_na_input->control_output - - + + - + ndp_rs_input->control_output - - + + - + ip6_forward->ip6_output - - + + ip6_error_ttl_exceeded - -ip6_error_ttl_exceeded + +ip6_error_ttl_exceeded - + ip6_forward->ip6_error_ttl_exceeded - - + + - + ip6_hold->control_output - - + + - + ip6_input_local->l4_input_local - - + + - + ip6_input_local->ospf_redirect - - + + - + ip6_input_local->icmp6_input - - + + - + sr6_local->ip_input - - + + - + sr6_local->ip6_input - - + + - + sr6_local->ip6_input_local - - + + - + sr6_local->ip6_error_dest_unreach - - + + - + ip6_loadbalance->ip6_output - - + + - + l4_loopback_output->loopback_output - - + + + + + +dhcp_input->control_output + + diff --git a/modules/dhcp/api/gr_dhcp.h b/modules/dhcp/api/gr_dhcp.h new file mode 100644 index 000000000..d3d57dfcd --- /dev/null +++ b/modules/dhcp/api/gr_dhcp.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#pragma once + +#include +#include +#include + +#include + +typedef enum dhcp_state : uint8_t { + DHCP_STATE_INIT = 0, + DHCP_STATE_SELECTING, + DHCP_STATE_REQUESTING, + DHCP_STATE_BOUND, + DHCP_STATE_RENEWING, + DHCP_STATE_REBINDING, +} dhcp_state_t; + +struct gr_dhcp_status { + uint16_t iface_id; + dhcp_state_t state; + ip4_addr_t server_ip; + ip4_addr_t assigned_ip; + uint32_t lease_time; + uint32_t renewal_time; // T1 + uint32_t rebind_time; // T2 +}; + +#define GR_DHCP_MODULE 0xd4c9 + +// list //////////////////////////////////////////////////////////////////////// + +#define GR_DHCP_LIST REQUEST_TYPE(GR_DHCP_MODULE, 0x01) + +// struct gr_dhcp_list_req { }; + +// STREAM(struct gr_dhcp_status); + +// start /////////////////////////////////////////////////////////////////////// + +#define GR_DHCP_START REQUEST_TYPE(GR_DHCP_MODULE, 0x02) + +struct gr_dhcp_start_req { + uint16_t iface_id; +}; + +// struct gr_dhcp_start_resp { }; + +// stop //////////////////////////////////////////////////////////////////////// + +#define GR_DHCP_STOP REQUEST_TYPE(GR_DHCP_MODULE, 0x03) + +struct gr_dhcp_stop_req { + uint16_t iface_id; +}; + +// struct gr_dhcp_stop_resp { }; diff --git a/modules/dhcp/api/meson.build b/modules/dhcp/api/meson.build new file mode 100644 index 000000000..2c4488e4d --- /dev/null +++ b/modules/dhcp/api/meson.build @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +api_headers += files('gr_dhcp.h') +api_inc += include_directories('.') diff --git a/modules/dhcp/cli/dhcp.c b/modules/dhcp/cli/dhcp.c new file mode 100644 index 000000000..08fa6fda7 --- /dev/null +++ b/modules/dhcp/cli/dhcp.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +static cmd_status_t dhcp_enable_cmd(struct gr_api_client *c, const struct ec_pnode *p) { + const char *iface_name = arg_str(p, "IFACE"); + struct gr_dhcp_start_req req; + struct gr_iface *iface; + + iface = iface_from_name(c, iface_name); + if (iface == NULL) + return CMD_ERROR; + + req.iface_id = iface->id; + free(iface); + + if (gr_api_client_send_recv(c, GR_DHCP_START, sizeof(req), &req, NULL) < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static cmd_status_t dhcp_disable_cmd(struct gr_api_client *c, const struct ec_pnode *p) { + const char *iface_name = arg_str(p, "IFACE"); + struct gr_dhcp_stop_req req; + struct gr_iface *iface; + + iface = iface_from_name(c, iface_name); + if (iface == NULL) + return CMD_ERROR; + + req.iface_id = iface->id; + free(iface); + + if (gr_api_client_send_recv(c, GR_DHCP_STOP, sizeof(req), &req, NULL) < 0) + return CMD_ERROR; + + return CMD_SUCCESS; +} + +static const char *dhcp_state_str(enum dhcp_state state) { + switch (state) { + case DHCP_STATE_INIT: + return "INIT"; + case DHCP_STATE_SELECTING: + return "SELECTING"; + case DHCP_STATE_REQUESTING: + return "REQUESTING"; + case DHCP_STATE_BOUND: + return "BOUND"; + case DHCP_STATE_RENEWING: + return "RENEWING"; + case DHCP_STATE_REBINDING: + return "REBINDING"; + default: + return "UNKNOWN"; + } +} + +static cmd_status_t dhcp_show_cmd(struct gr_api_client *c, const struct ec_pnode *) { + const struct gr_dhcp_status *status; + struct libscols_table *table; + int ret; + + table = scols_new_table(); + if (table == NULL) + return CMD_ERROR; + + scols_table_new_column(table, "INTERFACE", 0, 0); + scols_table_new_column(table, "STATE", 0, 0); + scols_table_new_column(table, "ADDRESS", 0, 0); + scols_table_new_column(table, "SERVER", 0, 0); + scols_table_new_column(table, "LEASE", 0, SCOLS_FL_RIGHT); + + gr_api_client_stream_foreach (status, ret, c, GR_DHCP_LIST, 0, NULL) { + struct libscols_line *line = scols_table_new_line(table, NULL); + struct gr_iface *iface = iface_from_id(c, status->iface_id); + + if (iface != NULL) { + scols_line_sprintf(line, 0, "%s", iface->name); + free(iface); + } else { + scols_line_sprintf(line, 0, "%u", status->iface_id); + } + + scols_line_sprintf(line, 1, "%s", dhcp_state_str(status->state)); + + if (status->assigned_ip != 0) { + scols_line_sprintf(line, 2, IP4_F, &status->assigned_ip); + } else { + scols_line_sprintf(line, 2, "-"); + } + + if (status->server_ip != 0) { + scols_line_sprintf(line, 3, IP4_F, &status->server_ip); + } else { + scols_line_sprintf(line, 3, "-"); + } + + if (status->lease_time != 0) + scols_line_sprintf(line, 4, "%us", status->lease_time); + else + scols_line_sprintf(line, 4, "-"); + } + + if (ret < 0) { + scols_unref_table(table); + return CMD_ERROR; + } + + scols_print_table(table); + scols_unref_table(table); + + return CMD_SUCCESS; +} + +static int ctx_init(struct ec_node *root) { + int ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("dhcp", "DHCP client.")), + "enable IFACE", + dhcp_enable_cmd, + "Enable DHCP on interface.", + with_help( + "Interface name.", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("dhcp", "DHCP client.")), + "disable IFACE", + dhcp_disable_cmd, + "Disable DHCP on interface.", + with_help( + "Interface name.", + ec_node_dyn("IFACE", complete_iface_names, INT2PTR(GR_IFACE_TYPE_UNDEF)) + ) + ); + if (ret < 0) + return ret; + + ret = CLI_COMMAND( + CLI_CONTEXT(root, CTX_ARG("dhcp", "DHCP client.")), + "show", + dhcp_show_cmd, + "Show DHCP client status." + ); + if (ret < 0) + return ret; + + return 0; +} + +static struct cli_context ctx = { + .name = "dhcp", + .init = ctx_init, +}; + +static void __attribute__((constructor, used)) init(void) { + cli_context_register(&ctx); +} diff --git a/modules/dhcp/cli/meson.build b/modules/dhcp/cli/meson.build new file mode 100644 index 000000000..5d0a912d0 --- /dev/null +++ b/modules/dhcp/cli/meson.build @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +cli_src += files( + 'dhcp.c', +) diff --git a/modules/dhcp/control/client.c b/modules/dhcp/control/client.c new file mode 100644 index 000000000..55976a17e --- /dev/null +++ b/modules/dhcp/control/client.c @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#include "client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +static struct event_base *dhcp_ev_base; +static struct dhcp_client *dhcp_clients[MAX_IFACES]; +static control_input_t dhcp_output; +static struct rte_mempool *dhcp_mp; + +static int dhcp_configure_interface(struct dhcp_client *client) { + const struct iface *iface; + struct rte_ether_addr mac; + struct nexthop *nh; + uint8_t prefixlen; + int ret; + + iface = iface_from_id(client->iface_id); + if (iface == NULL) + return errno_set(ENODEV); + + if (iface_get_eth_addr(iface->id, &mac) < 0 && errno != EOPNOTSUPP) + return -errno; + + if (client->subnet_mask == 0) { + LOG(ERR, "dhcp: server did not provide subnet mask, rejecting offer"); + return errno_set(EINVAL); + } + + prefixlen = __builtin_popcount(rte_be_to_cpu_32(client->subnet_mask)); + + struct gr_nexthop_base base = { + .type = GR_NH_T_L3, + .origin = GR_NH_ORIGIN_DHCP, + .iface_id = iface->id, + .vrf_id = iface->vrf_id, + }; + struct gr_nexthop_info_l3 l3 = { + .af = GR_AF_IP4, + .ipv4 = client->offered_ip, + .prefixlen = prefixlen, + .flags = GR_NH_F_LOCAL | GR_NH_F_LINK, + .state = GR_NH_S_REACHABLE, + .mac = mac, + }; + + if ((nh = nexthop_new(&base, &l3)) == NULL) + return -errno; + + ret = rib4_insert(iface->vrf_id, client->offered_ip, prefixlen, GR_NH_ORIGIN_LINK, nh); + if (ret < 0) { + LOG(ERR, "dhcp: failed to add address to RIB: %s", strerror(-ret)); + return ret; + } + + LOG(INFO, + "dhcp: configured address " IP4_F "/%u on iface %u", + &client->offered_ip, + prefixlen, + iface->id); + + // Add default route if router option was provided + if (client->router_ip != 0) { + struct nexthop *gw_nh; + + struct gr_nexthop_base gw_base = { + .type = GR_NH_T_L3, + .origin = GR_NH_ORIGIN_DHCP, + .iface_id = iface->id, + .vrf_id = iface->vrf_id, + }; + struct gr_nexthop_info_l3 gw_l3 = { + .af = GR_AF_IP4, + .ipv4 = client->router_ip, + .prefixlen = 0, + .flags = GR_NH_F_GATEWAY, + .state = GR_NH_S_REACHABLE, + }; + + if ((gw_nh = nexthop_new(&gw_base, &gw_l3)) == NULL) { + LOG(WARNING, "dhcp: failed to create gateway nexthop: %s", strerror(errno)); + return 0; // Continue even if gateway creation fails + } + + ret = rib4_insert(iface->vrf_id, 0, 0, GR_NH_ORIGIN_DHCP, gw_nh); + if (ret < 0) { + LOG(WARNING, "dhcp: failed to add default route: %s", strerror(-ret)); + } else { + LOG(INFO, "dhcp: added default route via " IP4_F, &client->router_ip); + } + } + + return 0; +} + +static void dhcp_send_request(struct dhcp_client *client); + +static void dhcp_cancel_timers(struct dhcp_client *client) { + if (client->t1_timer != NULL) { + event_free(client->t1_timer); + client->t1_timer = NULL; + } + if (client->t2_timer != NULL) { + event_free(client->t2_timer); + client->t2_timer = NULL; + } + if (client->expire_timer != NULL) { + event_free(client->expire_timer); + client->expire_timer = NULL; + } +} + +static void dhcp_t1_callback(evutil_socket_t, short, void *arg) { + struct dhcp_client *client = arg; + + if (client->state != DHCP_STATE_BOUND) { + LOG(WARNING, + "dhcp: T1 timer fired but not in BOUND state (state=%d)", + client->state); + return; + } + + LOG(INFO, "dhcp: T1 timer expired, transitioning to RENEWING (iface=%u)", client->iface_id); + client->state = DHCP_STATE_RENEWING; + + dhcp_send_request(client); +} + +static void dhcp_t2_callback(evutil_socket_t, short, void *arg) { + struct dhcp_client *client = arg; + + if (client->state != DHCP_STATE_RENEWING) { + LOG(WARNING, + "dhcp: T2 timer fired but not in RENEWING state (state=%d)", + client->state); + return; + } + + LOG(INFO, + "dhcp: T2 timer expired, transitioning to REBINDING (iface=%u)", + client->iface_id); + client->state = DHCP_STATE_REBINDING; + + dhcp_send_request(client); +} + +static void dhcp_expire_callback(evutil_socket_t, short, void *arg) { + struct dhcp_client *client = arg; + const struct iface *iface; + uint8_t prefixlen; + + LOG(WARNING, "dhcp: lease expired on iface %u", client->iface_id); + + iface = iface_from_id(client->iface_id); + if (iface == NULL) + return; + + if (client->subnet_mask == 0) { + LOG(ERR, "dhcp: lease expired but no subnet mask stored, cannot delete routes"); + client->state = DHCP_STATE_INIT; + return; + } + + prefixlen = __builtin_popcount(rte_be_to_cpu_32(client->subnet_mask)); + + if (client->offered_ip != 0) + rib4_delete(iface->vrf_id, client->offered_ip, prefixlen, GR_NH_T_L3); + if (client->router_ip != 0) + rib4_delete(iface->vrf_id, 0, 0, GR_NH_T_L3); + + client->state = DHCP_STATE_INIT; + client->offered_ip = 0; + client->server_ip = 0; + client->subnet_mask = 0; + client->router_ip = 0; + + client->xid = rte_rand(); + struct rte_mbuf *m = dhcp_build_discover(client->iface_id, client->xid); + if (m != NULL) { + post_to_stack(dhcp_output, m); + client->state = DHCP_STATE_SELECTING; + LOG(INFO, "dhcp: lease expired, sent new DISCOVER (iface=%u)", client->iface_id); + } +} + +static void dhcp_send_request(struct dhcp_client *client) { + struct rte_mbuf *m; + + m = dhcp_build_request( + client->iface_id, client->xid, client->server_ip, client->offered_ip + ); + if (m == NULL) { + LOG(ERR, "dhcp: failed to build REQUEST for renewal"); + return; + } + + post_to_stack(dhcp_output, m); + LOG(INFO, + "dhcp: sent REQUEST for renewal (iface=%u, state=%s)", + client->iface_id, + client->state == DHCP_STATE_RENEWING ? "RENEWING" : "REBINDING"); +} + +static void dhcp_schedule_timers(struct dhcp_client *client) { + struct timeval t1_tv, t2_tv, expire_tv; + uint32_t t1_secs, t2_secs; + + dhcp_cancel_timers(client); + + client->lease_start = time(NULL); + + if (client->renewal_time == 0) + t1_secs = client->lease_time / 2; // 50% of lease + else + t1_secs = client->renewal_time; + + if (client->rebind_time == 0) + t2_secs = (client->lease_time * 7) / 8; // 87.5% of lease + else + t2_secs = client->rebind_time; + + t1_tv.tv_sec = t1_secs; + t1_tv.tv_usec = 0; + client->t1_timer = evtimer_new(dhcp_ev_base, dhcp_t1_callback, client); + if (client->t1_timer != NULL) { + evtimer_add(client->t1_timer, &t1_tv); + } else { + LOG(WARNING, "dhcp: failed to create T1 timer"); + } + + t2_tv.tv_sec = t2_secs; + t2_tv.tv_usec = 0; + client->t2_timer = evtimer_new(dhcp_ev_base, dhcp_t2_callback, client); + if (client->t2_timer != NULL) { + evtimer_add(client->t2_timer, &t2_tv); + } else { + LOG(WARNING, "dhcp: failed to create T2 timer"); + } + + expire_tv.tv_sec = client->lease_time; + expire_tv.tv_usec = 0; + client->expire_timer = evtimer_new(dhcp_ev_base, dhcp_expire_callback, client); + if (client->expire_timer != NULL) { + evtimer_add(client->expire_timer, &expire_tv); + } else { + LOG(WARNING, "dhcp: failed to create expire timer"); + } + + LOG(INFO, + "dhcp: scheduled timers T1=%us, T2=%us, expire=%us (iface=%u)", + t1_secs, + t2_secs, + client->lease_time, + client->iface_id); +} + +void dhcp_input_cb(struct rte_mbuf *mbuf) { + dhcp_message_type_t msg_type = 0; + const struct iface *iface = mbuf_data(mbuf)->iface; + struct dhcp_client *client; + struct rte_mbuf *response; + + LOG(DEBUG, "dhcp_input_cb: received packet"); + + if (iface == NULL) { + LOG(ERR, "dhcp_input_cb: no interface in mbuf"); + rte_pktmbuf_free(mbuf); + return; + } + + LOG(DEBUG, "dhcp_input_cb: packet on iface %u", iface->id); + + if (iface->id >= MAX_IFACES) { + LOG(ERR, "dhcp_input_cb: iface %u exceeds MAX_IFACES", iface->id); + rte_pktmbuf_free(mbuf); + return; + } + + client = dhcp_clients[iface->id]; + if (client == NULL) { + LOG(DEBUG, "dhcp_input_cb: no DHCP client on iface %u, ignoring", iface->id); + rte_pktmbuf_free(mbuf); + return; + } + + LOG(DEBUG, "dhcp_input_cb: processing packet for client in state %d", client->state); + + if (dhcp_parse_packet(mbuf, client, &msg_type) < 0) { + LOG(ERR, "dhcp_input_cb: failed to parse DHCP packet on iface %u", iface->id); + rte_pktmbuf_free(mbuf); + return; + } + + switch (client->state) { + case DHCP_STATE_SELECTING: + if (msg_type == DHCP_OFFER) { + if (client->server_ip == 0 || client->offered_ip == 0) { + LOG(ERR, "dhcp: invalid OFFER (no server IP or offered IP)"); + break; + } + + LOG(INFO, "dhcp: received OFFER, sending REQUEST (iface=%u)", iface->id); + + response = dhcp_build_request( + client->iface_id, client->xid, client->server_ip, client->offered_ip + ); + if (response == NULL) { + LOG(ERR, "dhcp: failed to build REQUEST"); + break; + } + + if (post_to_stack(dhcp_output, response) < 0) { + LOG(ERR, "dhcp: failed to send REQUEST"); + rte_pktmbuf_free(response); + break; + } + + client->state = DHCP_STATE_REQUESTING; + LOG(INFO, "dhcp: transitioned to REQUESTING state (iface=%u)", iface->id); + } + break; + + case DHCP_STATE_REQUESTING: + if (msg_type == DHCP_ACK) { + if (client->offered_ip == 0) { + LOG(ERR, "dhcp: invalid ACK (no offered IP)"); + break; + } + + LOG(INFO, + "dhcp: received ACK, transitioning to BOUND (iface=%u)", + iface->id); + + if (dhcp_configure_interface(client) < 0) { + LOG(ERR, "dhcp: failed to configure interface"); + break; + } + + client->state = DHCP_STATE_BOUND; + dhcp_schedule_timers(client); + LOG(INFO, + "dhcp: acquired IP " IP4_F " (lease=%u, T1=%u, T2=%u)", + &client->offered_ip, + client->lease_time, + client->renewal_time, + client->rebind_time); + } else if (msg_type == DHCP_NAK) { + LOG(WARNING, "dhcp: received NAK, returning to INIT (iface=%u)", iface->id); + client->state = DHCP_STATE_INIT; + } + break; + + case DHCP_STATE_BOUND: + // Shouldn't receive DHCP messages while bound (unless it's a rogue server) + LOG(DEBUG, "dhcp: ignoring message in BOUND state"); + break; + + case DHCP_STATE_RENEWING: + case DHCP_STATE_REBINDING: + if (msg_type == DHCP_ACK) { + LOG(INFO, + "dhcp: lease renewed (state=%s, iface=%u)", + client->state == DHCP_STATE_RENEWING ? "RENEWING" : "REBINDING", + iface->id); + + client->state = DHCP_STATE_BOUND; + dhcp_schedule_timers(client); + + LOG(INFO, + "dhcp: lease extended (lease=%u, T1=%u, T2=%u)", + client->lease_time, + client->renewal_time, + client->rebind_time); + } else if (msg_type == DHCP_NAK) { + LOG(WARNING, + "dhcp: received NAK during renewal, returning to INIT (iface=%u)", + iface->id); + + dhcp_cancel_timers(client); + + if (client->subnet_mask != 0) { + uint8_t prefixlen = __builtin_popcount( + rte_be_to_cpu_32(client->subnet_mask) + ); + if (client->offered_ip != 0) + rib4_delete( + iface->vrf_id, + client->offered_ip, + prefixlen, + GR_NH_T_L3 + ); + } else if (client->offered_ip != 0) { + LOG(ERR, + "dhcp: NAK received but no subnet mask stored, cannot delete " + "address route"); + } + if (client->router_ip != 0) + rib4_delete(iface->vrf_id, 0, 0, GR_NH_T_L3); + + client->state = DHCP_STATE_INIT; + client->offered_ip = 0; + client->server_ip = 0; + + client->xid = rte_rand(); + response = dhcp_build_discover(client->iface_id, client->xid); + if (response != NULL) { + post_to_stack(dhcp_output, response); + client->state = DHCP_STATE_SELECTING; + } + } + break; + + default: + LOG(WARNING, "dhcp: received message in unexpected state %d", client->state); + break; + } + + rte_pktmbuf_free(mbuf); +} + +static void dhcp_init(struct event_base *ev_base) { + dhcp_ev_base = ev_base; + + dhcp_input_register_port(); + + dhcp_output = gr_control_input_register_handler("eth_output", true); + + dhcp_mp = gr_pktmbuf_pool_get(SOCKET_ID_ANY, 512); + if (dhcp_mp == NULL) + ABORT("dhcp: failed to get mempool"); + + LOG(INFO, "dhcp: module initialized"); +} + +int dhcp_start(uint16_t iface_id) { + struct dhcp_client *client; + struct rte_mbuf *m; + uint32_t xid; + + if (iface_id >= MAX_IFACES) { + errno = EINVAL; + return -1; + } + + if (iface_from_id(iface_id) == NULL) { + errno = ENODEV; + return -1; + } + + if (dhcp_clients[iface_id] != NULL) { + LOG(WARNING, "dhcp: client already running on iface %u", iface_id); + errno = EEXIST; + return -1; + } + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + LOG(ERR, "dhcp: failed to allocate client for iface %u", iface_id); + errno = ENOMEM; + return -1; + } + + xid = rte_rand(); + + client->iface_id = iface_id; + client->state = DHCP_STATE_INIT; + client->xid = xid; + + dhcp_clients[iface_id] = client; + + m = dhcp_build_discover(iface_id, xid); + if (m == NULL) { + LOG(ERR, "dhcp: failed to build DISCOVER for iface %u", iface_id); + free(client); + dhcp_clients[iface_id] = NULL; + errno = ENOMEM; + return -1; + } + + if (post_to_stack(dhcp_output, m) < 0) { + LOG(ERR, "dhcp: failed to send DISCOVER for iface %u", iface_id); + rte_pktmbuf_free(m); + free(client); + dhcp_clients[iface_id] = NULL; + errno = EIO; + return -1; + } + + client->state = DHCP_STATE_SELECTING; + + LOG(INFO, "dhcp: sent DISCOVER on iface %u (xid=0x%08x)", iface_id, xid); + return 0; +} + +void dhcp_stop(uint16_t iface_id) { + struct dhcp_client *client; + const struct iface *iface; + uint8_t prefixlen; + int ret; + + errno = 0; + + if (iface_id >= MAX_IFACES) { + errno = EINVAL; + return; + } + + client = dhcp_clients[iface_id]; + if (client == NULL) { + LOG(WARNING, "dhcp: no client running on iface %u", iface_id); + errno = ENOENT; + return; + } + + iface = iface_from_id(iface_id); + if (iface == NULL) { + errno = ENODEV; + return; + } + + if (client->offered_ip != 0) { + if (client->subnet_mask == 0) { + LOG(ERR, + "dhcp: stopping client but no subnet mask stored, cannot delete " + "address route"); + } else { + prefixlen = __builtin_popcount(rte_be_to_cpu_32(client->subnet_mask)); + ret = rib4_delete(iface->vrf_id, client->offered_ip, prefixlen, GR_NH_T_L3); + if (ret < 0) { + LOG(WARNING, + "dhcp: failed to remove address route: %s", + strerror(-ret)); + } else { + LOG(INFO, + "dhcp: removed address " IP4_F "/%u from iface %u", + &client->offered_ip, + prefixlen, + iface_id); + } + } + } + + if (client->router_ip != 0) { + ret = rib4_delete(iface->vrf_id, 0, 0, GR_NH_T_L3); + if (ret < 0) { + LOG(WARNING, "dhcp: failed to remove default route: %s", strerror(-ret)); + } else { + LOG(INFO, "dhcp: removed default route via " IP4_F, &client->router_ip); + } + } + + dhcp_cancel_timers(client); + + free(client); + dhcp_clients[iface_id] = NULL; + + LOG(INFO, "dhcp: stopped client on iface %u", iface_id); +} + +struct rte_mempool *dhcp_get_mempool(void) { + return dhcp_mp; +} + +control_input_t dhcp_get_output(void) { + return dhcp_output; +} + +static struct api_out dhcp_list_handler(const void *, struct api_ctx *ctx) { + struct dhcp_client *client; + uint16_t iface_id; + + for (iface_id = 0; iface_id < MAX_IFACES; iface_id++) { + client = dhcp_clients[iface_id]; + if (client == NULL) + continue; + + struct gr_dhcp_status status = { + .iface_id = client->iface_id, + .state = client->state, + .server_ip = client->server_ip, + .assigned_ip = client->offered_ip, + .lease_time = client->lease_time, + .renewal_time = client->renewal_time, + .rebind_time = client->rebind_time, + }; + + api_send(ctx, sizeof(status), &status); + } + + return api_out(0, 0, NULL); +} + +static struct api_out dhcp_start_handler(const void *request, struct api_ctx *) { + const struct gr_dhcp_start_req *req = request; + + if (dhcp_start(req->iface_id) < 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static struct api_out dhcp_stop_handler(const void *request, struct api_ctx *) { + const struct gr_dhcp_stop_req *req = request; + + dhcp_stop(req->iface_id); + if (errno != 0) + return api_out(errno, 0, NULL); + + return api_out(0, 0, NULL); +} + +static void dhcp_fini(struct event_base *) { + gr_pktmbuf_pool_release(dhcp_mp, 512); + LOG(INFO, "dhcp: module finalized"); +} + +static struct gr_module dhcp_module = { + .name = "dhcp", + .init = dhcp_init, + .fini = dhcp_fini, + .depends_on = "graph", +}; + +static struct gr_api_handler dhcp_list_api = { + .name = "dhcp list", + .request_type = GR_DHCP_LIST, + .callback = dhcp_list_handler, +}; + +static struct gr_api_handler dhcp_start_api = { + .name = "dhcp start", + .request_type = GR_DHCP_START, + .callback = dhcp_start_handler, +}; + +static struct gr_api_handler dhcp_stop_api = { + .name = "dhcp stop", + .request_type = GR_DHCP_STOP, + .callback = dhcp_stop_handler, +}; + +RTE_INIT(dhcp_constructor) { + gr_register_module(&dhcp_module); + gr_register_api_handler(&dhcp_list_api); + gr_register_api_handler(&dhcp_start_api); + gr_register_api_handler(&dhcp_stop_api); +} diff --git a/modules/dhcp/control/client.h b/modules/dhcp/control/client.h new file mode 100644 index 000000000..d55d2ee02 --- /dev/null +++ b/modules/dhcp/control/client.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include + +struct event; + +// DHCP message types (RFC 2132 option 53) +typedef enum dhcp_message_type : uint8_t { + DHCP_DISCOVER = 1, + DHCP_OFFER = 2, + DHCP_REQUEST = 3, + DHCP_DECLINE = 4, + DHCP_ACK = 5, + DHCP_NAK = 6, + DHCP_RELEASE = 7, + DHCP_INFORM = 8, +} dhcp_message_type_t; + +// DHCP options (RFC 2132) +typedef enum dhcp_option_code : uint8_t { + DHCP_OPT_PAD = 0, + DHCP_OPT_SUBNET_MASK = 1, + DHCP_OPT_ROUTER = 3, + DHCP_OPT_DNS_SERVER = 6, + DHCP_OPT_HOSTNAME = 12, + DHCP_OPT_DOMAIN_NAME = 15, + DHCP_OPT_REQUESTED_IP = 50, + DHCP_OPT_LEASE_TIME = 51, + DHCP_OPT_MESSAGE_TYPE = 53, + DHCP_OPT_SERVER_ID = 54, + DHCP_OPT_PARAM_REQUEST_LIST = 55, + DHCP_OPT_RENEWAL_TIME = 58, + DHCP_OPT_REBIND_TIME = 59, + DHCP_OPT_END = 255, +} dhcp_option_code_t; + +struct dhcp_client { + uint16_t iface_id; + dhcp_state_t state; + uint32_t xid; + ip4_addr_t server_ip; + ip4_addr_t offered_ip; + ip4_addr_t subnet_mask; + ip4_addr_t router_ip; + uint32_t lease_time; + uint32_t renewal_time; // T1 time in seconds + uint32_t rebind_time; // T2 time in seconds + time_t lease_start; + struct event *t1_timer; + struct event *t2_timer; + struct event *expire_timer; +}; + +struct dhcp_packet { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + rte_be32_t xid; + rte_be16_t secs; + rte_be16_t flags; + rte_be32_t ciaddr; + rte_be32_t yiaddr; + rte_be32_t siaddr; + rte_be32_t giaddr; + uint8_t chaddr[16]; + uint8_t sname[64]; + uint8_t file[128]; + rte_be32_t magic; + uint8_t options[]; +} __rte_packed; + +#define DHCP_MAGIC RTE_BE32(0x63825363) // RFC 2131 section 3 +#define BOOTREQUEST 1 +#define BOOTREPLY 2 + +void dhcp_input_cb(struct rte_mbuf *mbuf); + +void dhcp_input_register_port(void); + +int dhcp_start(uint16_t iface_id); + +void dhcp_stop(uint16_t iface_id); + +struct rte_mempool *dhcp_get_mempool(void); +control_input_t dhcp_get_output(void); + +int dhcp_parse_packet( + struct rte_mbuf *mbuf, + struct dhcp_client *client, + dhcp_message_type_t *msg_type_out +); +struct rte_mbuf *dhcp_build_discover(uint16_t iface_id, uint32_t xid); +struct rte_mbuf * +dhcp_build_request(uint16_t iface_id, uint32_t xid, ip4_addr_t server_ip, ip4_addr_t requested_ip); + +int dhcp_parse_options( + const uint8_t *options, + uint16_t options_len, + struct dhcp_client *client, + dhcp_message_type_t *msg_type +); +int dhcp_build_options(uint8_t *buf, uint16_t buf_len, dhcp_message_type_t msg_type); +int dhcp_build_options_ex( + uint8_t *buf, + uint16_t buf_len, + dhcp_message_type_t msg_type, + ip4_addr_t server_ip, + ip4_addr_t requested_ip +); diff --git a/modules/dhcp/control/meson.build b/modules/dhcp/control/meson.build new file mode 100644 index 000000000..c63c3114a --- /dev/null +++ b/modules/dhcp/control/meson.build @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +src += files( + 'client.c', + 'options.c', + 'packet.c', +) +inc += include_directories('.') diff --git a/modules/dhcp/control/options.c b/modules/dhcp/control/options.c new file mode 100644 index 000000000..5e02e6876 --- /dev/null +++ b/modules/dhcp/control/options.c @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#include "client.h" + +#include + +int dhcp_parse_options( + const uint8_t *options, + uint16_t options_len, + struct dhcp_client *client, + dhcp_message_type_t *msg_type +) { + dhcp_option_code_t opt; + uint16_t pos = 0; + uint8_t len; + + *msg_type = 0; + + while (pos < options_len) { + opt = options[pos++]; + + if (opt == DHCP_OPT_END) + break; + + if (opt == DHCP_OPT_PAD) + continue; + + if (pos >= options_len) { + LOG(ERR, "dhcp_parse_options: truncated option %u", opt); + return -1; + } + len = options[pos++]; + + if (pos + len > options_len) { + LOG(ERR, + "dhcp_parse_options: option %u length %u exceeds packet", + opt, + len); + return -1; + } + + switch (opt) { + case DHCP_OPT_MESSAGE_TYPE: + if (len != 1) { + LOG(ERR, "dhcp_parse_options: invalid message type length %u", len); + return -1; + } + *msg_type = options[pos]; + break; + + case DHCP_OPT_SUBNET_MASK: + if (len != 4) { + LOG(ERR, "dhcp_parse_options: invalid subnet mask length %u", len); + break; + } + memcpy(&client->subnet_mask, &options[pos], 4); + break; + + case DHCP_OPT_ROUTER: + if (len < 4) { + LOG(ERR, "dhcp_parse_options: invalid router length %u", len); + break; + } + memcpy(&client->router_ip, &options[pos], 4); + break; + + case DHCP_OPT_SERVER_ID: + if (len != 4) { + LOG(ERR, "dhcp_parse_options: invalid server ID length %u", len); + break; + } + memcpy(&client->server_ip, &options[pos], 4); + break; + + case DHCP_OPT_LEASE_TIME: + if (len != 4) { + LOG(ERR, "dhcp_parse_options: invalid lease time length %u", len); + break; + } + client->lease_time = (options[pos] << 24) | (options[pos + 1] << 16) + | (options[pos + 2] << 8) | options[pos + 3]; + break; + + case DHCP_OPT_RENEWAL_TIME: + if (len != 4) { + LOG(ERR, "dhcp_parse_options: invalid renewal time length %u", len); + break; + } + client->renewal_time = (options[pos] << 24) | (options[pos + 1] << 16) + | (options[pos + 2] << 8) | options[pos + 3]; + break; + + case DHCP_OPT_REBIND_TIME: + if (len != 4) { + LOG(ERR, "dhcp_parse_options: invalid rebind time length %u", len); + break; + } + client->rebind_time = (options[pos] << 24) | (options[pos + 1] << 16) + | (options[pos + 2] << 8) | options[pos + 3]; + break; + + default: + LOG(DEBUG, "dhcp_parse_options: ignoring option %u (len=%u)", opt, len); + break; + } + + pos += len; + } + + if (*msg_type == 0) { + LOG(ERR, "dhcp_parse_options: no message type found"); + return -1; + } + + return 0; +} + +int dhcp_build_options(uint8_t *buf, uint16_t buf_len, dhcp_message_type_t msg_type) { + return dhcp_build_options_ex(buf, buf_len, msg_type, 0, 0); +} + +int dhcp_build_options_ex( + uint8_t *buf, + uint16_t buf_len, + dhcp_message_type_t msg_type, + ip4_addr_t server_ip, + ip4_addr_t requested_ip +) { + uint16_t pos = 0; + + // Worst case: 3 (msg type) + 6 (server id) + 6 (requested ip) + 6 (param req) + 1 (end) = 22 + if (buf_len < 22) { + LOG(ERR, "dhcp_build_options: buffer too small"); + return -1; + } + + // Option 53: DHCP Message Type + buf[pos++] = DHCP_OPT_MESSAGE_TYPE; + buf[pos++] = 1; // Length + buf[pos++] = msg_type; + + // Option 54: Server Identifier + if (msg_type == DHCP_REQUEST && server_ip != 0) { + buf[pos++] = DHCP_OPT_SERVER_ID; + buf[pos++] = 4; // Length + memcpy(&buf[pos], &server_ip, 4); + pos += 4; + } + + // Option 50: Requested IP Address + if (msg_type == DHCP_REQUEST && requested_ip != 0) { + buf[pos++] = DHCP_OPT_REQUESTED_IP; + buf[pos++] = 4; // Length + memcpy(&buf[pos], &requested_ip, 4); + pos += 4; + } + + // Option 55: Parameter Request List + buf[pos++] = DHCP_OPT_PARAM_REQUEST_LIST; + buf[pos++] = 4; + buf[pos++] = DHCP_OPT_SUBNET_MASK; // 1 + buf[pos++] = DHCP_OPT_ROUTER; // 3 + buf[pos++] = DHCP_OPT_DNS_SERVER; // 6 + buf[pos++] = DHCP_OPT_DOMAIN_NAME; // 15 + + // Option 255: End + buf[pos++] = DHCP_OPT_END; + + return pos; +} diff --git a/modules/dhcp/control/packet.c b/modules/dhcp/control/packet.c new file mode 100644 index 000000000..2addd629c --- /dev/null +++ b/modules/dhcp/control/packet.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#include "client.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +int dhcp_parse_packet( + struct rte_mbuf *mbuf, + struct dhcp_client *client, + dhcp_message_type_t *msg_type_out +) { + dhcp_message_type_t msg_type; + struct dhcp_packet *dhcp; + struct rte_udp_hdr *udp; + uint16_t options_len; + uint8_t *pkt_data; + uint16_t pkt_len; + uint8_t *options; + + pkt_data = rte_pktmbuf_mtod(mbuf, uint8_t *); + pkt_len = rte_pktmbuf_data_len(mbuf); + + if (pkt_len < sizeof(*udp)) { + LOG(ERR, "dhcp_parse_packet: packet too short for UDP header"); + return -1; + } + udp = (struct rte_udp_hdr *)pkt_data; + + if (pkt_len < sizeof(*udp) + sizeof(*dhcp)) { + LOG(ERR, "dhcp_parse_packet: packet too short for DHCP header"); + return -1; + } + dhcp = (struct dhcp_packet *)(pkt_data + sizeof(*udp)); + + if (dhcp->magic != DHCP_MAGIC) { + LOG(ERR, "dhcp_parse_packet: invalid DHCP magic cookie"); + return -1; + } + + if (dhcp->xid != rte_cpu_to_be_32(client->xid)) { + LOG(DEBUG, + "dhcp_parse_packet: transaction ID mismatch (got 0x%x, expected 0x%x)", + rte_be_to_cpu_32(dhcp->xid), + client->xid); + return -1; + } + + if (dhcp->op != BOOTREPLY) { + LOG(ERR, "dhcp_parse_packet: not a BOOTREPLY"); + return -1; + } + + options = dhcp->options; + options_len = pkt_len - sizeof(*udp) - sizeof(*dhcp); + + if (dhcp_parse_options(options, options_len, client, &msg_type) < 0) { + LOG(ERR, "dhcp_parse_packet: failed to parse options"); + return -1; + } + + client->offered_ip = dhcp->yiaddr; + + LOG(INFO, + "dhcp: received %s from server (xid=0x%08x, offered_ip=" IP4_F ")", + msg_type == DHCP_OFFER ? "OFFER" : + msg_type == DHCP_ACK ? "ACK" : + "other", + client->xid, + &client->offered_ip); + + if (msg_type_out != NULL) + *msg_type_out = msg_type; + + return 0; +} + +static struct rte_mbuf *dhcp_build_packet_common( + uint16_t iface_id, + uint32_t xid, + dhcp_message_type_t msg_type, + ip4_addr_t server_ip, + ip4_addr_t requested_ip, + const char *caller +) { + const struct iface *iface; + struct rte_ether_addr mac; + struct dhcp_packet *dhcp; + struct rte_ipv4_hdr *ip; + struct rte_udp_hdr *udp; + struct rte_mbuf *m; + uint8_t *options; + int opt_len; + + iface = iface_from_id(iface_id); + if (iface == NULL) { + LOG(ERR, "%s: interface %u not found", caller, iface_id); + return NULL; + } + + if (iface_get_eth_addr(iface_id, &mac) < 0) { + LOG(ERR, "%s: failed to get MAC for iface %u", caller, iface_id); + return NULL; + } + + m = rte_pktmbuf_alloc(dhcp_get_mempool()); + if (m == NULL) { + LOG(ERR, "%s: failed to allocate mbuf", caller); + return NULL; + } + + mbuf_data(m)->iface = iface; + + struct rte_ether_addr broadcast_mac; + memset(&broadcast_mac, 0xFF, RTE_ETHER_ADDR_LEN); + eth_output_mbuf_data(m)->dst = broadcast_mac; + eth_output_mbuf_data(m)->ether_type = RTE_BE16(RTE_ETHER_TYPE_IPV4); + + ip = (struct rte_ipv4_hdr *)rte_pktmbuf_append(m, sizeof(*ip)); + if (ip == NULL) { + LOG(ERR, "%s: failed to append IP header", caller); + rte_pktmbuf_free(m); + return NULL; + } + + udp = (struct rte_udp_hdr *)rte_pktmbuf_append(m, sizeof(*udp)); + if (udp == NULL) { + LOG(ERR, "%s: failed to append UDP header", caller); + rte_pktmbuf_free(m); + return NULL; + } + + dhcp = (struct dhcp_packet *)rte_pktmbuf_append(m, sizeof(*dhcp)); + if (dhcp == NULL) { + LOG(ERR, "%s: failed to append DHCP packet", caller); + rte_pktmbuf_free(m); + return NULL; + } + + memset(dhcp, 0, sizeof(*dhcp)); + dhcp->op = BOOTREQUEST; + dhcp->htype = 1; + dhcp->hlen = 6; + dhcp->xid = rte_cpu_to_be_32(xid); + dhcp->flags = RTE_BE16(0x8000); // Broadcast flag + rte_ether_addr_copy(&mac, (struct rte_ether_addr *)dhcp->chaddr); + dhcp->magic = DHCP_MAGIC; + + // Allocate space for options (worst case: 22 bytes, see dhcp_build_options_ex) + options = (uint8_t *)rte_pktmbuf_append(m, 22); + if (options == NULL) { + LOG(ERR, "%s: failed to append options", caller); + rte_pktmbuf_free(m); + return NULL; + } + + opt_len = dhcp_build_options_ex(options, 22, msg_type, server_ip, requested_ip); + if (opt_len < 0) { + LOG(ERR, "%s: failed to build options", caller); + rte_pktmbuf_free(m); + return NULL; + } + + udp->src_port = RTE_BE16(68); + udp->dst_port = RTE_BE16(67); + udp->dgram_len = rte_cpu_to_be_16(sizeof(*udp) + sizeof(*dhcp) + opt_len); + udp->dgram_cksum = 0; + + ip->version_ihl = RTE_IPV4_VHL_DEF; + ip->type_of_service = 0; + ip->total_length = rte_cpu_to_be_16(sizeof(*ip) + sizeof(*udp) + sizeof(*dhcp) + opt_len); + ip->packet_id = 0; + ip->fragment_offset = 0; + ip->time_to_live = 64; + ip->next_proto_id = IPPROTO_UDP; + ip->src_addr = 0; + ip->dst_addr = RTE_BE32(0xFFFFFFFF); + ip->hdr_checksum = 0; + ip->hdr_checksum = rte_ipv4_cksum(ip); + + return m; +} + +struct rte_mbuf *dhcp_build_discover(uint16_t iface_id, uint32_t xid) { + return dhcp_build_packet_common(iface_id, xid, DHCP_DISCOVER, 0, 0, "dhcp_build_discover"); +} + +struct rte_mbuf * +dhcp_build_request(uint16_t iface_id, uint32_t xid, ip4_addr_t server_ip, ip4_addr_t requested_ip) { + return dhcp_build_packet_common( + iface_id, xid, DHCP_REQUEST, server_ip, requested_ip, "dhcp_build_request" + ); +} diff --git a/modules/dhcp/datapath/dhcp_input.c b/modules/dhcp/datapath/dhcp_input.c new file mode 100644 index 000000000..3c4403d3e --- /dev/null +++ b/modules/dhcp/datapath/dhcp_input.c @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025 Anthony Harivel + +#include "../control/client.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +struct dhcp_input_trace_data { + uint32_t xid; + uint8_t msg_type; + uint8_t op; +}; + +enum { + CONTROL = 0, + EDGE_COUNT, +}; + +static uint16_t +dhcp_input_process(struct rte_graph *graph, struct rte_node *node, void **objs, uint16_t nb_objs) { + struct control_output_mbuf_data *d; + struct rte_mbuf *mbuf; + + for (uint16_t i = 0; i < nb_objs; i++) { + mbuf = objs[i]; + + d = control_output_mbuf_data(mbuf); + d->callback = dhcp_input_cb; + + if (gr_mbuf_is_traced(mbuf)) { + struct dhcp_input_trace_data *t; + const struct rte_udp_hdr *udp; + const struct dhcp_packet *dhcp; + uint16_t pkt_len; + + t = gr_mbuf_trace_add(mbuf, node, sizeof(*t)); + pkt_len = rte_pktmbuf_data_len(mbuf); + + // Parse minimal DHCP header for trace + if (pkt_len >= sizeof(*udp) + sizeof(*dhcp)) { + udp = rte_pktmbuf_mtod(mbuf, const struct rte_udp_hdr *); + dhcp = PAYLOAD(udp); + + t->xid = rte_be_to_cpu_32(dhcp->xid); + t->op = dhcp->op; + + // Extract message type from options (option 53) + t->msg_type = 0; + const uint8_t *options = dhcp->options; + uint16_t options_len = pkt_len - sizeof(*udp) - sizeof(*dhcp); + uint16_t pos = 0; + + while (pos < options_len && pos < 64) { // Limit search + uint8_t opt = options[pos++]; + if (opt == DHCP_OPT_END) + break; + if (opt == DHCP_OPT_PAD) + continue; + if (pos >= options_len) + break; + uint8_t len = options[pos++]; + if (opt == DHCP_OPT_MESSAGE_TYPE && len == 1 + && pos < options_len) { + t->msg_type = options[pos]; + break; + } + pos += len; + } + } + } + + rte_node_enqueue_x1(graph, node, CONTROL, mbuf); + } + + return nb_objs; +} + +static const char *dhcp_msg_type_str(uint8_t type) { + switch (type) { + case DHCP_DISCOVER: + return "DISCOVER"; + case DHCP_OFFER: + return "OFFER"; + case DHCP_REQUEST: + return "REQUEST"; + case DHCP_DECLINE: + return "DECLINE"; + case DHCP_ACK: + return "ACK"; + case DHCP_NAK: + return "NAK"; + case DHCP_RELEASE: + return "RELEASE"; + case DHCP_INFORM: + return "INFORM"; + default: + return "UNKNOWN"; + } +} + +static int dhcp_input_trace_format(char *buf, size_t len, const void *data, size_t /*data_len*/) { + const struct dhcp_input_trace_data *t = data; + return snprintf( + buf, + len, + "%s xid=0x%08x %s", + t->op == BOOTREQUEST ? "REQUEST" : + t->op == BOOTREPLY ? "REPLY" : + "?", + t->xid, + dhcp_msg_type_str(t->msg_type) + ); +} + +static struct rte_node_register node = { + .name = "dhcp_input", + .process = dhcp_input_process, + .nb_edges = EDGE_COUNT, + .next_nodes = { + [CONTROL] = "control_output", + }, +}; + +static struct gr_node_info info = { + .node = &node, + .trace_format = dhcp_input_trace_format, +}; + +GR_NODE_REGISTER(info); + +void dhcp_input_register_port(void) { + l4_input_register_port(IPPROTO_UDP, RTE_BE16(68), "dhcp_input"); + LOG(INFO, "dhcp_input_register_port: registered UDP port 68 for dhcp_input node"); +} diff --git a/modules/dhcp/datapath/meson.build b/modules/dhcp/datapath/meson.build new file mode 100644 index 000000000..634ec028a --- /dev/null +++ b/modules/dhcp/datapath/meson.build @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +src += files( + 'dhcp_input.c', +) +inc += include_directories('.') diff --git a/modules/dhcp/meson.build b/modules/dhcp/meson.build new file mode 100644 index 000000000..ab5ccfbe7 --- /dev/null +++ b/modules/dhcp/meson.build @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +subdir('api') +subdir('cli') +subdir('control') +subdir('datapath') diff --git a/modules/meson.build b/modules/meson.build index b16d2a1dc..1859be3d3 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -8,3 +8,4 @@ subdir('ipip') subdir('l4') subdir('policy') subdir('srv6') +subdir('dhcp') diff --git a/smoke/dhcp_test.sh b/smoke/dhcp_test.sh new file mode 100755 index 000000000..5058344fd --- /dev/null +++ b/smoke/dhcp_test.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Anthony Harivel + +. $(dirname $0)/_init.sh + +command -v dnsmasq || fail "dnsmasq is not installed" + +netns_add dhcp-server + +port_add p0 mode l3 + +ip link set x-p0 netns dhcp-server + +ip -n dhcp-server link set x-p0 up +ip -n dhcp-server addr add 192.168.100.1/24 dev x-p0 + +cat > $tmp/dnsmasq.conf <> $tmp/cleanup </dev/null || true +EOF + +# Wait for dnsmasq to start +sleep 1 + +# Enable DHCP on grout interface +grcli dhcp enable p0 + +# Wait for DHCP to acquire lease (adjust timeout as needed) +sleep 5 + +# Verify default route was added +grcli route show | grep -q "0.0.0.0/0.*192.168.100.1" || fail "DHCP did not add default route" + +# Verify DHCP assigned an address by checking the /24 route exists +grcli route show | grep -q "192.168.100.0/24" || fail "DHCP did not assign IP address" + +# Query DHCP status via API +echo "=== DHCP Client Status ===" +grcli dhcp show +grcli dhcp show | grep -q "p0" || fail "DHCP status not available" +grcli dhcp show | grep -q "BOUND" || fail "DHCP client not in BOUND state" + +# Test DHCP release/disable +grcli dhcp disable p0 + +# Verify default route was removed +! grcli route show | grep -q "0.0.0.0/0.*192.168.100.1" || fail "DHCP route not removed after disable" + +# Verify DHCP client is no longer active +! grcli dhcp show | grep -q "p0" || fail "DHCP client still active after disable" + +echo "DHCP smoke test passed!"