dhcp: implement ACD (address collision detection) for DHCPv4

This was working for internal plugin in the past, but broken by l3cfg
rework with 1.36. Re-add it. Not it also works with dhclient. For other
plugins, it's not really working, because we can't decline.

Now NMDhcpClient does ACD (using NML3Cfg) and abstracts that from
the caller (NMDevice).

It is complicated. Because there is state involved, meaning, we need
to remember the current state for ACD and react on and handle a
multitude of events. Getting this right, is non-trivial.

What we want is that if ACD fails, we decline the lease (and don't use
it).

https://bugzilla.redhat.com/show_bug.cgi?id=1713380
This commit is contained in:
Thomas Haller 2022-05-17 13:55:08 +02:00
parent 156d84217c
commit 240ec7f891
No known key found for this signature in database
GPG key ID: 29C2366E4DFC5728
5 changed files with 622 additions and 124 deletions

View file

@ -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),
},
};

View file

@ -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(&reglist_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(&notify_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);

View file

@ -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,

View file

@ -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;
}

View file

@ -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));