From 1b9aef7b51590d940a5e6a3bc8ad21fcd309839f Mon Sep 17 00:00:00 2001 From: Anthony Harivel Date: Wed, 26 Nov 2025 16:18:49 +0100 Subject: [PATCH] dhcp: add client implementation Add a DHCP client that automatically acquires IP addresses and default routes for interfaces. The client handles the complete DHCP workflow including initial address assignment, lease renewal, and cleanup. When enabled on an interface, the client sends DISCOVER messages and processes server responses to configure the interface with an assigned IP address, netmask, and default gateway. Leases are automatically renewed before expiration using T1 and T2 timers. When disabled, all DHCP-assigned configuration is removed. CLI commands allow enabling and disabling DHCP per interface, and viewing the current DHCP client status including assigned addresses, lease times, and client state. The implementation follows RFC 2131 with support for standard DHCP options including subnet mask, router, lease time, and renewal timings. Signed-off-by: Anthony Harivel Reviewed-by: Robin Jarry --- .github/workflows/check.yml | 1 + docs/graph.svg | 964 +++++++++++++++-------------- modules/dhcp/api/gr_dhcp.h | 59 ++ modules/dhcp/api/meson.build | 5 + modules/dhcp/cli/dhcp.c | 177 ++++++ modules/dhcp/cli/meson.build | 6 + modules/dhcp/control/client.c | 668 ++++++++++++++++++++ modules/dhcp/control/client.h | 121 ++++ modules/dhcp/control/meson.build | 9 + modules/dhcp/control/options.c | 171 +++++ modules/dhcp/control/packet.c | 208 +++++++ modules/dhcp/datapath/dhcp_input.c | 146 +++++ modules/dhcp/datapath/meson.build | 7 + modules/dhcp/meson.build | 7 + modules/meson.build | 1 + smoke/dhcp_test.sh | 70 +++ 16 files changed, 2150 insertions(+), 470 deletions(-) create mode 100644 modules/dhcp/api/gr_dhcp.h create mode 100644 modules/dhcp/api/meson.build create mode 100644 modules/dhcp/cli/dhcp.c create mode 100644 modules/dhcp/cli/meson.build create mode 100644 modules/dhcp/control/client.c create mode 100644 modules/dhcp/control/client.h create mode 100644 modules/dhcp/control/meson.build create mode 100644 modules/dhcp/control/options.c create mode 100644 modules/dhcp/control/packet.c create mode 100644 modules/dhcp/datapath/dhcp_input.c create mode 100644 modules/dhcp/datapath/meson.build create mode 100644 modules/dhcp/meson.build create mode 100755 smoke/dhcp_test.sh 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!"