diff --git a/sbin/dhclient/Makefile b/sbin/dhclient/Makefile --- a/sbin/dhclient/Makefile +++ b/sbin/dhclient/Makefile @@ -36,7 +36,7 @@ PACKAGE=dhclient SRCS= dhclient.c clparse.c alloc.c dispatch.c hash.c bpf.c options.c \ tree.c conflex.c errwarn.c inet.c packet.c convert.c tables.c \ - parse.c privsep.c + parse.c privsep.c inet6.c PROG= dhclient SCRIPTS=dhclient-script @@ -44,6 +44,14 @@ dhclient-script.8 LIBADD= util +.if ${MK_INET6_SUPPORT} != "no" +CFLAGS+=-DINET6 +.endif + +.if ${MK_NETLINK_SUPPORT} == "no" +CFLAGS+=-DWITHOUT_NETLINK +.endif + .if ${MK_DYNAMICROOT} == "no" .warning ${PROG} built without libcasper support .elif ${MK_CASPER} != "no" && !defined(RESCUE) diff --git a/sbin/dhclient/clparse.c b/sbin/dhclient/clparse.c --- a/sbin/dhclient/clparse.c +++ b/sbin/dhclient/clparse.c @@ -51,6 +51,27 @@ static char client_script_name[] = "/sbin/dhclient-script"; +/* + * Options the user ignores must not be asked for either. + * This matters for the IPv6-Only Preferred option. + */ +static void +unrequest_ignored_options(struct client_config *config) +{ + bool ignored[256] = {}; + int i, n; + + for (i = 0; config->ignored_options[i] != 0; i++) + ignored[config->ignored_options[i]] = true; + + for (i = n = 0; i < config->requested_option_count; i++) { + if (ignored[config->requested_options[i]]) + continue; + config->requested_options[n++] = config->requested_options[i]; + } + config->requested_option_count = n; +} + /* * client-conf-file :== client-declarations EOF * client-declarations :== @@ -104,6 +125,15 @@ [top_level_config.requested_option_count++] = DHO_DOMAIN_SEARCH; top_level_config.requested_options [top_level_config.requested_option_count++] = DHO_INTERFACE_MTU; +#ifdef INET6 + /* + * RFC 8925 sec 3.2: The DHCPv4 client on an IPv4-requiring host MUST + * NOT include the IPv6-Only Preferred option code in the Parameter + * Request List. + */ + top_level_config.requested_options + [top_level_config.requested_option_count++] = DHO_IPV6_ONLY; +#endif if ((cfile = fopen(path_dhclient_conf, "r")) != NULL) { do { @@ -137,6 +167,7 @@ } ifi->client->config = config; } + unrequest_ignored_options(ifi->client->config); return (!warnings_occurred); } diff --git a/sbin/dhclient/dhclient.c b/sbin/dhclient/dhclient.c --- a/sbin/dhclient/dhclient.c +++ b/sbin/dhclient/dhclient.c @@ -128,6 +128,10 @@ static int unknown_ok = 1; static int routefd; +#ifndef WITHOUT_NETLINK +struct snl_state nl_ss; +#endif + struct interface_info *ifi; int findproto(char *, int); @@ -141,6 +145,10 @@ int check_search(const char *srch); const char *option_as_string(unsigned int code, unsigned char *data, int len); int fork_privchld(int, int); +static bool ipv6_only_preferred(struct interface_info *, struct packet *); +static void v6only_wait_expired(void *); +static void make_release(struct interface_info *, struct client_lease *); +static void send_release(struct interface_info *, struct client_lease *); #define ROUNDUP(a) \ ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long)) @@ -534,6 +542,14 @@ if (caph_rights_limit(routefd, &rights) < 0) error("can't limit route socket: %m"); +#ifndef WITHOUT_NETLINK + if (!snl_init(&nl_ss, NETLINK_ROUTE)) + error("can't open netlink socket"); + cap_rights_init(&rights, CAP_EVENT, CAP_READ, CAP_WRITE); + if (caph_rights_limit(nl_ss.fd, &rights) < 0) + error("can't limit netlink route socket: %m"); +#endif + endpwent(); setproctitle("%s", ifi->name); @@ -619,6 +635,8 @@ { struct interface_info *ip = ipp; + cancel_timeout(v6only_wait_expired, ip); + /* If we don't remember an active lease, go straight to INIT. */ if (!ip->client->active || ip->client->active->is_bootp) { state_init(ip); @@ -658,6 +676,8 @@ ASSERT_STATE(state, S_INIT); + cancel_timeout(v6only_wait_expired, ip); + /* Make a DHCPDISCOVER packet, and set appropriate per-interface flags. */ make_discover(ip, ip->client->active); @@ -762,8 +782,74 @@ send_request(ip); } -/* state_requesting is called when we receive a DHCPACK message after - having sent out one or more DHCPREQUEST packets. */ +/* + * state_requesting is called when we receive a DHCPACK message after + * RFC 8925, sec 3.2: if the packet carries a valid IPv6-Only Preferred + * option and we have IPv6 connectivity, stop DHCPv4 for V6ONLY_WAIT + * seconds or until a network attachment event, whichever comes first. + * Returns true if DHCPv4 was stopped. + */ +static bool +ipv6_only_preferred(struct interface_info *ip, struct packet *packet) +{ + struct client_lease *lp, *next; + uint32_t v6wait; + + if (packet->options[DHO_IPV6_ONLY].data == NULL) + return (false); + + if (!check_ipv6_connectivity(ip->index)) { + note("IPv6-Only Preferred option received, " + "but we can't verify IPv6 connectivity, ignore"); + return (false); + } + + v6wait = getULong(packet->options[DHO_IPV6_ONLY].data); + note("IPv6-Only Preferred option received (%u seconds), abort", v6wait); + + cancel_timeout(send_discover, ip); + cancel_timeout(send_request, ip); + cancel_timeout(state_selecting, ip); + for (lp = ip->client->offered_leases; lp != NULL; lp = next) { + next = lp->next; + free_client_lease(lp); + } + ip->client->offered_leases = NULL; + + /* + * In INIT-REBOOT the DHCPACK just re-confirmed our old lease. + * Release it so the server doesn't keep the address committed, + * and forget it so the next attempt starts with a DHCPDISCOVER. + */ + if (ip->client->state == S_REBOOTING && ip->client->active != NULL) { + make_release(ip, ip->client->active); + send_release(ip, ip->client->active); + disassoc(ip); + free_client_lease(ip->client->active); + ip->client->active = NULL; + rewrite_client_leases(); + } + + ip->client->state = S_INIT; + if (v6wait < UINT32_MAX) { + struct timespec stop_time, v6wait_left = { + .tv_sec = (time_t)v6wait + }; + timespecadd(&time_now, &v6wait_left, &stop_time); + add_timeout_timespec(stop_time, v6only_wait_expired, ip); + } + go_daemon(); + return (true); +} + +static void +v6only_wait_expired(void *ipp) +{ + struct interface_info *ip = ipp; + + note("V6ONLY_WAIT expired, restarting DHCPv4"); + state_reboot(ip); +} void dhcpack(struct packet *packet) @@ -787,6 +873,11 @@ note("DHCPACK from %s", piaddr(packet->client_addr)); + /* RFC 8925, sec 3.2: only INIT-REBOOT stops, other states keep the lease. */ + if (ip->client->state == S_REBOOTING && + ipv6_only_preferred(ip, packet)) + return; + lease = packet_to_lease(packet); if (!lease) { note("packet_to_lease failed."); @@ -1069,6 +1160,10 @@ } } + /* RFC 8925, sec 3.2: with v6only, don't request the offered address. */ + if (ipv6_only_preferred(ip, packet)) + return; + lease = packet_to_lease(packet); if (!lease) { note("packet_to_lease failed."); @@ -1663,6 +1758,23 @@ ip->client->packet_length, inaddr_any, inaddr_broadcast); } +static void +send_release(struct interface_info *ip, struct client_lease *lease) +{ + struct in_addr from, to; + + /* RFC 2131, sec 4.4.4: DHCPRELEASE is unicast to the server. */ + memcpy(&from, lease->address.iabuf, sizeof(from)); + if (lease->options[DHO_DHCP_SERVER_IDENTIFIER].len == sizeof(to)) + memcpy(&to, lease->options[DHO_DHCP_SERVER_IDENTIFIER].data, sizeof(to)); + else + to = inaddr_broadcast; + + note("DHCPRELEASE on %s to %s port %d", ip->name, inet_ntoa(to), REMOTE_PORT); + send_packet_unpriv(privfd, &ip->client->packet, + ip->client->packet_length, from, to); +} + void make_discover(struct interface_info *ip, struct client_lease *lease) { @@ -1909,24 +2021,26 @@ ip->hw_address.haddr, ip->hw_address.hlen); } -void -make_decline(struct interface_info *ip, struct client_lease *lease) +/* + * Build the packet DHCPDECLINE and DHCPRELEASE share: message type, + * server and client identifiers, plus any options already in options[]. + */ +static void +make_decline_or_release(struct interface_info *ip, + struct client_lease *lease, unsigned char type, + struct tree_cache **options) { - struct tree_cache *options[256], message_type_tree; - struct tree_cache requested_address_tree; - struct tree_cache server_id_tree, client_id_tree; - unsigned char decline = DHCPDECLINE; + struct tree_cache message_type_tree, server_id_tree, client_id_tree; int i; - memset(options, 0, sizeof(options)); memset(&ip->client->packet, 0, sizeof(ip->client->packet)); - /* Set DHCP_MESSAGE_TYPE to DHCPDECLINE */ + /* Set DHCP_MESSAGE_TYPE */ i = DHO_DHCP_MESSAGE_TYPE; options[i] = &message_type_tree; - options[i]->value = &decline; - options[i]->len = sizeof(decline); - options[i]->buf_size = sizeof(decline); + options[i]->value = &type; + options[i]->len = sizeof(type); + options[i]->buf_size = sizeof(type); options[i]->timeout = 0xFFFFFFFF; /* Send back the server identifier... */ @@ -1937,14 +2051,6 @@ options[i]->buf_size = lease->options[i].len; options[i]->timeout = 0xFFFFFFFF; - /* Send back the address we're declining. */ - i = DHO_DHCP_REQUESTED_ADDRESS; - options[i] = &requested_address_tree; - options[i]->value = lease->address.iabuf; - options[i]->len = lease->address.len; - options[i]->buf_size = lease->address.len; - options[i]->timeout = 0xFFFFFFFF; - /* Send the uid if the user supplied one. */ i = DHO_DHCP_CLIENT_IDENTIFIER; if (ip->client->config->send_options[i].len) { @@ -1955,7 +2061,6 @@ options[i]->timeout = 0xFFFFFFFF; } - /* Set up the option buffer... */ ip->client->packet_length = cons_options(NULL, &ip->client->packet, 0, options, 0, 0, 0, NULL, 0); @@ -1983,6 +2088,38 @@ ip->hw_address.haddr, ip->hw_address.hlen); } +void +make_decline(struct interface_info *ip, struct client_lease *lease) +{ + struct tree_cache *options[256], requested_address_tree; + int i; + + memset(options, 0, sizeof(options)); + + /* Send back the address we're declining. */ + i = DHO_DHCP_REQUESTED_ADDRESS; + options[i] = &requested_address_tree; + options[i]->value = lease->address.iabuf; + options[i]->len = lease->address.len; + options[i]->buf_size = lease->address.len; + options[i]->timeout = 0xFFFFFFFF; + + make_decline_or_release(ip, lease, DHCPDECLINE, options); +} + +static void +make_release(struct interface_info *ip, struct client_lease *lease) +{ + struct tree_cache *options[256]; + + memset(options, 0, sizeof(options)); + make_decline_or_release(ip, lease, DHCPRELEASE, options); + + /* RFC 2131, sec 4.4.4: ciaddr carries the address being released. */ + memcpy(&ip->client->packet.ciaddr, lease->address.iabuf, + sizeof(ip->client->packet.ciaddr)); +} + void free_client_lease(struct client_lease *lease) { @@ -2682,6 +2819,7 @@ case DHO_SIP_SERVERS: case DHO_V_I_VENDOR_CLASS: case DHO_V_I_VENDOR_OPTS: + case DHO_IPV6_ONLY: case DHO_END: return (1); case DHO_CLASSLESS_ROUTES: diff --git a/sbin/dhclient/dhcp-options.5 b/sbin/dhclient/dhcp-options.5 --- a/sbin/dhclient/dhcp-options.5 +++ b/sbin/dhclient/dhcp-options.5 @@ -372,6 +372,12 @@ To specify the default route, use the .Ic routers option. +.It Ic option ipv6only Ar uint32 ; +This option specifies the number of seconds for which the client should disable +DHCPv4 configuration provided by the DHCP server. +The IPv6-Only Preferred option will check if the host is IPv6-Only capable or +not. +The time is specified as a 32-bit unsigned integer. .El .Ss Link Layer Parameters per Interface .Bl -tag -width indent @@ -596,7 +602,7 @@ .Xr dhclient 8 , .Xr dhcpd 8 .Rs -.%R "RFC 2131, RFC 2132, RFC 3769" +.%R "RFC 2131, RFC 2132, RFC 3769, RFC 8925" .Re .Sh AUTHORS .An -nosplit diff --git a/sbin/dhclient/dhcp.h b/sbin/dhclient/dhcp.h --- a/sbin/dhclient/dhcp.h +++ b/sbin/dhclient/dhcp.h @@ -92,6 +92,8 @@ extensions field). */ #define DHCP_OPTIONS_COOKIE "\143\202\123\143" +#define MIN_V6ONLY_WAIT 300 /* RFC 8925 */ + /* DHCP Option codes: */ #define DHO_PAD 0 @@ -170,6 +172,7 @@ #define DHO_STREETTALK_SERVER 75 #define DHO_STREETTALK_DA_SERVER 76 #define DHO_DHCP_USER_CLASS_ID 77 +#define DHO_IPV6_ONLY 108 #define DHO_URL 114 #define DHO_DOMAIN_SEARCH 119 #define DHO_SIP_SERVERS 120 diff --git a/sbin/dhclient/dhcpd.h b/sbin/dhclient/dhcpd.h --- a/sbin/dhclient/dhcpd.h +++ b/sbin/dhclient/dhcpd.h @@ -54,6 +54,10 @@ #include #include +#ifndef WITHOUT_NETLINK +#include +#endif + #include #include @@ -357,6 +361,9 @@ int addr_eq(struct iaddr, struct iaddr); char *piaddr(struct iaddr); +/* inet6.c */ +bool check_ipv6_connectivity(uint16_t ifindex); + /* dhclient.c */ extern cap_channel_t *capsyslog; extern const char *path_dhclient_conf; @@ -372,6 +379,10 @@ extern struct interface_info *ifi; +#ifndef WITHOUT_NETLINK +extern struct snl_state nl_ss; +#endif + void dhcpoffer(struct packet *); void dhcpack(struct packet *); void dhcpnak(struct packet *); diff --git a/sbin/dhclient/inet6.c b/sbin/dhclient/inet6.c new file mode 100644 --- /dev/null +++ b/sbin/dhclient/inet6.c @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 Pouria Mousavizadeh Tehrani + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#ifndef WITHOUT_NETLINK +#include +#include +#include +#include +#include +#include +#endif + +#include "dhcpd.h" + +#ifndef WITHOUT_NETLINK +/* + * Check if the interface has a routable IPv6 address, if true + * assume it has IPv6 connectivity. + * Returns true if a unicast IPv6 address with non-link-local scope + * is found, false otherwise. + */ +static bool +check_ipv6_address(struct snl_state *ss, uint16_t ifindex) +{ + struct snl_writer nw; + struct snl_errmsg_data e = {}; + struct snl_parsed_addr addr = {}; + struct nlmsghdr *hdr, *rx_hdr; + struct ifaddrmsg *ifahdr; + + snl_init_writer(ss, &nw); + hdr = snl_create_msg_request(&nw, RTM_GETADDR); + hdr->nlmsg_flags |= NLM_F_DUMP; + ifahdr = snl_reserve_msg_object(&nw, struct ifaddrmsg); + ifahdr->ifa_family = AF_INET6; + ifahdr->ifa_index = ifindex; + + if ((hdr = snl_finalize_msg(&nw)) == NULL || !snl_send_message(ss, hdr)) + return (false); + + while ((rx_hdr = snl_read_reply_multi(ss, hdr->nlmsg_seq, &e)) != NULL) { + struct sockaddr_in6 *sin6; + + if (!snl_parse_nlmsg(ss, rx_hdr, &snl_rtm_addr_parser, &addr)) + continue; + + if (addr.ifa_address != NULL) { + sin6 = (struct sockaddr_in6 *)addr.ifa_address; + if (!IN6_IS_ADDR_LINKLOCAL(&sin6->sin6_addr)) + return (true); + } + } + return (false); +} + +/* + * If a default route is found, assume IPv6 connectivity. + * Returns true if found, false otherwise. + */ +static bool +check_ipv6_defaultroute(struct snl_state *ss) +{ + struct snl_writer nw; + struct snl_parsed_route r = {}; + struct in6_addr in6 = IN6ADDR_ANY_INIT; + struct nlmsghdr *hdr, *rx_hdr; + struct rtmsg *rtmsg; + + snl_init_writer(ss, &nw); + hdr = snl_create_msg_request(&nw, RTM_GETROUTE); + rtmsg = snl_reserve_msg_object(&nw, struct rtmsg); + rtmsg->rtm_family = AF_INET6; + rtmsg->rtm_dst_len = 0; + rtmsg->rtm_type = RTN_UNICAST; + rtmsg->rtm_flags = RTM_F_PREFIX; + snl_add_msg_attr_ip6(&nw, RTA_DST, &in6); + + if ((hdr = snl_finalize_msg(&nw)) == NULL || !snl_send_message(ss, hdr)) + return (false); + + rx_hdr = snl_read_reply(ss, hdr->nlmsg_seq); + if (rx_hdr == NULL || rx_hdr->nlmsg_type != NL_RTM_NEWROUTE) + return (false); + + if (!snl_parse_nlmsg(ss, rx_hdr, &snl_rtm_route_parser, &r)) + return (false); + + if (r.rta_gw == NULL) + return (false); + + return (true); +} +#endif + +bool +check_ipv6_connectivity(uint16_t ifindex __unused) +{ + bool ret = false; + +#ifndef WITHOUT_NETLINK + /* Return true if the interface has a routable ipv6 address */ + ret = check_ipv6_address(&nl_ss, ifindex); + + /* A default route using a link-local address may exist. */ + if (!ret) + ret = check_ipv6_defaultroute(&nl_ss); + snl_clear_lb(&nl_ss); +#endif + + return (ret); +} diff --git a/sbin/dhclient/options.c b/sbin/dhclient/options.c --- a/sbin/dhclient/options.c +++ b/sbin/dhclient/options.c @@ -59,6 +59,7 @@ int find_search_domain_name_len(struct option_data *option, size_t *offset); void expand_search_domain_name(struct option_data *option, size_t *offset, unsigned char **domain_search); +void ipv6_only_option(struct packet *packet); /* @@ -99,10 +100,12 @@ sizeof(packet->raw->sname)); } - /* Expand DHCP Domain Search option. */ + /* Process options */ if (packet->options_valid) { expand_domain_search(packet); + ipv6_only_option(packet); } + } /* @@ -372,6 +375,69 @@ } } +/* + * process ipv6_only option. + * See: RFC 8925 + */ +void +ipv6_only_option(struct packet *packet) +{ + struct option_data *option; + struct client_config *config; + uint32_t val; + bool requested; + + if (packet->options[DHO_IPV6_ONLY].data == NULL) + return; + + option = &packet->options[DHO_IPV6_ONLY]; + config = packet->interface->client->config; + + /* + * RFC 8925, sec 3.1: The client MUST ignore the IPv6-Only Preferred + * option if the length field value is not 4. + */ + if (option->len != 4) { + warning("IPv6-Only preferred option length is invalid"); + goto bad; + } + val = getULong(option->data); + + /* + * RFC 8925, sec 3.2: If the client did not include the IPv6-Only Preferred + * option code in the Parameter Request List in the DHCPDISCOVER or + * DHCPREQUEST message, it MUST ignore the IPv6-Only Preferred option + * in any messages received from the server. + */ + requested = false; + for (int i = 0; i < config->requested_option_count; i++) { + if (config->requested_options[i] == DHO_IPV6_ONLY) { + requested = true; + break; + } + } + if (!requested) { + note("Unwanted IPv6-Only Preferred option received, ignore it"); + goto bad; + } + + /* + * If the IPv6-Only Preferred option returned by the server contains + * a value greater than or equal to MIN_V6ONLY_WAIT, the client SHOULD + * set the V6ONLY_WAIT timer to that value. + * Otherwise, the client SHOULD set the V6ONLY_WAIT timer + * to MIN_V6ONLY_WAIT. + */ + if (val < MIN_V6ONLY_WAIT) + putULong(option->data, MIN_V6ONLY_WAIT); + + return; +bad: + free(option->data); + option->len = 0; + option->data = NULL; +} + /* * cons options into a big buffer, and then split them out into the * three separate buffers if needed. This allows us to cons up a set of diff --git a/sbin/dhclient/tables.c b/sbin/dhclient/tables.c --- a/sbin/dhclient/tables.c +++ b/sbin/dhclient/tables.c @@ -173,7 +173,7 @@ { "option-105", "X", &dhcp_universe, 105 }, { "option-106", "X", &dhcp_universe, 106 }, { "option-107", "X", &dhcp_universe, 107 }, - { "option-108", "X", &dhcp_universe, 108 }, + { "ipv6only", "L", &dhcp_universe, 108 }, { "option-109", "X", &dhcp_universe, 109 }, { "option-110", "X", &dhcp_universe, 110 }, { "option-111", "X", &dhcp_universe, 111 }, @@ -403,11 +403,12 @@ DHO_DHCP_USER_CLASS_ID, DHO_DOMAIN_SEARCH, DHO_URL, + DHO_IPV6_ONLY, /* Presently-undefined options... */ 62, 63, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, - 106, 107, 108, 109, 110, 111, 112, 113, 115, 116, 117, + 106, 107, 109, 110, 111, 112, 113, 115, 116, 117, 118, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154,