/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2005 - 2010 Red Hat, Inc. */ #include "src/core/nm-default-daemon.h" #include "nm-dhcp-client.h" #include #include #include #include #include #include #include #include "libnm-glib-aux/nm-dedup-multi.h" #include "libnm-glib-aux/nm-random-utils.h" #include "NetworkManagerUtils.h" #include "nm-utils.h" #include "nm-l3cfg.h" #include "nm-l3-config-data.h" #include "nm-dhcp-utils.h" #include "nm-dhcp-options.h" #include "libnm-platform/nm-platform.h" #include "nm-hostname-manager.h" #include "libnm-systemd-shared/nm-sd-utils-shared.h" #include "nm-dhcp-client-logging.h" /*****************************************************************************/ enum { SIGNAL_NOTIFY, LAST_SIGNAL }; 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 *ipv6_lladdr_timeout_source; GBytes *effective_client_id; pid_t pid; guint watch_id; NMDhcpState state; bool iaid_explicit : 1; bool is_stopped : 1; struct { gulong id; bool wait_dhcp_commit : 1; bool wait_ll_address : 1; } l3cfg_notify; } NMDhcpClientPrivate; G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT) #define NM_DHCP_CLIENT_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMDhcpClient, NM_IS_DHCP_CLIENT) /*****************************************************************************/ static void l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self); /*****************************************************************************/ /* we use pid=-1 for invalid PIDs. Ensure that pid_t can hold negative values. */ G_STATIC_ASSERT(!(((pid_t) -1) > 0)); /*****************************************************************************/ static void _emit_notify(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data) { g_signal_emit(G_OBJECT(self), signals[SIGNAL_NOTIFY], 0, notify_data); } /*****************************************************************************/ static void connect_l3cfg_notify(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; if (!do_connect) { nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id); return; } if (priv->l3cfg_notify.id == 0) { priv->l3cfg_notify.id = g_signal_connect(priv->config.l3cfg, NM_L3CFG_SIGNAL_NOTIFY, G_CALLBACK(l3_cfg_notify_cb), self); } } pid_t nm_dhcp_client_get_pid(NMDhcpClient *self) { g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1); return NM_DHCP_CLIENT_GET_PRIVATE(self)->pid; } static void _set_effective_client_id(NMDhcpClient *self, GBytes *client_id, gboolean take) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_assert(!client_id || g_bytes_get_size(client_id) >= 2); if (priv->effective_client_id == client_id || (priv->effective_client_id && client_id && g_bytes_equal(priv->effective_client_id, client_id))) { if (take && client_id) g_bytes_unref(client_id); return; } if (priv->effective_client_id) g_bytes_unref(priv->effective_client_id); priv->effective_client_id = client_id; if (!take && client_id) g_bytes_ref(client_id); { gs_free char *s = NULL; _LOGT("%s: set %s", priv->config.addr_family == AF_INET6 ? "duid" : "client-id", priv->effective_client_id ? (s = nm_dhcp_utils_duid_to_string(priv->effective_client_id)) : "default"); } } void nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id) { g_return_if_fail(NM_IS_DHCP_CLIENT(self)); g_return_if_fail(!client_id || g_bytes_get_size(client_id) >= 2); _set_effective_client_id(self, client_id, FALSE); } /*****************************************************************************/ NM_UTILS_LOOKUP_STR_DEFINE(nm_dhcp_state_to_string, NMDhcpState, NM_UTILS_LOOKUP_DEFAULT(NULL), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_BOUND, "bound"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_DONE, "done"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_EXPIRE, "expire"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_EXTENDED, "extended"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_FAIL, "fail"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_NOOP, "noop"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_TERMINATED, "terminated"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_TIMEOUT, "timeout"), NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_UNKNOWN, "unknown"), ); static NMDhcpState reason_to_state(NMDhcpClient *self, const char *iface, const char *reason) { if (g_ascii_strcasecmp(reason, "bound") == 0 || g_ascii_strcasecmp(reason, "bound6") == 0 || g_ascii_strcasecmp(reason, "static") == 0) return NM_DHCP_STATE_BOUND; else if (g_ascii_strcasecmp(reason, "renew") == 0 || g_ascii_strcasecmp(reason, "renew6") == 0 || g_ascii_strcasecmp(reason, "reboot") == 0 || g_ascii_strcasecmp(reason, "rebind") == 0 || g_ascii_strcasecmp(reason, "rebind6") == 0) return NM_DHCP_STATE_EXTENDED; else if (g_ascii_strcasecmp(reason, "timeout") == 0) return NM_DHCP_STATE_TIMEOUT; else if (g_ascii_strcasecmp(reason, "nak") == 0 || g_ascii_strcasecmp(reason, "expire") == 0 || g_ascii_strcasecmp(reason, "expire6") == 0) return NM_DHCP_STATE_EXPIRE; else if (g_ascii_strcasecmp(reason, "end") == 0 || g_ascii_strcasecmp(reason, "stop") == 0 || g_ascii_strcasecmp(reason, "stopped") == 0) return NM_DHCP_STATE_DONE; else if (g_ascii_strcasecmp(reason, "fail") == 0 || g_ascii_strcasecmp(reason, "abend") == 0) return NM_DHCP_STATE_FAIL; else if (g_ascii_strcasecmp(reason, "preinit") == 0) return NM_DHCP_STATE_NOOP; _LOGD("unmapped DHCP state '%s'", reason); return NM_DHCP_STATE_UNKNOWN; } /*****************************************************************************/ static void watch_cleanup(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_clear_g_source(&priv->watch_id); } void nm_dhcp_client_stop_pid(pid_t pid, const char *iface) { char *name = iface ? g_strdup_printf("dhcp-client-%s", iface) : NULL; g_return_if_fail(pid > 1); nm_utils_kill_child_sync(pid, SIGTERM, LOGD_DHCP, name ?: "dhcp-client", NULL, 1000 / 2, 1000 / 20); g_free(name); } static void stop(NMDhcpClient *self, gboolean release) { NMDhcpClientPrivate *priv; g_return_if_fail(NM_IS_DHCP_CLIENT(self)); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); if (priv->pid > 0) { /* Clean up the watch handler since we're explicitly killing the daemon */ watch_cleanup(self); nm_dhcp_client_stop_pid(priv->pid, priv->config.iface); } priv->pid = -1; } static gboolean _no_lease_timeout(gpointer user_data) { NMDhcpClient *self = user_data; NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_clear_g_source_inst(&priv->no_lease_timeout_source); _emit_notify(self, &((NMDhcpClientNotifyData){ .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT, })); return G_SOURCE_CONTINUE; } const NMDhcpClientConfig * nm_dhcp_client_get_config(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return &priv->config; } static void schedule_no_lease_timeout(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); if (priv->no_lease_timeout_source) return; if (priv->config.timeout == NM_DHCP_TIMEOUT_INFINITY) { _LOGI("activation: beginning transaction (no timeout)"); priv->no_lease_timeout_source = g_source_ref(nm_g_source_sentinel_get(0)); } else { _LOGI("activation: beginning transaction (timeout in %u seconds)", (guint) priv->config.timeout); priv->no_lease_timeout_source = nm_g_timeout_add_seconds_source(priv->config.timeout, _no_lease_timeout, self); } } void nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3ConfigData *l3cd) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); GHashTable *options; const int IS_IPv4 = NM_IS_IPv4(priv->config.addr_family); nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL; g_return_if_fail(NM_IS_DHCP_CLIENT(self)); if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) { g_return_if_fail(NM_IS_L3_CONFIG_DATA(l3cd)); g_return_if_fail(nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family)); } else g_return_if_fail(!l3cd); if (l3cd) nm_l3_config_data_seal(l3cd); if (new_state >= NM_DHCP_STATE_TIMEOUT) watch_cleanup(self); if (!IS_IPv4 && l3cd) { if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd, l3cd, &l3cd_merged)) { 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) schedule_no_lease_timeout(self); } /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API. * The internals like NMDhcpState 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) { gs_free const char **keys = NULL; guint nkeys; guint i; keys = nm_strdict_get_keys(options, TRUE, &nkeys); for (i = 0; i < nkeys; i++) { _LOGD("option %-20s => '%s'", keys[i], (char *) g_hash_table_lookup(options, keys[i])); } if (priv->config.addr_family == AF_INET6) { gs_free char *event_id = NULL; event_id = nm_dhcp_utils_get_dhcp6_event_id(options); if (event_id) _LOGT("event-id: \"%s\"", event_id); } } } 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, "", "")); } /* 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 nm_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 (nm_dhcp_client_can_accept(self) && new_state == NM_DHCP_STATE_BOUND && priv->l3cd && nm_l3_config_data_get_num_addresses(priv->l3cd, priv->config.addr_family) > 0) { priv->l3cfg_notify.wait_dhcp_commit = TRUE; } else { priv->l3cfg_notify.wait_dhcp_commit = FALSE; } connect_l3cfg_notify(self); { const NMDhcpClientNotifyData notify_data = { .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE, .lease_update = { .l3cd = priv->l3cd, .accepted = !priv->l3cfg_notify.wait_dhcp_commit, }, }; _emit_notify(self, ¬ify_data); } } static void daemon_watch_cb(GPid pid, int status, gpointer user_data) { NMDhcpClient *self = NM_DHCP_CLIENT(user_data); NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); gs_free char *desc = NULL; g_return_if_fail(priv->watch_id); priv->watch_id = 0; _LOGI("client pid %d %s", pid, (desc = nm_utils_get_process_exit_status_desc(status))); priv->pid = -1; nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL); } void nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_if_fail(priv->pid == -1); priv->pid = pid; g_return_if_fail(priv->watch_id == 0); priv->watch_id = g_child_watch_add(pid, daemon_watch_cb, self); } void nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_if_fail(priv->pid == pid); priv->pid = -1; watch_cleanup(self); } gboolean nm_dhcp_client_start_ip4(NMDhcpClient *self, GError **error) { NMDhcpClientPrivate *priv; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_val_if_fail(priv->pid == -1, FALSE); g_return_val_if_fail(priv->config.addr_family == AF_INET, FALSE); g_return_val_if_fail(priv->config.uuid, FALSE); schedule_no_lease_timeout(self); return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, error); } gboolean nm_dhcp_client_accept(NMDhcpClient *self, GError **error) { NMDhcpClientPrivate *priv; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_val_if_fail(priv->l3cd, FALSE); if (NM_DHCP_CLIENT_GET_CLASS(self)->accept) { return NM_DHCP_CLIENT_GET_CLASS(self)->accept(self, error); } return TRUE; } gboolean nm_dhcp_client_can_accept(NMDhcpClient *self) { gboolean can_accept; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); can_accept = !!(NM_DHCP_CLIENT_GET_CLASS(self)->accept); nm_assert(can_accept == (!!(NM_DHCP_CLIENT_GET_CLASS(self)->decline))); return can_accept; } gboolean nm_dhcp_client_decline(NMDhcpClient *self, const char *error_message, GError **error) { NMDhcpClientPrivate *priv; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_val_if_fail(priv->l3cd, FALSE); if (NM_DHCP_CLIENT_GET_CLASS(self)->decline) { return NM_DHCP_CLIENT_GET_CLASS(self)->decline(self, error_message, error); } return TRUE; } static GBytes * get_duid(NMDhcpClient *self) { return NULL; } static gboolean ipv6_lladdr_timeout(gpointer user_data) { NMDhcpClient *self = user_data; NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source); _emit_notify( self, &((NMDhcpClientNotifyData){ .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD, .it_looks_bad.reason = "timeout reached while waiting for an IPv6 link-local address", })); return G_SOURCE_CONTINUE; } static const NMPlatformIP6Address * ipv6_lladdr_find(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); NML3Cfg *l3cfg; NMPLookup lookup; NMDedupMultiIter iter; const NMPObject *obj; l3cfg = priv->config.l3cfg; nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, nm_l3cfg_get_ifindex(l3cfg)); nm_platform_iter_obj_for_each (&iter, nm_l3cfg_get_platform(l3cfg), &lookup, &obj) { const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj); if (!IN6_IS_ADDR_LINKLOCAL(&pladdr->address)) continue; if (NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_TENTATIVE) && !NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_OPTIMISTIC)) continue; return pladdr; } return NULL; } static void l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_assert(l3cfg == priv->config.l3cfg); switch (notify_data->notify_type) { case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE: { const NMPlatformIP6Address *addr; gs_free_error GError *error = NULL; if (!priv->l3cfg_notify.wait_ll_address) return; addr = ipv6_lladdr_find(self); if (addr) { _LOGD("got IPv6LL address, starting transaction"); priv->l3cfg_notify.wait_ll_address = FALSE; connect_l3cfg_notify(self); nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source); schedule_no_lease_timeout(self); if (!NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, &error)) { _emit_notify(self, &((NMDhcpClientNotifyData){ .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD, .it_looks_bad.reason = error->message, })); } } break; } case NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT: { const NML3ConfigData *committed_l3cd; NMDedupMultiIter ipconf_iter; const NMPlatformIPAddress *lease_address; gs_free_error GError *error = NULL; /* A new configuration was committed to the interface. If we previously * got a lease, check whether we are waiting for the address to be * configured. If the address was added, we can proceed accepting the * lease and notifying NMDevice. */ if (!priv->l3cfg_notify.wait_dhcp_commit) return; nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter, priv->l3cd, priv->config.addr_family, &lease_address) break; nm_assert(lease_address); committed_l3cd = nm_l3cfg_get_combined_l3cd(l3cfg, TRUE); if (priv->config.addr_family == AF_INET) { const NMPlatformIP4Address *address4 = (const NMPlatformIP4Address *) lease_address; if (!nm_l3_config_data_lookup_address_4(committed_l3cd, address4->address, address4->plen, address4->peer_address)) return; } else { const NMPlatformIP6Address *address6 = (const NMPlatformIP6Address *) lease_address; if (!nm_l3_config_data_lookup_address_6(committed_l3cd, &address6->address)) return; } priv->l3cfg_notify.wait_dhcp_commit = FALSE; connect_l3cfg_notify(self); _LOGD("accept address"); if (!nm_dhcp_client_accept(self, &error)) { gs_free char *reason = g_strdup_printf("error accepting lease: %s", error->message); _emit_notify(self, &((NMDhcpClientNotifyData){ .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD, .it_looks_bad.reason = reason, })); return; } _emit_notify( self, &((NMDhcpClientNotifyData){.notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE, .lease_update = { .l3cd = priv->l3cd, .accepted = TRUE, }})); break; }; default: /* ignore */; } } gboolean nm_dhcp_client_start_ip6(NMDhcpClient *self, GError **error) { NMDhcpClientPrivate *priv; gs_unref_bytes GBytes *own_client_id = NULL; const NMPlatformIP6Address *addr; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); g_return_val_if_fail(priv->pid == -1, FALSE); g_return_val_if_fail(priv->config.addr_family == AF_INET6, FALSE); g_return_val_if_fail(priv->config.uuid, FALSE); g_return_val_if_fail(!priv->effective_client_id, FALSE); if (!priv->config.v6.enforce_duid) own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self); _set_effective_client_id(self, own_client_id ?: priv->config.client_id, FALSE); addr = ipv6_lladdr_find(self); if (!addr) { _LOGD("waiting for IPv6LL address"); priv->l3cfg_notify.wait_ll_address = TRUE; connect_l3cfg_notify(self); priv->ipv6_lladdr_timeout_source = nm_g_timeout_add_seconds_source(10, ipv6_lladdr_timeout, self); return TRUE; } schedule_no_lease_timeout(self); return NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, error); } void nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name) { guint64 start_time; pid_t pid, ppid; const char *exe; char proc_path[NM_STRLEN("/proc/%lu/cmdline") + 100]; gs_free char *pid_contents = NULL, *proc_contents = NULL; /* Check for an existing instance and stop it */ if (!g_file_get_contents(pid_file, &pid_contents, NULL, NULL)) return; pid = _nm_utils_ascii_str_to_int64(pid_contents, 10, 1, G_MAXINT64, 0); if (pid <= 0) goto out; start_time = nm_utils_get_start_time_for_pid(pid, NULL, &ppid); if (start_time == 0) goto out; nm_sprintf_buf(proc_path, "/proc/%lu/cmdline", (unsigned long) pid); if (!g_file_get_contents(proc_path, &proc_contents, NULL, NULL)) goto out; exe = strrchr(proc_contents, '/'); if (exe) exe++; else exe = proc_contents; if (!nm_streq0(exe, binary_name)) goto out; if (ppid == getpid()) { /* the process is our own child. */ nm_utils_kill_child_sync(pid, SIGTERM, LOGD_DHCP, "dhcp-client", NULL, 1000 / 2, 1000 / 20); } else { nm_utils_kill_process_sync(pid, start_time, SIGTERM, LOGD_DHCP, "dhcp-client", 1000 / 2, 1000 / 20, 2000); } out: if (remove(pid_file) == -1) { int errsv = errno; nm_log_dbg(LOGD_DHCP, "dhcp: could not remove pid file \"%s\": %s (%d)", pid_file, nm_strerror_native(errsv), errsv); } } void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release) { NMDhcpClientPrivate *priv; pid_t old_pid = 0; g_return_if_fail(NM_IS_DHCP_CLIENT(self)); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); if (priv->is_stopped) return; priv->is_stopped = TRUE; priv->l3cfg_notify.wait_dhcp_commit = FALSE; priv->l3cfg_notify.wait_ll_address = FALSE; connect_l3cfg_notify(self); /* Kill the DHCP client */ old_pid = priv->pid; NM_DHCP_CLIENT_GET_CLASS(self)->stop(self, release); if (old_pid > 0) _LOGI("canceled DHCP transaction, DHCP client pid %d", old_pid); else _LOGI("canceled DHCP transaction"); nm_assert(priv->pid == -1); nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL); } /*****************************************************************************/ static char * bytearray_variant_to_string(NMDhcpClient *self, GVariant *value, const char *key) { const guint8 *array; gsize length; GString *str; int i; unsigned char c; char *converted = NULL; g_return_val_if_fail(value != NULL, NULL); array = g_variant_get_fixed_array(value, &length, 1); /* Since the DHCP options come through environment variables, they should * already be UTF-8 safe, but just make sure. */ str = g_string_sized_new(length); for (i = 0; i < length; i++) { c = array[i]; /* Convert NULLs to spaces and non-ASCII characters to ? */ if (c == '\0') c = ' '; else if (c > 127) c = '?'; str = g_string_append_c(str, c); } str = g_string_append_c(str, '\0'); converted = str->str; if (!g_utf8_validate(converted, -1, NULL)) _LOGW("option '%s' couldn't be converted to UTF-8", key); g_string_free(str, FALSE); return converted; } static int label_is_unknown_xyz(const char *label) { if (!NM_STR_HAS_PREFIX(label, "unknown_")) return -EINVAL; label += NM_STRLEN("unknown_"); if (label[0] != '2' || !g_ascii_isdigit(label[1]) || !g_ascii_isdigit(label[2]) || label[3] != '\0') return -EINVAL; return _nm_utils_ascii_str_to_int64(label, 10, 224, 254, -EINVAL); } #define OLD_TAG "old_" #define NEW_TAG "new_" static void maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant *value) { char *str_value = NULL; g_return_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING)); if (g_str_has_prefix(key, OLD_TAG)) return; /* Filter out stuff that's not actually new DHCP options */ if (NM_IN_STRSET(key, "interface", "pid", "reason", "dhcp_message_type")) return; if (NM_STR_HAS_PREFIX(key, NEW_TAG)) key += NM_STRLEN(NEW_TAG); if (NM_STR_HAS_PREFIX(key, "private_") || !key[0]) return; str_value = bytearray_variant_to_string(self, value, key); if (str_value) { int priv_opt_num; g_hash_table_insert(hash, g_strdup(key), str_value); /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz" * labels for that. We need to identify those to alias them to our "private_xyz" * format unused in the internal dchp plugins. */ if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) { gs_free guint8 *check_val = NULL; char *hex_str = NULL; gsize len; /* dhclient passes values from dhcp private options in its own "string" format: * if the raw values are printable as ascii strings, it will pass the string * representation; if the values are not printable as an ascii string, it will * pass a string displaying the hex values (hex string). Try to enforce passing * always an hex string, converting string representation if needed. */ check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len); hex_str = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value, check_val ? len : strlen(str_value), ':', FALSE, NULL); g_hash_table_insert(hash, g_strdup_printf("private_%d", priv_opt_num), hex_str); } } } void nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient *self, const NMPlatformIP6Address *prefix) { const NMDhcpClientNotifyData notify_data = { .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED, .prefix_delegated = { .prefix = prefix, }, }; _emit_notify(self, ¬ify_data); } gboolean nm_dhcp_client_handle_event(gpointer unused, const char *iface, int pid, GVariant *options, const char *reason, NMDhcpClient *self) { NMDhcpClientPrivate *priv; guint32 new_state; nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL; NMPlatformIP6Address prefix = { 0, }; g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); g_return_val_if_fail(iface != NULL, FALSE); g_return_val_if_fail(pid > 0, FALSE); g_return_val_if_fail(g_variant_is_of_type(options, G_VARIANT_TYPE_VARDICT), FALSE); g_return_val_if_fail(reason != NULL, FALSE); priv = NM_DHCP_CLIENT_GET_PRIVATE(self); if (!nm_streq0(priv->config.iface, iface)) return FALSE; if (priv->pid != pid) return FALSE; new_state = reason_to_state(self, priv->config.iface, reason); if (new_state == NM_DHCP_STATE_NOOP) return TRUE; _LOGD("DHCP state '%s' -> '%s' (reason: '%s')", nm_dhcp_state_to_string(priv->state), nm_dhcp_state_to_string(new_state), reason); priv->state = new_state; if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) { gs_unref_hashtable GHashTable *str_options = NULL; GVariantIter iter; const char *name; GVariant *value; /* Copy options */ str_options = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free); g_variant_iter_init(&iter, options); while (g_variant_iter_next(&iter, "{&sv}", &name, &value)) { maybe_add_option(self, str_options, name, value); g_variant_unref(value); } /* Create the IP config */ if (g_hash_table_size(str_options) > 0) { if (priv->config.addr_family == AF_INET) { l3cd = nm_dhcp_utils_ip4_config_from_options( nm_l3cfg_get_multi_idx(priv->config.l3cfg), nm_l3cfg_get_ifindex(priv->config.l3cfg), priv->config.iface, str_options); } else { prefix = nm_dhcp_utils_ip6_prefix_from_options(str_options); l3cd = nm_dhcp_utils_ip6_config_from_options( nm_l3cfg_get_multi_idx(priv->config.l3cfg), nm_l3cfg_get_ifindex(priv->config.l3cfg), priv->config.iface, str_options, priv->config.v6.info_only); } } else g_warn_if_reached(); if (l3cd) { nm_l3_config_data_set_dhcp_lease_from_options(l3cd, priv->config.addr_family, g_steal_pointer(&str_options)); } } if (!IN6_IS_ADDR_UNSPECIFIED(&prefix.address)) { /* If we got an IPv6 prefix to delegate, we don't change the state * of the DHCP client instance. Instead, we just signal the prefix * to the device. */ nm_dhcp_client_emit_ipv6_prefix_delegated(self, &prefix); return TRUE; } /* Fail if no valid IP config was received */ if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED) && !l3cd) { _LOGW("client bound but IP config not received"); new_state = NM_DHCP_STATE_FAIL; } nm_dhcp_client_set_state(self, new_state, l3cd); return TRUE; } gboolean nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); in_addr_t addr4 = *(in_addr_t *) addr; guint i; /* IPv6 not implemented yet */ nm_assert(priv->config.addr_family == AF_INET); if (!priv->config.reject_servers || !priv->config.reject_servers[0]) return FALSE; for (i = 0; priv->config.reject_servers[i]; i++) { in_addr_t r_addr; in_addr_t mask; int r_prefix; if (!nm_utils_parse_inaddr_prefix_bin(AF_INET, priv->config.reject_servers[i], NULL, &r_addr, &r_prefix)) nm_assert_not_reached(); mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix); if ((addr4 & mask) == (r_addr & mask)) return TRUE; } return FALSE; } static void config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src) { *config = *src; g_object_ref(config->l3cfg); if (config->hwaddr) g_bytes_ref(config->hwaddr); if (config->bcast_hwaddr) g_bytes_ref(config->bcast_hwaddr); if (config->vendor_class_identifier) g_bytes_ref(config->vendor_class_identifier); if (config->client_id) g_bytes_ref(config->client_id); config->iface = g_strdup(config->iface); config->uuid = g_strdup(config->uuid); config->anycast_address = g_strdup(config->anycast_address); config->hostname = g_strdup(config->hostname); config->mud_url = g_strdup(config->mud_url); config->reject_servers = (const char *const *) nm_strv_dup(config->reject_servers, -1, TRUE); if (config->addr_family == AF_INET) { config->v4.last_address = g_strdup(config->v4.last_address); } else if (config->addr_family == AF_INET6) { config->hwaddr = NULL; config->bcast_hwaddr = NULL; config->use_fqdn = TRUE; } else { nm_assert_not_reached(); } if (!config->hostname && config->send_hostname) { const char *hostname; gs_free char *hostname_tmp = NULL; hostname = nm_hostname_manager_get_static_hostname(nm_hostname_manager_get()); if (nm_utils_is_specific_hostname(hostname)) { if (config->addr_family == AF_INET) { char *dot; hostname_tmp = g_strdup(hostname); dot = strchr(hostname_tmp, '.'); if (dot) *dot = '\0'; } config->hostname = hostname_tmp ? g_steal_pointer(&hostname_tmp) : g_strdup(hostname); } } if (config->hostname) { if (!config->send_hostname) { nm_clear_g_free((gpointer *) &config->hostname); } else if ((config->use_fqdn && !nm_sd_dns_name_is_valid(config->hostname)) || (!config->use_fqdn && !nm_sd_hostname_is_valid(config->hostname, FALSE))) { nm_log_warn(LOGD_DHCP, "dhcp%c: %s '%s' is invalid, will be ignored", nm_utils_addr_family_to_char(config->addr_family), config->use_fqdn ? "FQDN" : "hostname", config->hostname); nm_clear_g_free((gpointer *) &config->hostname); } } } static void config_clear(NMDhcpClientConfig *config) { g_object_unref(config->l3cfg); nm_clear_pointer(&config->hwaddr, g_bytes_unref); nm_clear_pointer(&config->bcast_hwaddr, g_bytes_unref); nm_clear_pointer(&config->vendor_class_identifier, g_bytes_unref); nm_clear_pointer(&config->client_id, g_bytes_unref); nm_clear_g_free((gpointer *) &config->iface); nm_clear_g_free((gpointer *) &config->uuid); nm_clear_g_free((gpointer *) &config->anycast_address); nm_clear_g_free((gpointer *) &config->hostname); nm_clear_g_free((gpointer *) &config->mud_url); nm_clear_pointer((gpointer *) &config->reject_servers, g_strfreev); if (config->addr_family == AF_INET) { nm_clear_g_free((gpointer *) &config->v4.last_address); } } int nm_dhcp_client_get_addr_family(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return priv->config.addr_family; } const char * nm_dhcp_client_get_iface(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return priv->config.iface; } NMDedupMultiIndex * nm_dhcp_client_get_multi_idx(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return nm_l3cfg_get_multi_idx(priv->config.l3cfg); } int nm_dhcp_client_get_ifindex(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return nm_l3cfg_get_ifindex(priv->config.l3cfg); } GBytes * nm_dhcp_client_get_effective_client_id(NMDhcpClient *self) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); return priv->effective_client_id; } /*****************************************************************************/ static void set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(object); switch (prop_id) { case PROP_CONFIG: /* construct-only */ config_init(&priv->config, g_value_get_pointer(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } /*****************************************************************************/ static void nm_dhcp_client_init(NMDhcpClient *self) { NMDhcpClientPrivate *priv; priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DHCP_CLIENT, NMDhcpClientPrivate); self->_priv = priv; priv->pid = -1; } static void dispose(GObject *object) { NMDhcpClient *self = NM_DHCP_CLIENT(object); NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); nm_dhcp_client_stop(self, FALSE); watch_cleanup(self); nm_clear_g_source_inst(&priv->no_lease_timeout_source); nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source); G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object); } static void finalize(GObject *object) { NMDhcpClient *self = NM_DHCP_CLIENT(object); NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); config_clear(&priv->config); G_OBJECT_CLASS(nm_dhcp_client_parent_class)->finalize(object); } static void nm_dhcp_client_class_init(NMDhcpClientClass *client_class) { GObjectClass *object_class = G_OBJECT_CLASS(client_class); g_type_class_add_private(client_class, sizeof(NMDhcpClientPrivate)); object_class->dispose = dispose; object_class->finalize = finalize; object_class->set_property = set_property; client_class->stop = stop; client_class->get_duid = get_duid; obj_properties[PROP_CONFIG] = g_param_spec_pointer(NM_DHCP_CLIENT_CONFIG, "", "", G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); signals[SIGNAL_NOTIFY] = g_signal_new(NM_DHCP_CLIENT_NOTIFY, G_OBJECT_CLASS_TYPE(object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER /* const NMDhcpClientNotifyData *notify_data */); }