From nobody Sat Sep 12 16:28:19 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hhxfx1HyLz6sPcJ for ; Sat, 12 Sep 2026 16:28:25 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hhxfw6ZZYz4ZZ5 for ; Sat, 12 Sep 2026 16:28:24 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789230504; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=5eA9Re/7h9JnspYGylH5FNvHPLShX3GsbRNn8zhtlHI=; b=eqgfyR3wvEZq6mrzHTS5pFwUhL2WAYIVKGne30G4mbjCfHOIx8bhMVTiwvGR3zddgOgNnS IRGkksQnzoB4FiSr1jhQQES3YXk/YnVO07b6edKsiJvc1S9nzzJ5O/rbgsOGvOjphM3cEZ EjyuOhsFVI8muKYDAYrKb18XWHcC63iecmSIiPnuyfSKUGh7c/bWroRk6zlAbSlfjMxhFo qcU30HU8TH2G+rK2UH9HXmrlySxXwuCq9xvVeyDoUxzEBA5K6LrJCemohhJWI/w02QMlgX i/Y+gHM1YIWAmrfBmwt55qE6HV5ICzGsj3BMV4zDx5pI16FajjHR6Tlev4rYiQ== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1789230504; b=SqO0O1DBoPxHZYloAUOvxM4zzGLkfTvltYZbxN2s8AFOKS2et/kEbZT8mevuvV9EeyTT80 VQnQiWmomwklB5tGB4aR6avf8vvCgAtUyUmn5lQWB5MDkvU8rQ26CZdEHgw/HhiECm0Yhq ePTwZXSthSJsPNhs/cB/1RMSSX4hV13nwpcpJgJOoKLE0cOZs6wO8mJlV6qblJOFr4XMu4 t2y5BKZUdu1O63h7dfhCwzU8LCkQPx+y7s863ATH+QAigHMg+IYKUYKHqdpY5q6sa1QqY2 vUgH1xecr0hewH11FpjR/I155UvuVCHVr2BT2bLV6zdCluYvyRLQ8QuZmFXi/A== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789230504; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=5eA9Re/7h9JnspYGylH5FNvHPLShX3GsbRNn8zhtlHI=; b=netsfux/F3k9XmYYkyN1JHEJjODprfrb9bls2AP7sHg0Wb5IcER+qKYD6nVe9taCUEKwbD 2W1yOljo+UHn8AR/4stFhsUrpXDIPlAjLHs8RlX37qhLHXCDLbiTMgnHeXCa0kK88e5Ipm BOO2i5tx2QBqRY0X4Jv/NyAc4eluhL3XdnW6Nnafx6o7hxaWli1yz2KK5qCZ1EI+rFCvGb t/dg2B67qrF7U2h5obchMERlg+mDsKNTn7ehflFJyY8ncPJH8KCBneSJfVDC8M9q8O2fSr MzFPw3WIs004saJdvv2Zwp3WaQ/ppZO8vXwUcoZWV1jo+AAfQqrJdmxiKToIqQ== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hhxfw5JHyz15jw for ; Sat, 12 Sep 2026 16:28:24 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 3cb2d by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Sat, 12 Sep 2026 16:28:19 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Pouria Mousavizadeh Tehrani Subject: git: 37fd3fcaaf0e - main - dhclient(8): Add support for IPv6-Only option (RFC 8925) List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: pouria X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 37fd3fcaaf0ee3e1066ef9d4fb8b9245f7288743 Auto-Submitted: auto-generated Date: Sat, 12 Sep 2026 16:28:19 +0000 Message-Id: <6aa57da3.3cb2d.150382a8@gitrepo.freebsd.org> The branch main has been updated by pouria: URL: https://cgit.FreeBSD.org/src/commit/?id=37fd3fcaaf0ee3e1066ef9d4fb8b9245f7288743 commit 37fd3fcaaf0ee3e1066ef9d4fb8b9245f7288743 Author: Pouria Mousavizadeh Tehrani AuthorDate: 2026-09-11 07:17:01 +0000 Commit: Pouria Mousavizadeh Tehrani CommitDate: 2026-09-12 15:39:16 +0000 dhclient(8): Add support for IPv6-Only option (RFC 8925) Accept and validate the ipv6only option. When dhclient receives this option and IPv6 connectivity is available, stop the DHCP configuration process and wait for the duration specified by the option before restarting DHCP discovery. If the address was previously leased, disassociate it and send a DHCPRELEASE packet. Use netlink to check for IPv6 connectivity. Also, unregister ignored options from default PRL. Reviewed by: ziaee, kfv Tested by: Marek Zarychta Relnotes: yes Differential Revision: https://reviews.freebsd.org/D56637 --- sbin/dhclient/Makefile | 10 ++- sbin/dhclient/clparse.c | 31 ++++++++ sbin/dhclient/dhclient.c | 182 +++++++++++++++++++++++++++++++++++++------ sbin/dhclient/dhcp-options.5 | 8 +- sbin/dhclient/dhcp.h | 3 + sbin/dhclient/dhcpd.h | 11 +++ sbin/dhclient/inet6.c | 118 ++++++++++++++++++++++++++++ sbin/dhclient/options.c | 68 +++++++++++++++- sbin/dhclient/tables.c | 5 +- 9 files changed, 409 insertions(+), 27 deletions(-) diff --git a/sbin/dhclient/Makefile b/sbin/dhclient/Makefile index 27c81d9d9e6c..94d67fd115fa 100644 --- a/sbin/dhclient/Makefile +++ b/sbin/dhclient/Makefile @@ -36,7 +36,7 @@ CONFS= dhclient.conf 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 @@ MAN= dhclient.8 dhclient.conf.5 dhclient.leases.5 dhcp-options.5 \ 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 index c883e2a0ddb5..46ea6554849f 100644 --- a/sbin/dhclient/clparse.c +++ b/sbin/dhclient/clparse.c @@ -51,6 +51,27 @@ static struct interface_info *dummy_interfaces; 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 @@ read_client_conf(void) [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 @@ read_client_conf(void) } 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 index 695451088231..88963aa93bf4 100644 --- a/sbin/dhclient/dhclient.c +++ b/sbin/dhclient/dhclient.c @@ -128,6 +128,10 @@ static int no_daemon; 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 res_hnok(const char *dn); 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 @@ main(int argc, char *argv[]) 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 @@ state_reboot(void *ipp) { 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 @@ state_init(void *ipp) 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 @@ freeit: 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 @@ dhcpack(struct packet *packet) 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 @@ dhcpoffer(struct packet *packet) } } + /* 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 @@ send_decline(void *ipp) 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 @@ make_request(struct interface_info *ip, struct client_lease * lease) 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 @@ make_decline(struct interface_info *ip, struct client_lease *lease) 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 @@ make_decline(struct interface_info *ip, struct client_lease *lease) 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 @@ make_decline(struct interface_info *ip, struct client_lease *lease) 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 @@ check_option(struct client_lease *l, int option) 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 index 24604514a016..db89140c1748 100644 --- a/sbin/dhclient/dhcp-options.5 +++ b/sbin/dhclient/dhcp-options.5 @@ -372,6 +372,12 @@ The default route (0.0.0.0) is an illegal destination for a static route. 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 @@ boot from a HTTP server. .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 index 02ea42a66079..3b36038fce46 100644 --- a/sbin/dhclient/dhcp.h +++ b/sbin/dhclient/dhcp.h @@ -92,6 +92,8 @@ struct dhcp_packet { 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 @@ struct dhcp_packet { #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 index c61564067598..bd89ed1be4a9 100644 --- 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 @@ struct iaddr broadcast_addr(struct iaddr, struct iaddr); 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 pidfh *pidfile; 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 index 000000000000..b6ccc6331f74 --- /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 index 4bc26c007921..a3cfdb55003e 100644 --- a/sbin/dhclient/options.c +++ b/sbin/dhclient/options.c @@ -59,6 +59,7 @@ void expand_domain_search(struct packet *packet); 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 @@ parse_options(struct packet *packet) 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 @@ expand_search_domain_name(struct option_data *option, size_t *offset, } } +/* + * 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 index 134b54fa3b56..a3757ebc6b65 100644 --- a/sbin/dhclient/tables.c +++ b/sbin/dhclient/tables.c @@ -173,7 +173,7 @@ struct option dhcp_options[256] = { { "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 @@ unsigned char dhcp_option_default_priority_list[] = { 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,