diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 7c77c9f39a..a16603320a 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -10299,6 +10299,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family) .v4 = { .request_broadcast = request_broadcast, + .acd_timeout_msec = _prop_get_ipv4_dad_timeout(self), }, }; diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c index d6cfbf3f71..64fa0abace 100644 --- a/src/core/dhcp/nm-dhcp-client.c +++ b/src/core/dhcp/nm-dhcp-client.c @@ -32,6 +32,38 @@ /*****************************************************************************/ +/* This is how long we do ACD for each entry and reject new offers for + * the same address. Note that the maximum ACD timeout is limited to 30 seconds + * (NM_ACD_TIMEOUT_MAX_MSEC). + **/ +#define ACD_REGLIST_GRACE_PERIOD_MSEC 300000u + +G_STATIC_ASSERT(ACD_REGLIST_GRACE_PERIOD_MSEC > (NM_ACD_TIMEOUT_MAX_MSEC + 1000)); + +#define ACD_REGLIST_MAX_ENTRIES 30 + +/* To do ACD for an address (new lease), we will register a NML3ConfigData + * with l3cfg. After ACD completes, we still continue having NML3Cfg + * watch that address, for ACD_REGLIST_GRACE_PERIOD_MSEC. The reasons are: + * + * - the caller is supposed to actually configure the address right after + * ACD passed. We would not want to drop the ACD state before the caller + * got a chance to do that. + * - when ACD fails, we decline the address and expect the DHCP client + * to present a new lease. We may want to outright reject the address, + * if ACD is bad. Thus, we want to keep running ACD for the address a bit + * longer, so that future requests for the same address can be rejected. + * + * This data structure is used for tracking the registered ACD address. + */ +typedef struct { + const NML3ConfigData *l3cd; + gint64 expiry_msec; + in_addr_t addr; +} AcdRegListData; + +/*****************************************************************************/ + enum { SIGNAL_NOTIFY, LAST_SIGNAL, @@ -42,14 +74,47 @@ static guint signals[LAST_SIGNAL] = {0}; NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient, PROP_CONFIG, ); typedef struct _NMDhcpClientPrivate { - NMDhcpClientConfig config; - const NML3ConfigData *l3cd; - GSource *no_lease_timeout_source; - GSource *watch_source; - GBytes *effective_client_id; + NMDhcpClientConfig config; + + /* This is the "next" data. That is, the one what was received last via + * _nm_dhcp_client_notify(), but which is currently pending on ACD. */ + const NML3ConfigData *l3cd_next; + + /* This is the currently exposed data. It passed ACD (or no ACD was performed), + * and is set from l3cd_next. */ + const NML3ConfigData *l3cd_curr; + + GSource *no_lease_timeout_source; + GSource *watch_source; + GBytes *effective_client_id; union { struct { + struct { + NML3CfgCommitTypeHandle *l3cfg_commit_handle; + GSource *done_source; + + /* When we do ACD for a l3cd lease, we will keep running ACD for + * the grace period ACD_REGLIST_GRACE_PERIOD_MSEC, even if we already + * determined the state. There are two reasons for that: + * + * - after ACD completes we notify the lease to the user, who is supposed + * to configure the address in NML3Cfg. If we were already removing the + * ACD state from NML3Cfg, ACD might need to start over. Instead, when + * the caller tries to configure the address, ACD state is already good. + * + * - if we decline on ACD offer, we may want to keep running and + * select other offers. Offers for which we just failed ACD (within + * ACD_REGLIST_GRACE_PERIOD_MSEC) are rejected. See _nm_dhcp_client_accept_offer(). + * For that, we keep monitoring the ACD state for up to ACD_REGLIST_MAX_ENTRIES + * addresses, to not restart and select the same lease twice in a row. + */ + GArray *reglist; + GSource *reglist_timeout_source; + + in_addr_t addr; + NMOptionBool state; + } acd; struct { GDBusMethodInvocation *invocation; } bound; @@ -75,16 +140,22 @@ G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT) /*****************************************************************************/ +#define L3CD_ACD_TAG(priv) (&(priv)->v4.acd.addr) + static gboolean _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error); -_nm_unused static gboolean _dhcp_client_decline(NMDhcpClient *self, - const NML3ConfigData *l3cd, - const char *error_message, - GError **error); +static gboolean _dhcp_client_decline(NMDhcpClient *self, + const NML3ConfigData *l3cd, + const char *error_message, + GError **error); static void l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self); +static void _acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec); + +static void _acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log); + /*****************************************************************************/ /* we use pid=-1 for invalid PIDs. Ensure that pid_t can hold negative values. */ @@ -197,7 +268,8 @@ l3_cfg_notify_check_connected(NMDhcpClient *self) NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); gboolean do_connect; - do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address; + do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address + | (NM_IS_IPv4(priv->config.addr_family) && priv->v4.acd.l3cfg_commit_handle); if (!do_connect) { nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id); @@ -303,6 +375,333 @@ _no_lease_timeout_schedule(NMDhcpClient *self) /*****************************************************************************/ +static void +_acd_state_reset(NMDhcpClient *self, gboolean forget_addr, gboolean forget_reglist) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + if (!NM_IS_IPv4(priv->config.addr_family)) + return; + + if (priv->v4.acd.addr != INADDR_ANY) { + nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle); + l3_cfg_notify_check_connected(self); + nm_clear_g_source_inst(&priv->v4.acd.done_source); + if (forget_addr) { + priv->v4.acd.addr = INADDR_ANY; + priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT; + } + } else + nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT); + + if (forget_reglist) { + guint n; + + while ((n = nm_g_array_len(priv->v4.acd.reglist)) > 0) + _acd_reglist_data_remove(self, n - 1, TRUE); + } + + nm_assert(!priv->v4.acd.l3cfg_commit_handle); + nm_assert(!priv->v4.acd.done_source); + nm_assert(!forget_reglist + || !nm_l3cfg_remove_config_all(priv->config.l3cfg, L3CD_ACD_TAG(priv))); +} + +static gboolean +_acd_complete_on_idle_cb(gpointer user_data) +{ + NMDhcpClient *self = user_data; + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + nm_assert(NM_IS_IPv4(priv->config.addr_family)); + nm_assert(priv->v4.acd.addr != INADDR_ANY); + nm_assert(!priv->v4.acd.l3cfg_commit_handle); + nm_assert(priv->l3cd_next); + + _acd_state_reset(self, FALSE, FALSE); + + _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_BOUND, priv->l3cd_next); + + return G_SOURCE_CONTINUE; +} + +#define _acd_reglist_data_get(priv, idx) \ + nm_g_array_index_p((priv)->v4.acd.reglist, AcdRegListData, (idx)) + +static guint +_acd_reglist_data_find(NMDhcpClientPrivate *priv, in_addr_t addr_needle) +{ + const guint n = nm_g_array_len(priv->v4.acd.reglist); + guint i; + + nm_assert(addr_needle != INADDR_ANY); + + for (i = 0; i < n; i++) { + AcdRegListData *reglist_data = _acd_reglist_data_get(priv, i); + + if (reglist_data->addr == addr_needle) + return i; + } + return G_MAXUINT; +} + +static void +_acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + AcdRegListData *reglist_data; + + nm_assert(idx < nm_g_array_len(priv->v4.acd.reglist)); + + reglist_data = _acd_reglist_data_get(priv, idx); + + if (do_log) { + char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN]; + + _LOGD("acd: drop check for address %s (l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")", + _nm_utils_inet4_ntop(reglist_data->addr, sbuf_addr), + NM_HASH_OBFUSCATE_PTR(reglist_data->l3cd)); + } + + if (!nm_l3cfg_remove_config(priv->config.l3cfg, L3CD_ACD_TAG(priv), reglist_data->l3cd)) + nm_assert_not_reached(); + + nm_clear_l3cd(®list_data->l3cd); + + nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_UPDATE); + + g_array_remove_index(priv->v4.acd.reglist, idx); + + if (priv->v4.acd.reglist->len == 0) { + nm_clear_pointer(&priv->v4.acd.reglist, g_array_unref); + nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source); + } +} + +static gboolean +_acd_reglist_timeout_cb(gpointer user_data) +{ + NMDhcpClient *self = user_data; + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + gint64 now_msec; + + nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source); + + now_msec = nm_utils_get_monotonic_timestamp_msec(); + + while (nm_g_array_len(priv->v4.acd.reglist) > 0) { + AcdRegListData *reglist_data = _acd_reglist_data_get(priv, 0); + + if (reglist_data->expiry_msec > now_msec) + break; + + _acd_reglist_data_remove(self, 0, TRUE); + } + + _acd_reglist_timeout_reschedule(self, now_msec); + + return G_SOURCE_CONTINUE; +} + +static void +_acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + AcdRegListData *reglist_data; + + if (nm_g_array_len(priv->v4.acd.reglist) == 0) { + nm_assert(!priv->v4.acd.reglist_timeout_source); + return; + } + + if (priv->v4.acd.reglist_timeout_source) { + /* already pending. As we only add new elements with a *later* + * expiry, we don't need to ever cancel a pending timer. Worst + * case, the timer fires, and there is nothing to do and we + * reschedule. */ + return; + } + + now_msec = nm_utils_get_monotonic_timestamp_msec(); + + reglist_data = _acd_reglist_data_get(priv, 0); + + nm_assert(reglist_data->expiry_msec > now_msec); + + priv->v4.acd.reglist_timeout_source = + nm_g_timeout_add_source(reglist_data->expiry_msec - now_msec, + _acd_reglist_timeout_cb, + self); +} + +static void +_acd_check_lease(NMDhcpClient *self, NMOptionBool *out_acd_state) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN]; + in_addr_t addr; + gboolean addr_changed = FALSE; + guint idx; + gint64 now_msec; + + if (!NM_IS_IPv4(priv->config.addr_family)) + goto handle_no_acd; + + if (!priv->l3cd_next) + goto handle_no_acd; + + /* an IPv4 lease is always expected to have exactly one address. */ + nm_assert(nm_l3_config_data_get_num_addresses(priv->l3cd_next, AF_INET) == 1); + + if (priv->config.v4.acd_timeout_msec == 0) + goto handle_no_acd; + + addr = NMP_OBJECT_CAST_IP4_ADDRESS( + nm_l3_config_data_get_first_obj(priv->l3cd_next, NMP_OBJECT_TYPE_IP4_ADDRESS, NULL)) + ->address; + nm_assert(addr != INADDR_ANY); + + nm_clear_g_source_inst(&priv->v4.acd.done_source); + + if (priv->v4.acd.state != NM_OPTION_BOOL_DEFAULT && priv->v4.acd.addr == addr) { + /* the ACD state is already determined. Return right away. */ + nm_assert(!priv->v4.acd.l3cfg_commit_handle); + *out_acd_state = !!priv->v4.acd.state; + return; + } + + if (priv->v4.acd.addr != addr) { + addr_changed = TRUE; + priv->v4.acd.addr = addr; + } + + _LOGD("acd: %s check for address %s (timeout %u msec, l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")", + addr_changed ? "add" : "update", + _nm_utils_inet4_ntop(addr, sbuf_addr), + priv->config.v4.acd_timeout_msec, + NM_HASH_OBFUSCATE_PTR(priv->l3cd_next)); + + priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT; + + if (nm_l3cfg_add_config(priv->config.l3cfg, + L3CD_ACD_TAG(priv), + FALSE, + priv->l3cd_next, + NM_L3CFG_CONFIG_PRIORITY_IPV4LL, + 0, + 0, + NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4, + NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6, + 0, + 0, + NM_DNS_PRIORITY_DEFAULT_NORMAL, + NM_DNS_PRIORITY_DEFAULT_NORMAL, + NM_L3_ACD_DEFEND_TYPE_ONCE, + NM_MIN(priv->config.v4.acd_timeout_msec, NM_ACD_TIMEOUT_MAX_MSEC), + NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD, + NM_L3_CONFIG_MERGE_FLAGS_NONE)) + addr_changed = TRUE; + + if (!priv->v4.acd.reglist) + priv->v4.acd.reglist = g_array_new(FALSE, FALSE, sizeof(AcdRegListData)); + + idx = _acd_reglist_data_find(priv, addr); + + now_msec = nm_utils_get_monotonic_timestamp_msec(); + + g_array_append_val(priv->v4.acd.reglist, + ((AcdRegListData){ + .l3cd = nm_l3_config_data_ref(priv->l3cd_next), + .addr = addr, + .expiry_msec = now_msec + ACD_REGLIST_GRACE_PERIOD_MSEC, + })); + + if (idx != G_MAXUINT) { + /* we already tracked this "addr". We don't need to track it twice, + * forget about this one. This also has the effect, that we will + * always append the new entry to the list (so the list + * stays sorted by the increasing timestamp). */ + _acd_reglist_data_remove(self, idx, FALSE); + } + + if (priv->v4.acd.reglist->len > ACD_REGLIST_MAX_ENTRIES) { + /* rate limit how many addresses we track for ACD. */ + _acd_reglist_data_remove(self, 0, TRUE); + } + + _acd_reglist_timeout_reschedule(self, now_msec); + + if (!priv->v4.acd.l3cfg_commit_handle) { + priv->v4.acd.l3cfg_commit_handle = + nm_l3cfg_commit_type_register(priv->config.l3cfg, + NM_L3_CFG_COMMIT_TYPE_UPDATE, + NULL, + "dhcp4-acd"); + l3_cfg_notify_check_connected(self); + } + + if (addr_changed) + nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO); + + /* ACD is started/pending... */ + nm_assert(priv->v4.acd.addr != INADDR_ANY); + nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT); + nm_assert(priv->v4.acd.l3cfg_commit_handle); + nm_assert(priv->l3cfg_notify.id); + *out_acd_state = NM_OPTION_BOOL_DEFAULT; + return; + +handle_no_acd: + /* Indicate that ACD is good (or disabled) by returning TRUE. */ + _acd_state_reset(self, TRUE, FALSE); + *out_acd_state = NM_OPTION_BOOL_TRUE; + return; +} + +/*****************************************************************************/ + +gboolean +_nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN]; + NMIPAddr yiaddr; + const NML3AcdAddrInfo *acd_info; + + if (!NM_IS_IPv4(priv->config.addr_family)) + return nm_assert_unreachable_val(FALSE); + + if (priv->config.v4.acd_timeout_msec == 0) { + /* ACD is disabled. Note that we might track the address for other + * reasons and have information about the ACD state below. But + * with ACD disabled, we always ignore that information. */ + return TRUE; + } + + nm_ip_addr_set(priv->config.addr_family, &yiaddr, p_yiaddr); + + /* Note that once we do ACD for a certain address, even after completing + * it, we keep the l3cd registered in NML3Cfg for ACD_REGLIST_GRACE_PERIOD_MSEC + * The idea is, that we don't yet turn off ACD for a grace period, so that + * we can avoid selecting the same lease again. + * + * Note that we even check whether we have an ACD state if priv->v4.acd.reglist + * is empty. Maybe for odd reasons, we track ACD for the address already. */ + + acd_info = nm_l3cfg_get_acd_addr_info(priv->config.l3cfg, yiaddr.addr4); + + if (!acd_info) + return TRUE; + + if (!NM_IN_SET(acd_info->state, NM_L3_ACD_ADDR_STATE_USED, NM_L3_ACD_ADDR_STATE_CONFLICT)) + return TRUE; + + _LOGD("offered lease rejected: address %s failed ACD check", + _nm_utils_inet4_ntop(yiaddr.addr4, sbuf_addr)); + + return FALSE; +} + void _nm_dhcp_client_notify(NMDhcpClient *self, NMDhcpClientEventType client_event_type, @@ -310,6 +709,8 @@ _nm_dhcp_client_notify(NMDhcpClient *self, { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); GHashTable *options; + gboolean l3cd_changed; + NMOptionBool acd_state; const int IS_IPv4 = NM_IS_IPv4(priv->config.addr_family); nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL; char sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE]; @@ -340,8 +741,7 @@ _nm_dhcp_client_notify(NMDhcpClient *self, nm_dhcp_client_event_type_to_string(client_event_type), NM_PRINT_FMT_QUOTED2(l3cd, ", l3cd=", NM_HASH_OBFUSCATE_PTR_STR(l3cd, sbuf1), "")); - if (l3cd) - nm_l3_config_data_seal(l3cd); + nm_l3_config_data_seal(l3cd); if (client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT) watch_cleanup(self); @@ -350,34 +750,40 @@ _nm_dhcp_client_notify(NMDhcpClient *self, /* nm_dhcp_utils_merge_new_dhcp6_lease() relies on "life_starts" option * for merging, which is only set by dhclient. Internal client never sets that, * but it supports multiple IP addresses per lease. */ - if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd, l3cd, &l3cd_merged)) { + if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd_next, l3cd, &l3cd_merged)) { _LOGD("lease merged with existing one"); l3cd = nm_l3_config_data_seal(l3cd_merged); } } - if (priv->l3cd == l3cd) - return; - if (l3cd) { nm_clear_g_source_inst(&priv->no_lease_timeout_source); - } else { - if (priv->l3cd) - _no_lease_timeout_schedule(self); + } else + _no_lease_timeout_schedule(self); + + l3cd_changed = nm_l3_config_data_reset(&priv->l3cd_next, l3cd); + + _acd_check_lease(self, &acd_state); + + options = priv->l3cd_next ? nm_dhcp_lease_get_options( + nm_l3_config_data_get_dhcp_lease(priv->l3cd_next, priv->config.addr_family)) + : NULL; + + if (_LOGI_ENABLED()) { + const char *req_str = + IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS) + : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS); + const char *addr = nm_g_hash_table_lookup(options, req_str); + + _LOGI("state changed %s%s%s%s", + priv->l3cd_next ? "new lease" : "no lease", + NM_PRINT_FMT_QUOTED2(addr, ", address=", addr, ""), + acd_state == NM_OPTION_BOOL_DEFAULT ? ", acd pending" + : (acd_state ? "" : ", acd conflict")); } - /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API. - * The internals like the state should not be exposed (or possibly dropped in large - * parts). */ - - nm_l3_config_data_reset(&priv->l3cd, l3cd); - - options = l3cd ? nm_dhcp_lease_get_options( - nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family)) - : NULL; - if (_LOGD_ENABLED()) { - if (options) { + if (l3cd_changed && options) { gs_free const char **keys = NULL; guint nkeys; guint i; @@ -391,41 +797,33 @@ _nm_dhcp_client_notify(NMDhcpClient *self, } } - if (_LOGI_ENABLED()) { - const char *req_str = - IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS) - : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS); - const char *addr = nm_g_hash_table_lookup(options, req_str); - - _LOGI("state changed %s%s%s%s", - priv->l3cd ? "new lease" : "no lease", - NM_PRINT_FMT_QUOTED(addr, ", address=", addr, "", "")); + if (acd_state == NM_OPTION_BOOL_DEFAULT) { + /* ACD is in progress... */ + return; } - /* FIXME(l3cfg:dhcp:acd): NMDhcpClient must also do ACD. It needs acd_timeout_msec - * as a configuration parameter (in NMDhcpClientConfig). When ACD is enabled, - * when a new lease gets announced, it must first use NML3Cfg to run ACD on the - * interface (the previous lease -- if any -- will still be used at that point). - * If ACD fails, we call _dhcp_client_decline() and try to get a different - * lease. - * If ACD passes, we need to notify the new lease, and the user (NMDevice) may - * then configure the address. We need to watch the configured addresses (in NML3Cfg), - * and if the address appears there, we need to accept the lease. That is complicated - * but necessary, because we can only accept the lease after we configured the - * address. - * - * As a whole, ACD is transparent for the user (NMDevice). It's entirely managed - * by NMDhcpClient. Note that we do ACD through NML3Cfg, which centralizes IP handling - * for one interface, so for example if the same address happens to be configured - * as a static address (bypassing ACD), then NML3Cfg is aware of that and signals - * immediate success. */ + if (!acd_state) { + gs_free_error GError *error = NULL; - if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd - && nm_l3_config_data_get_num_addresses(priv->l3cd, priv->config.addr_family) > 0) { + /* We only decline. We don't actually emit to the caller that + * something is wrong (like NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD). + * If we would, NMDevice might decide to tear down the device, when + * we actually should continue trying to get a better lease. There + * is already "ipv4.dhcp-timeout" which will handle the failure if + * we don't get a good lease. */ + if (!_dhcp_client_decline(self, priv->l3cd_next, "acd failed", &error)) + _LOGD("decline failed: %s", error->message); + return; + } + + nm_l3_config_data_reset(&priv->l3cd_curr, priv->l3cd_next); + + if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd_curr + && nm_l3_config_data_get_num_addresses(priv->l3cd_curr, priv->config.addr_family) > 0) priv->l3cfg_notify.wait_dhcp_commit = TRUE; - } else { + else priv->l3cfg_notify.wait_dhcp_commit = FALSE; - } + l3_cfg_notify_check_connected(self); { @@ -433,7 +831,7 @@ _nm_dhcp_client_notify(NMDhcpClient *self, .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE, .lease_update = { - .l3cd = priv->l3cd, + .l3cd = priv->l3cd_curr, .accepted = !priv->l3cfg_notify.wait_dhcp_commit, }, }; @@ -512,7 +910,7 @@ _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **err klass = NM_DHCP_CLIENT_GET_CLASS(self); - g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE); + g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_curr, FALSE); return klass->accept(self, l3cd, error); } @@ -552,7 +950,7 @@ _dhcp_client_decline(NMDhcpClient *self, klass = NM_DHCP_CLIENT_GET_CLASS(self); - g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE); + g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_next, FALSE); return klass->decline(self, l3cd, error_message, error); } @@ -611,6 +1009,7 @@ static void l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN]; nm_assert(l3cfg == priv->config.l3cfg); @@ -651,7 +1050,7 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp * lease and notifying NMDevice. */ nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter, - priv->l3cd, + priv->l3cd_curr, priv->config.addr_family, &lease_address) break; @@ -678,9 +1077,11 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp _LOGD("accept lease"); - if (!_dhcp_client_accept(self, priv->l3cd, &error)) { + if (!_dhcp_client_accept(self, priv->l3cd_curr, &error)) { gs_free char *reason = g_strdup_printf("error accepting lease: %s", error->message); + _LOGD("accept failed: %s", error->message); + _emit_notify(self, &((NMDhcpClientNotifyData){ .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD, @@ -693,11 +1094,53 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp self, &((NMDhcpClientNotifyData){.notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE, .lease_update = { - .l3cd = priv->l3cd, + .l3cd = priv->l3cd_curr, .accepted = TRUE, }})); } wait_dhcp_commit_done: + + if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT + && priv->v4.acd.l3cfg_commit_handle) { + nm_assert(priv->v4.acd.addr != INADDR_ANY); + nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT); + nm_assert(!priv->v4.acd.done_source); + + if (priv->v4.acd.addr == notify_data->acd_event.info.addr + && nm_l3_acd_addr_info_find_track_info(¬ify_data->acd_event.info, + L3CD_ACD_TAG(priv), + NULL, + NULL)) { + NMOptionBool acd_state; + + switch (notify_data->acd_event.info.state) { + default: + nm_assert_not_reached(); + /* fall-through */ + case NM_L3_ACD_ADDR_STATE_INIT: + case NM_L3_ACD_ADDR_STATE_PROBING: + acd_state = NM_OPTION_BOOL_DEFAULT; + break; + case NM_L3_ACD_ADDR_STATE_USED: + case NM_L3_ACD_ADDR_STATE_CONFLICT: + case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED: + acd_state = NM_OPTION_BOOL_FALSE; + break; + case NM_L3_ACD_ADDR_STATE_READY: + case NM_L3_ACD_ADDR_STATE_DEFENDING: + acd_state = NM_OPTION_BOOL_TRUE; + break; + } + if (acd_state != NM_OPTION_BOOL_DEFAULT) { + _LOGD("acd: acd %s for %s", + acd_state ? "ready" : "conflict", + _nm_utils_inet4_ntop(priv->v4.acd.addr, sbuf_addr)); + nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle); + priv->v4.acd.state = acd_state; + priv->v4.acd.done_source = nm_g_idle_add_source(_acd_complete_on_idle_cb, self); + } + } + } } gboolean @@ -826,6 +1269,8 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release) "dhcp stopping"); } + _acd_state_reset(self, TRUE, TRUE); + priv->l3cfg_notify.wait_dhcp_commit = FALSE; priv->l3cfg_notify.wait_ll_address = FALSE; l3_cfg_notify_check_connected(self); @@ -839,6 +1284,9 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release) _LOGI("canceled DHCP transaction"); nm_assert(priv->pid == -1); + nm_clear_l3cd(&priv->l3cd_next); + nm_clear_l3cd(&priv->l3cd_curr); + _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED, NULL); } @@ -973,6 +1421,7 @@ nm_dhcp_client_handle_event(gpointer unused, NMPlatformIP6Address prefix = { 0, }; + int IS_IPv4; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); g_return_val_if_fail(iface != NULL, FALSE); @@ -983,6 +1432,8 @@ nm_dhcp_client_handle_event(gpointer unused, priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + g_return_val_if_fail(!priv->is_stopped, FALSE); + if (!nm_streq0(priv->config.iface, iface)) return FALSE; if (priv->pid != pid) @@ -1067,10 +1518,12 @@ nm_dhcp_client_handle_event(gpointer unused, client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_FAIL; } - if (priv->v4.bound.invocation) + IS_IPv4 = NM_IS_IPv4(priv->config.addr_family); + + if (IS_IPv4 && priv->v4.bound.invocation) g_dbus_method_invocation_return_value(g_steal_pointer(&priv->v4.bound.invocation), NULL); - if (NM_IS_IPv4(priv->config.addr_family) + if (IS_IPv4 && NM_IN_SET(client_event_type, NM_DHCP_CLIENT_EVENT_TYPE_BOUND, NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED)) @@ -1229,6 +1682,13 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps { .invocation = NULL, }, + .acd = + { + .addr = INADDR_ANY, + .state = NM_OPTION_BOOL_DEFAULT, + .l3cfg_commit_handle = NULL, + .done_source = NULL, + }, }; } else { priv->v6 = (typeof(priv->v6)){ @@ -1266,12 +1726,15 @@ dispose(GObject *object) watch_cleanup(self); nm_clear_g_source_inst(&priv->no_lease_timeout_source); + if (!NM_IS_IPv4(priv->config.addr_family)) nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source); + nm_clear_pointer(&priv->effective_client_id, g_bytes_unref); nm_assert(!priv->watch_source); - nm_assert(!priv->l3cd); + nm_assert(!priv->l3cd_next); + nm_assert(!priv->l3cd_curr); nm_assert(priv->l3cfg_notify.id == 0); G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object); diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h index e4b9992922..72444c3fc0 100644 --- a/src/core/dhcp/nm-dhcp-client.h +++ b/src/core/dhcp/nm-dhcp-client.h @@ -150,6 +150,10 @@ typedef struct { /* The address from the previous lease */ const char *last_address; + /* Whether to do ACD for the DHCPv4 address. With timeout zero, ACD + * is disabled. */ + guint acd_timeout_msec; + /* Set BOOTP broadcast flag in request packets, so that servers * will always broadcast replies. */ bool request_broadcast : 1; @@ -261,6 +265,8 @@ void _nm_dhcp_client_notify(NMDhcpClient *self, NMDhcpClientEventType client_event_type, const NML3ConfigData *l3cd); +gboolean _nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr); + gboolean nm_dhcp_client_handle_event(gpointer unused, const char *iface, int pid, diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c index aab658a2aa..5a17f4e8ab 100644 --- a/src/core/dhcp/nm-dhcp-helper.c +++ b/src/core/dhcp/nm-dhcp-helper.c @@ -103,14 +103,17 @@ next:; int main(int argc, char *argv[]) { - gs_unref_object GDBusConnection *connection = NULL; - gs_free_error GError *error = NULL; - gs_unref_variant GVariant *parameters = NULL; - gs_unref_variant GVariant *result = NULL; - gboolean success = FALSE; + gs_unref_object GDBusConnection *connection = NULL; + gs_free_error GError *error = NULL; + gs_free_error GError *error_flush = NULL; + gs_unref_variant GVariant *parameters = NULL; + gs_unref_variant GVariant *result = NULL; + gs_free char *s_err = NULL; + gboolean success; guint try_count; gint64 time_start; gint64 time_end; + gint64 remaining_time; /* Connecting to the unix socket can fail with EAGAIN if there are too * many pending connections and the server can't accept them in time @@ -121,6 +124,8 @@ main(int argc, char *argv[]) time_end = time_start + (5000 * 1000L); try_count = 0; + _LOGi("nm-dhcp-helper: event called"); + do_connect: try_count++; connection = @@ -131,16 +136,16 @@ do_connect: &error); if (!connection) { if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { - gint64 time_remaining = time_end - g_get_monotonic_time(); - gint64 interval; + remaining_time = time_end - g_get_monotonic_time(); + if (remaining_time > 0) { + gint64 interval; - if (time_remaining > 0) { _LOGi("failure to connect: %s (retry %u, waited %lld ms)", error->message, try_count, - (long long) (time_end - time_remaining - time_start) / 1000); + (long long) (time_end - remaining_time - time_start) / 1000); interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 100000); - g_usleep(NM_MIN(interval, time_remaining)); + g_usleep(NM_MIN(interval, remaining_time)); g_clear_error(&error); goto do_connect; } @@ -148,6 +153,7 @@ do_connect: g_dbus_error_strip_remote_error(error); _LOGE("could not connect to NetworkManager D-Bus socket: %s", error->message); + success = FALSE; goto out; } @@ -169,57 +175,74 @@ do_notify: NULL, &error); - if (!result) { - gs_free char *s_err = NULL; - - s_err = g_dbus_error_get_remote_error(error); - if (NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) { - gint64 remaining_time = time_end - g_get_monotonic_time(); - gint64 interval; - - /* I am not sure that a race can actually happen, as we register the object - * on the server side during GDBusServer:new-connection signal. - * - * However, there was also a race for subscribing to an event, so let's just - * do some retry. */ - if (remaining_time > 0) { - _LOGi("failure to call notify: %s (retry %u)", error->message, try_count); - interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000); - g_usleep(NM_MIN(interval, remaining_time)); - g_clear_error(&error); - goto do_notify; - } - } - _LOGW("failure to call notify: %s (try signal via Event)", error->message); - g_clear_error(&error); - - /* for backward compatibility, try to emit the signal. There is no stable - * API between the dhcp-helper and NetworkManager. However, while upgrading - * the NetworkManager package, a newer helper might want to notify an - * older server, which still uses the "Event". */ - if (!g_dbus_connection_emit_signal(connection, - NULL, - "/", - NM_DHCP_CLIENT_DBUS_IFACE, - "Event", - parameters, - &error)) { - g_dbus_error_strip_remote_error(error); - _LOGE("could not send DHCP Event signal: %s", error->message); - goto out; - } - /* We were able to send the asynchronous Event. Consider that a success. */ - success = TRUE; - } else + if (result) { success = TRUE; + goto out; + } - if (!g_dbus_connection_flush_sync(connection, NULL, &error)) { - g_dbus_error_strip_remote_error(error); - _LOGE("could not flush D-Bus connection: %s", error->message); + s_err = g_dbus_error_get_remote_error(error); + + if (NM_IN_STRSET(s_err, "org.freedesktop.NetworkManager.Device.Failed")) { + _LOGi("notify failed with reason: %s", error->message); success = FALSE; goto out; } + if (!NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) { + /* Some unexpected error. We treat that as a failure. In particular, + * the daemon will fail the request if ACD fails. This causes nm-dhcp-helper + * to fail, which in turn causes dhclient to send a DECLINE. */ + _LOGW("failure to call notify: %s (try signal via Event)", error->message); + success = FALSE; + goto out; + } + + /* I am not sure that a race can actually happen, as we register the object + * on the server side during GDBusServer:new-connection signal. + * + * However, there was also a race for subscribing to an event, so let's just + * do some retry. */ + remaining_time = time_end - g_get_monotonic_time(); + if (remaining_time > 0) { + gint64 interval; + + _LOGi("failure to call notify: %s (retry %u)", error->message, try_count); + interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000); + g_usleep(NM_MIN(interval, remaining_time)); + g_clear_error(&error); + goto do_notify; + } + + /* for backward compatibility, try to emit the signal. There is no stable + * API between the dhcp-helper and NetworkManager. However, while upgrading + * the NetworkManager package, a newer helper might want to notify an + * older server, which still uses the "Event". */ + + _LOGW("failure to call notify: %s (try signal via Event)", error->message); + g_clear_error(&error); + + if (g_dbus_connection_emit_signal(connection, + NULL, + "/", + NM_DHCP_CLIENT_DBUS_IFACE, + "Event", + parameters, + &error)) { + /* We were able to send the asynchronous Event. Consider that a success. */ + success = TRUE; + goto out; + } + + g_dbus_error_strip_remote_error(error); + _LOGE("could not send DHCP Event signal: %s", error->message); + success = FALSE; + out: + if (!g_dbus_connection_flush_sync(connection, NULL, &error_flush)) { + _LOGE("could not flush D-Bus connection: %s", error_flush->message); + /* if we considered this a success so far, don't fail because of this. */ + } + + _LOGi("success: %s", success ? "YES" : "NO"); return success ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c index eba4891ef2..a986dffe92 100644 --- a/src/core/dhcp/nm-dhcp-nettools.c +++ b/src/core/dhcp/nm-dhcp-nettools.c @@ -964,6 +964,11 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event) return; } + 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. */ + return; + } + _LOGT("selecting offered lease from %s for %s", _nm_utils_inet4_ntop(server_id.s_addr, addr_str), _nm_utils_inet4_ntop(yiaddr.s_addr, addr_str2));