dhcp: support the IPv6-Only Preferred option

Add support for handling the IPv6-Only Preferred option. When enabled,
the client adds the option code to the "Parameter Request List" option
of the DHCPDISCOVER and DHCPREQUEST messages. If the server sends the
option back in the DHCPOFFER and DHCPACK, the host stops the DHCP
client for the time interval specified in the option. After the
timeout expires, DHCP is restarted.
This commit is contained in:
Beniamino Galvani
2024-10-08 11:33:12 +02:00
parent 1fa08e7d1b
commit 022b7ac184
6 changed files with 133 additions and 10 deletions

View File

@@ -91,6 +91,9 @@ typedef struct _NMDhcpClientPrivate {
union { union {
struct { struct {
/* Timer for restarting DHCP after the IPv6-only timeout */
GSource *ipv6_only_restart_source;
struct { struct {
NML3CfgCommitTypeHandle *l3cfg_commit_handle; NML3CfgCommitTypeHandle *l3cfg_commit_handle;
GSource *done_source; GSource *done_source;
@@ -1403,6 +1406,51 @@ nm_dhcp_client_start(NMDhcpClient *self, GError **error)
/*****************************************************************************/ /*****************************************************************************/
static gboolean
ipv6_only_restart_timeout_cb(gpointer user_data)
{
NMDhcpClient *self = user_data;
NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
gs_free_error GError *error = NULL;
nm_assert(priv->config.addr_family == AF_INET);
nm_clear_g_source_inst(&priv->v4.ipv6_only_restart_source);
if (!nm_dhcp_client_start(self, &error)) {
_LOGW("failed to restart the DHCP client after the IPv6-only timeout: %s", error->message);
_emit_notify(self,
NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
.it_looks_bad.reason = error->message);
}
return G_SOURCE_CONTINUE;
}
/**
* nm_dhcp_client_schedule_ipv6_only_restart():
* @self: the client
* @timeout: the raw value from the DHCP option
*
* Stops the DHCPv4 client and restarts it after the timeout announced
* by the "IPv6-Only preferred" option.
*/
void
nm_dhcp_client_schedule_ipv6_only_restart(NMDhcpClient *self, guint timeout)
{
NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
nm_assert(priv->config.addr_family == AF_INET);
nm_assert(!priv->is_stopped);
timeout = NM_MAX(NM_DHCP_MIN_V6ONLY_WAIT_DEFAULT, timeout);
_LOGI("received option \"ipv6-only-preferred\": stopping DHCPv4 for %u seconds", timeout);
nm_dhcp_client_stop(self, FALSE);
nm_clear_g_source_inst(&priv->no_lease_timeout_source);
priv->v4.ipv6_only_restart_source =
nm_g_timeout_add_seconds_source(timeout, ipv6_only_restart_timeout_cb, self);
}
void void
nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name) nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name)
{ {
@@ -1477,6 +1525,8 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
nm_clear_pointer(&priv->effective_client_id, g_bytes_unref); nm_clear_pointer(&priv->effective_client_id, g_bytes_unref);
nm_clear_g_source_inst(&priv->previous_lease_timeout_source); nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
if (priv->config.addr_family == AF_INET)
nm_clear_g_source_inst(&priv->v4.ipv6_only_restart_source);
priv->is_stopped = TRUE; priv->is_stopped = TRUE;
@@ -1978,7 +2028,9 @@ dispose(GObject *object)
nm_clear_g_source_inst(&priv->previous_lease_timeout_source); nm_clear_g_source_inst(&priv->previous_lease_timeout_source);
nm_clear_g_source_inst(&priv->no_lease_timeout_source); nm_clear_g_source_inst(&priv->no_lease_timeout_source);
if (!NM_IS_IPv4(priv->config.addr_family)) { if (priv->config.addr_family == AF_INET) {
nm_clear_g_source_inst(&priv->v4.ipv6_only_restart_source);
} else {
nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source); nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source);
nm_clear_g_source_inst(&priv->v6.dad_timeout_source); nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
} }

View File

@@ -27,6 +27,8 @@
#define NM_DHCP_CLIENT_NOTIFY "dhcp-notify" #define NM_DHCP_CLIENT_NOTIFY "dhcp-notify"
#define NM_DHCP_MIN_V6ONLY_WAIT_DEFAULT 300u /* (seconds). RFC 8925, section 3.4 */
typedef enum { typedef enum {
NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED, NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED,
@@ -172,6 +174,8 @@ typedef struct {
/* Whether to send or not the client identifier */ /* Whether to send or not the client identifier */
bool send_client_id : 1; bool send_client_id : 1;
/* Request and honor the "IPv6-only Preferred" option (RFC 8925).*/
bool ipv6_only_preferred : 1;
} v4; } v4;
struct { struct {
/* If set, the DUID from the connection is used; otherwise /* If set, the DUID from the connection is used; otherwise
@@ -246,6 +250,8 @@ const NML3ConfigData *nm_dhcp_client_get_lease(NMDhcpClient *self);
void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release); void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release);
void nm_dhcp_client_schedule_ipv6_only_restart(NMDhcpClient *self, guint timeout);
/* Backend helpers for subclasses */ /* Backend helpers for subclasses */
void nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name); void nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name);

View File

@@ -462,6 +462,11 @@ dhclient_start(NMDhcpClient *client,
"to LOWDELAY (0x10)."); "to LOWDELAY (0x10).");
} }
if (client_config->v4.ipv6_only_preferred) {
_LOGW("the dhclient backend does not support the \"IPv6-Only Preferred\" option; ignoring "
"it");
}
/* Usually the system bus address is well-known; but if it's supposed /* Usually the system bus address is well-known; but if it's supposed
* to be something else, we need to push it to dhclient, since dhclient * to be something else, we need to push it to dhclient, since dhclient
* sanitizes the environment it gives the action scripts. * sanitizes the environment it gives the action scripts.

View File

@@ -168,6 +168,27 @@ lease_option_consume_route(const uint8_t **datap,
/*****************************************************************************/ /*****************************************************************************/
static gboolean
lease_get_ipv6_only_wait_time(NDhcp4ClientLease *lease, guint32 *out_val, const char *iface)
{
const uint8_t *data;
size_t len;
int r;
r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_IPV6_ONLY_PREFERRED, &data, &len);
if (r == 0
&& nm_dhcp_lease_data_parse_u32(data,
len,
out_val,
iface,
AF_INET,
NM_DHCP_OPTION_DHCP4_IPV6_ONLY_PREFERRED)) {
return TRUE;
}
return FALSE;
}
static gboolean static gboolean
lease_parse_address(NMDhcpNettools *self /* for logging context only */, lease_parse_address(NMDhcpNettools *self /* for logging context only */,
NDhcp4ClientLease *lease, NDhcp4ClientLease *lease,
@@ -929,6 +950,22 @@ bound4_handle(NMDhcpNettools *self, guint event, NDhcp4ClientLease *lease)
l3cd); l3cd);
} }
static gboolean
dhcp4_handle_ipv6_only(NMDhcpNettools *self, NDhcp4ClientEvent *event)
{
NMDhcpClient *client = NM_DHCP_CLIENT(self);
guint32 val;
if (nm_dhcp_client_get_config(client)->v4.ipv6_only_preferred
&& lease_get_ipv6_only_wait_time(event->offer.lease,
&val,
nm_dhcp_client_get_iface(client))) {
nm_dhcp_client_schedule_ipv6_only_restart(client, val);
return TRUE;
}
return FALSE;
}
static void static void
dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event) dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
{ {
@@ -962,18 +999,23 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
return; return;
} }
n_dhcp4_client_lease_get_yiaddr(event->offer.lease, &yiaddr);
if (yiaddr.s_addr == INADDR_ANY) {
_LOGD("selecting lease failed: no yiaddr address");
return;
}
if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) { if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) {
_LOGD("server-id %s is in the reject-list, ignoring", _LOGD("server-id %s is in the reject-list, ignoring",
nm_inet_ntop(AF_INET, &server_id, addr_str)); nm_inet_ntop(AF_INET, &server_id, addr_str));
return; return;
} }
if (dhcp4_handle_ipv6_only(self, event))
return;
/* Check yiaddr only after evaluating the ipv6-only-preferred option, because if
* the option is present yiaddr can be zero. */
n_dhcp4_client_lease_get_yiaddr(event->offer.lease, &yiaddr);
if (yiaddr.s_addr == INADDR_ANY) {
_LOGD("selecting lease failed: no yiaddr address");
return;
}
if (!_nm_dhcp_client_accept_offer(NM_DHCP_CLIENT(self), &yiaddr.s_addr)) { if (!_nm_dhcp_client_accept_offer(NM_DHCP_CLIENT(self), &yiaddr.s_addr)) {
/* We don't log about this, the parent class is expected to notify about the reasons. */ /* We don't log about this, the parent class is expected to notify about the reasons. */
return; return;
@@ -1001,6 +1043,17 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
_nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL); _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
return; return;
case N_DHCP4_CLIENT_EVENT_GRANTED: case N_DHCP4_CLIENT_EVENT_GRANTED:
if (dhcp4_handle_ipv6_only(self, event)) {
/* RFC 8925 says that when the client receives a DHCPACK, it should
* stop the client; but only in the INIT-REBOOT (actually, REBOOTING)
* state, otherwise it should continue to use the address.
* The GRANTED event is emitted both in the REBOOTING and REQUESTING
* state; however if we got the IPv6-only option in the OFFER we have
* already stopped the client. Therefore this point can be reached
* only in the REBOOTING state.
*/
return;
}
bound4_handle(self, event->event, event->granted.lease); bound4_handle(self, event->event, event->granted.lease);
return; return;
case N_DHCP4_CLIENT_EVENT_EXTENDED: case N_DHCP4_CLIENT_EVENT_EXTENDED:
@@ -1377,6 +1430,11 @@ ip4_start(NMDhcpClient *client, GError **error)
} }
} }
if (client_config->v4.ipv6_only_preferred) {
n_dhcp4_client_probe_config_request_option(config,
NM_DHCP_OPTION_DHCP4_IPV6_ONLY_PREFERRED);
}
if (client_config->mud_url) { if (client_config->mud_url) {
r = n_dhcp4_client_probe_config_append_option(config, r = n_dhcp4_client_probe_config_append_option(config,
NM_DHCP_OPTION_DHCP4_MUD_URL, NM_DHCP_OPTION_DHCP4_MUD_URL,

View File

@@ -113,6 +113,7 @@ const NMDhcpOption _nm_dhcp_option_dhcp4_options[] = {
REQ(NM_DHCP_OPTION_DHCP4_PXE_CLIENT_ID, "pxe_client_id", FALSE), REQ(NM_DHCP_OPTION_DHCP4_PXE_CLIENT_ID, "pxe_client_id", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_UAP_SERVERS, "uap_servers", FALSE), REQ(NM_DHCP_OPTION_DHCP4_UAP_SERVERS, "uap_servers", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC, "geoconf_civic", FALSE), REQ(NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC, "geoconf_civic", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_IPV6_ONLY_PREFERRED, "ipv6_only_preferred", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS, "netinfo_server_address", FALSE), REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS, "netinfo_server_address", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG, "netinfo_server_tag", FALSE), REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG, "netinfo_server_tag", FALSE),
REQ(NM_DHCP_OPTION_DHCP4_DEFAULT_URL, "default_url", FALSE), REQ(NM_DHCP_OPTION_DHCP4_DEFAULT_URL, "default_url", FALSE),
@@ -183,11 +184,11 @@ static const NMDhcpOption *const _sorted_options_4[G_N_ELEMENTS(_nm_dhcp_option_
A(13), A(53), A(54), A(55), A(57), A(58), A(59), A(60), A(61), A(62), A(63), A(64), A(13), A(53), A(54), A(55), A(57), A(58), A(59), A(60), A(61), A(62), A(63), A(64),
A(65), A(66), A(67), A(68), A(69), A(70), A(71), A(72), A(73), A(74), A(75), A(76), A(65), A(66), A(67), A(68), A(69), A(70), A(71), A(72), A(73), A(74), A(75), A(76),
A(77), A(78), A(79), A(80), A(81), A(82), A(83), A(84), A(85), A(86), A(87), A(56), A(77), A(78), A(79), A(80), A(81), A(82), A(83), A(84), A(85), A(86), A(87), A(56),
A(88), A(89), A(90), A(91), A(92), A(93), A(14), A(7), A(94), A(95), A(96), A(97), A(88), A(89), A(90), A(91), A(92), A(93), A(94), A(14), A(7), A(95), A(96), A(97),
A(98), A(99), A(100), A(101), A(102), A(103), A(104), A(105), A(106), A(107), A(108), A(109), A(98), A(99), A(100), A(101), A(102), A(103), A(104), A(105), A(106), A(107), A(108), A(109),
A(110), A(111), A(112), A(113), A(114), A(115), A(116), A(117), A(118), A(119), A(120), A(121), A(110), A(111), A(112), A(113), A(114), A(115), A(116), A(117), A(118), A(119), A(120), A(121),
A(122), A(123), A(124), A(125), A(126), A(127), A(128), A(129), A(130), A(131), A(132), A(133), A(122), A(123), A(124), A(125), A(126), A(127), A(128), A(129), A(130), A(131), A(132), A(133),
A(134), A(15), A(135), A(136), A(16), A(137), A(138), A(139), A(140), A(141), A(142), A(134), A(135), A(15), A(136), A(137), A(16), A(138), A(139), A(140), A(141), A(142), A(143),
#undef A #undef A
}; };

View File

@@ -93,6 +93,7 @@ typedef enum {
NM_DHCP_OPTION_DHCP4_UAP_SERVERS = 98, NM_DHCP_OPTION_DHCP4_UAP_SERVERS = 98,
NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC = 99, NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC = 99,
NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE = 101, NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE = 101,
NM_DHCP_OPTION_DHCP4_IPV6_ONLY_PREFERRED = 108,
NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS = 112, NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS = 112,
NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG = 113, NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG = 113,
NM_DHCP_OPTION_DHCP4_DEFAULT_URL = 114, NM_DHCP_OPTION_DHCP4_DEFAULT_URL = 114,
@@ -188,7 +189,7 @@ typedef struct {
bool include; bool include;
} NMDhcpOption; } NMDhcpOption;
extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[143]; extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[144];
extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[18]; extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[18];
static inline const char * static inline const char *