/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2005 - 2013 Red Hat, Inc. * Copyright (C) 2006 - 2008 Novell, Inc. */ #include "nm-default.h" #include "nm-dhcp-manager.h" #include #include #include #include #include #include #include #include "nm-glib-aux/nm-dedup-multi.h" #include "systemd/nm-sd-utils-shared.h" #include "nm-config.h" #include "NetworkManagerUtils.h" /*****************************************************************************/ typedef struct { const NMDhcpClientFactory *client_factory; char * default_hostname; CList dhcp_client_lst_head; } NMDhcpManagerPrivate; struct _NMDhcpManager { GObject parent; NMDhcpManagerPrivate _priv; }; struct _NMDhcpManagerClass { GObjectClass parent; }; G_DEFINE_TYPE(NMDhcpManager, nm_dhcp_manager, G_TYPE_OBJECT) #define NM_DHCP_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDhcpManager, NM_IS_DHCP_MANAGER) /*****************************************************************************/ static void client_state_changed(NMDhcpClient * client, NMDhcpState state, GObject * ip_config, GVariant * options, NMDhcpManager *self); /*****************************************************************************/ /* default to installed helper, but can be modified for testing */ const char *nm_dhcp_helper_path = LIBEXECDIR "/nm-dhcp-helper"; /*****************************************************************************/ static const NMDhcpClientFactory * _client_factory_find_by_name(const char *name) { int i; g_return_val_if_fail(name, NULL); for (i = 0; i < G_N_ELEMENTS(_nm_dhcp_manager_factories); i++) { const NMDhcpClientFactory *f = _nm_dhcp_manager_factories[i]; if (f && nm_streq(f->name, name)) return f; } return NULL; } static const NMDhcpClientFactory * _client_factory_available(const NMDhcpClientFactory *client_factory) { if (client_factory && (!client_factory->get_path || client_factory->get_path())) return client_factory; return NULL; } static GType _client_factory_get_gtype(const NMDhcpClientFactory *client_factory, int addr_family) { GType gtype; nm_auto_unref_gtypeclass NMDhcpClientClass *klass = NULL; nm_assert(client_factory); nm_assert_addr_family(addr_family); /* currently, the chosen DHCP plugin for IPv4 and IPv6 is configured in NetworkManager.conf * and cannot be reloaded. It would be nice to configure the plugin per address family * or to be able to reload it. * * Note that certain options in NetworkManager.conf depend on the chosen DHCP plugin. * See "dhcp-plugin:" in "Device List Format" (`man NetworkManager.conf`). * Supporting reloading the plugin would also require to re-evalate the decisions from * the "Device List Format". Likewise, having per-address family plugins would make the * "main.dhcp" setting and "dhcp-plugin:" match non-sensical because these configurations * currently are address family independent. * * So actually, we don't want that complexity. We want to phase out all plugins in favor * of the internal plugin. * However, certain existing plugins are well known to not support an address family. * In those cases, we should just silently fallback to the internal plugin. * * This could be a problem with forward compatibility if we ever intended to add IPv6 support * to those plugins. But we don't intend to do so. The internal plugin is the way forward and * not extending other plugins. */ if (client_factory->get_type_per_addr_family) gtype = client_factory->get_type_per_addr_family(addr_family); else gtype = client_factory->get_type(); if (client_factory == &_nm_dhcp_client_factory_internal) { /* we are already using the internal plugin. Nothing to do. */ goto out; } klass = g_type_class_ref(gtype); nm_assert(NM_IS_DHCP_CLIENT_CLASS(klass)); if (addr_family == AF_INET6) { if (!klass->ip6_start) gtype = _client_factory_get_gtype(&_nm_dhcp_client_factory_internal, addr_family); } else { if (!klass->ip4_start) gtype = _client_factory_get_gtype(&_nm_dhcp_client_factory_internal, addr_family); } out: nm_assert(g_type_is_a(gtype, NM_TYPE_DHCP_CLIENT)); nm_assert(({ nm_auto_unref_gtypeclass NMDhcpClientClass *k = g_type_class_ref(gtype); (addr_family == AF_INET6 && k->ip6_start) || (addr_family == AF_INET && k->ip4_start); })); return gtype; } /*****************************************************************************/ static NMDhcpClient * get_client_for_ifindex(NMDhcpManager *manager, int addr_family, int ifindex) { NMDhcpManagerPrivate *priv; NMDhcpClient * client; g_return_val_if_fail(NM_IS_DHCP_MANAGER(manager), NULL); g_return_val_if_fail(ifindex > 0, NULL); priv = NM_DHCP_MANAGER_GET_PRIVATE(manager); c_list_for_each_entry (client, &priv->dhcp_client_lst_head, dhcp_client_lst) { if (nm_dhcp_client_get_ifindex(client) == ifindex && nm_dhcp_client_get_addr_family(client) == addr_family) return client; } return NULL; } static void remove_client(NMDhcpManager *self, NMDhcpClient *client) { g_signal_handlers_disconnect_by_func(client, client_state_changed, self); c_list_unlink(&client->dhcp_client_lst); /* Stopping the client is left up to the controlling device * explicitly since we may want to quit NetworkManager but not terminate * the DHCP client. */ } static void remove_client_unref(NMDhcpManager *self, NMDhcpClient *client) { remove_client(self, client); g_object_unref(client); } static void client_state_changed(NMDhcpClient * client, NMDhcpState state, GObject * ip_config, GVariant * options, NMDhcpManager *self) { if (state >= NM_DHCP_STATE_TIMEOUT) remove_client_unref(self, client); } static NMDhcpClient * client_start(NMDhcpManager * self, int addr_family, NMDedupMultiIndex * multi_idx, const char * iface, int ifindex, GBytes * hwaddr, GBytes * bcast_hwaddr, const char * uuid, guint32 route_table, guint32 route_metric, const struct in6_addr * ipv6_ll_addr, GBytes * dhcp_client_id, gboolean enforce_duid, guint32 iaid, gboolean iaid_explicit, guint32 timeout, const char * dhcp_anycast_addr, const char * hostname, gboolean hostname_use_fqdn, NMDhcpHostnameFlags hostname_flags, const char * mud_url, gboolean info_only, NMSettingIP6ConfigPrivacy privacy, const char * last_ip4_address, guint needed_prefixes, GBytes * vendor_class_identifier, const char *const * reject_servers, GError ** error) { NMDhcpManagerPrivate *priv; NMDhcpClient * client; gboolean success = FALSE; gsize hwaddr_len; GType gtype; g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL); g_return_val_if_fail(iface, NULL); g_return_val_if_fail(ifindex > 0, NULL); g_return_val_if_fail(uuid != NULL, NULL); g_return_val_if_fail(!dhcp_client_id || g_bytes_get_size(dhcp_client_id) >= 2, NULL); g_return_val_if_fail(!vendor_class_identifier || g_bytes_get_size(vendor_class_identifier) <= 255, NULL); g_return_val_if_fail(!error || !*error, NULL); if (addr_family == AF_INET) { if (!hwaddr || !bcast_hwaddr) { nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "missing %s address", hwaddr ? "broadcast" : "MAC"); return NULL; } hwaddr_len = g_bytes_get_size(hwaddr); if (hwaddr_len == 0 || hwaddr_len > NM_UTILS_HWADDR_LEN_MAX) { nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address"); g_return_val_if_reached(NULL); } nm_assert(g_bytes_get_size(hwaddr) == g_bytes_get_size(bcast_hwaddr)); } else { hwaddr = NULL; bcast_hwaddr = NULL; } if (hostname) { if ((hostname_use_fqdn && !nm_sd_dns_name_is_valid(hostname)) || (!hostname_use_fqdn && !nm_sd_hostname_is_valid(hostname, FALSE))) { nm_log_warn(LOGD_DHCP, "dhcp%c: %s '%s' is invalid, will be ignored", nm_utils_addr_family_to_char(addr_family), hostname_use_fqdn ? "FQDN" : "hostname", hostname); hostname = NULL; } } priv = NM_DHCP_MANAGER_GET_PRIVATE(self); /* Kill any old client instance */ client = get_client_for_ifindex(self, addr_family, ifindex); if (client) { /* FIXME: we cannot just call synchronously "stop()" and forget about the client. * We need to wait for the client to be fully stopped because most/all clients * cannot quit right away. * * FIXME(shutdown): also fix this during shutdown, to wait for all DHCP clients * to be fully stopped. */ remove_client(self, client); nm_dhcp_client_stop(client, FALSE); g_object_unref(client); } gtype = _client_factory_get_gtype(priv->client_factory, addr_family); nm_log_trace(LOGD_DHCP, "dhcp%c: creating IPv%c DHCP client of type %s", nm_utils_addr_family_to_char(addr_family), nm_utils_addr_family_to_char(addr_family), g_type_name(gtype)); client = g_object_new(gtype, NM_DHCP_CLIENT_MULTI_IDX, multi_idx, NM_DHCP_CLIENT_ADDR_FAMILY, addr_family, NM_DHCP_CLIENT_INTERFACE, iface, NM_DHCP_CLIENT_IFINDEX, ifindex, NM_DHCP_CLIENT_HWADDR, hwaddr, NM_DHCP_CLIENT_BROADCAST_HWADDR, bcast_hwaddr, NM_DHCP_CLIENT_UUID, uuid, NM_DHCP_CLIENT_IAID, (guint) iaid, NM_DHCP_CLIENT_IAID_EXPLICIT, iaid_explicit, NM_DHCP_CLIENT_HOSTNAME, hostname, NM_DHCP_CLIENT_MUD_URL, mud_url, NM_DHCP_CLIENT_ROUTE_TABLE, (guint) route_table, NM_DHCP_CLIENT_ROUTE_METRIC, (guint) route_metric, NM_DHCP_CLIENT_TIMEOUT, (guint) timeout, NM_DHCP_CLIENT_HOSTNAME_FLAGS, (guint) hostname_flags, NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER, vendor_class_identifier, NM_DHCP_CLIENT_REJECT_SERVERS, reject_servers, NM_DHCP_CLIENT_FLAGS, (guint)(0 | (hostname_use_fqdn ? NM_DHCP_CLIENT_FLAGS_USE_FQDN : 0) | (info_only ? NM_DHCP_CLIENT_FLAGS_INFO_ONLY : 0)), NULL); nm_assert(client && c_list_is_empty(&client->dhcp_client_lst)); c_list_link_tail(&priv->dhcp_client_lst_head, &client->dhcp_client_lst); g_signal_connect(client, NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED, G_CALLBACK(client_state_changed), self); /* unfortunately, our implementations work differently per address-family regarding client-id/DUID. * * - for IPv4, the calling code may determine a client-id (from NM's connection profile). * If present, it is taken. If not present, the DHCP plugin uses a plugin specific default. * - for "internal" plugin, the default is just "mac". * - for "dhclient", we try to get the configuration from dhclient's /etc/dhcp or fallback * to whatever dhclient uses by default. * We do it this way, because for dhclient the user may configure a default * outside of NM, and we want to honor that. Worse, dhclient could be a wapper * script where the wrapper script overwrites the client-id. We need to distinguish * between: force a particular client-id and leave it unspecified to whatever dhclient * wants. * * - for IPv6, the calling code always determines a client-id. It also specifies @enforce_duid, * to determine whether the given client-id must be used. * - for "internal" plugin @enforce_duid doesn't matter and the given client-id is * always used. * - for "dhclient", @enforce_duid FALSE means to first try to load the DUID from the * lease file, and only otherwise fallback to the given client-id. * - other plugins don't support DHCPv6. * It's done this way, so that existing dhclient setups don't change behavior on upgrade. * * This difference is cumbersome and only exists because of "dhclient" which supports hacking the * default outside of NetworkManager API. */ if (addr_family == AF_INET) { success = nm_dhcp_client_start_ip4(client, dhcp_client_id, dhcp_anycast_addr, last_ip4_address, error); } else { success = nm_dhcp_client_start_ip6(client, dhcp_client_id, enforce_duid, dhcp_anycast_addr, ipv6_ll_addr, privacy, needed_prefixes, error); } if (!success) { remove_client_unref(self, client); return NULL; } return g_object_ref(client); } /* Caller owns a reference to the NMDhcpClient on return */ NMDhcpClient * nm_dhcp_manager_start_ip4(NMDhcpManager * self, NMDedupMultiIndex * multi_idx, const char * iface, int ifindex, GBytes * hwaddr, GBytes * bcast_hwaddr, const char * uuid, guint32 route_table, guint32 route_metric, gboolean send_hostname, const char * dhcp_hostname, const char * dhcp_fqdn, NMDhcpHostnameFlags hostname_flags, const char * mud_url, GBytes * dhcp_client_id, guint32 timeout, const char * dhcp_anycast_addr, const char * last_ip_address, GBytes * vendor_class_identifier, const char *const * reject_servers, GError ** error) { NMDhcpManagerPrivate *priv; const char * hostname = NULL; gs_free char * hostname_tmp = NULL; gboolean use_fqdn = FALSE; char * dot; g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL); priv = NM_DHCP_MANAGER_GET_PRIVATE(self); if (send_hostname) { /* Use, in order of preference: * 1. FQDN from configuration * 2. hostname from configuration * 3. system hostname (only host part) */ if (dhcp_fqdn) { hostname = dhcp_fqdn; use_fqdn = TRUE; } else if (dhcp_hostname) hostname = dhcp_hostname; else { hostname = priv->default_hostname; if (hostname) { hostname_tmp = g_strdup(hostname); dot = strchr(hostname_tmp, '.'); if (dot) *dot = '\0'; hostname = hostname_tmp; } } } return client_start(self, AF_INET, multi_idx, iface, ifindex, hwaddr, bcast_hwaddr, uuid, route_table, route_metric, NULL, dhcp_client_id, FALSE, 0, FALSE, timeout, dhcp_anycast_addr, hostname, use_fqdn, hostname_flags, mud_url, FALSE, 0, last_ip_address, 0, vendor_class_identifier, reject_servers, error); } /* Caller owns a reference to the NMDhcpClient on return */ NMDhcpClient * nm_dhcp_manager_start_ip6(NMDhcpManager * self, NMDedupMultiIndex * multi_idx, const char * iface, int ifindex, const struct in6_addr * ll_addr, const char * uuid, guint32 route_table, guint32 route_metric, gboolean send_hostname, const char * dhcp_hostname, NMDhcpHostnameFlags hostname_flags, const char * mud_url, GBytes * duid, gboolean enforce_duid, guint32 iaid, gboolean iaid_explicit, guint32 timeout, const char * dhcp_anycast_addr, gboolean info_only, NMSettingIP6ConfigPrivacy privacy, guint needed_prefixes, GError ** error) { NMDhcpManagerPrivate *priv; const char * hostname = NULL; g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL); priv = NM_DHCP_MANAGER_GET_PRIVATE(self); if (send_hostname) { /* Always prefer the explicit dhcp-hostname if given */ hostname = dhcp_hostname ?: priv->default_hostname; } return client_start(self, AF_INET6, multi_idx, iface, ifindex, NULL, NULL, uuid, route_table, route_metric, ll_addr, duid, enforce_duid, iaid, iaid_explicit, timeout, dhcp_anycast_addr, hostname, TRUE, hostname_flags, mud_url, info_only, privacy, NULL, needed_prefixes, NULL, NULL, error); } void nm_dhcp_manager_set_default_hostname(NMDhcpManager *manager, const char *hostname) { NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE(manager); nm_clear_g_free(&priv->default_hostname); /* Never send 'localhost'-type names to the DHCP server */ if (!nm_utils_is_specific_hostname(hostname)) return; priv->default_hostname = g_strdup(hostname); } const char * nm_dhcp_manager_get_config(NMDhcpManager *self) { const NMDhcpClientFactory *factory; g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL); factory = NM_DHCP_MANAGER_GET_PRIVATE(self)->client_factory; return factory ? factory->name : NULL; } /*****************************************************************************/ NM_DEFINE_SINGLETON_GETTER(NMDhcpManager, nm_dhcp_manager_get, NM_TYPE_DHCP_MANAGER); void nmtst_dhcp_manager_unget(gpointer self) { _nmtst_nm_dhcp_manager_get_reset(self); } static void nm_dhcp_manager_init(NMDhcpManager *self) { NMDhcpManagerPrivate * priv = NM_DHCP_MANAGER_GET_PRIVATE(self); NMConfig * config = nm_config_get(); gs_free char * client_free = NULL; const char * client; int i; const NMDhcpClientFactory *client_factory = NULL; c_list_init(&priv->dhcp_client_lst_head); for (i = 0; i < G_N_ELEMENTS(_nm_dhcp_manager_factories); i++) { const NMDhcpClientFactory *f = _nm_dhcp_manager_factories[i]; if (!f) continue; nm_log_dbg(LOGD_DHCP, "dhcp-init: enabled DHCP client '%s'%s%s", f->name, _client_factory_available(f) ? "" : " (not available)", f->experimental ? " (undocumented internal plugin)" : ""); } /* Client-specific setup */ client_free = nm_config_data_get_value(nm_config_get_data_orig(config), NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_DHCP, NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY); client = client_free; if (nm_config_get_configure_and_quit(config) == NM_CONFIG_CONFIGURE_AND_QUIT_ENABLED) { client_factory = &_nm_dhcp_client_factory_internal; if (client && !nm_streq(client, client_factory->name)) nm_log_info(LOGD_DHCP, "dhcp-init: Using internal DHCP client since configure-and-quit is set."); } else { if (client) { client_factory = _client_factory_available(_client_factory_find_by_name(client)); if (!client_factory) nm_log_warn(LOGD_DHCP, "dhcp-init: DHCP client '%s' not available", client); } if (!client_factory) { client_factory = _client_factory_find_by_name("" NM_CONFIG_DEFAULT_MAIN_DHCP); if (!client_factory) nm_log_err(LOGD_DHCP, "dhcp-init: default DHCP client '%s' is not installed", NM_CONFIG_DEFAULT_MAIN_DHCP); else { client_factory = _client_factory_available(client_factory); if (!client_factory) nm_log_info(LOGD_DHCP, "dhcp-init: default DHCP client '%s' is not available", NM_CONFIG_DEFAULT_MAIN_DHCP); } } if (!client_factory) { for (i = 0; i < G_N_ELEMENTS(_nm_dhcp_manager_factories); i++) { client_factory = _client_factory_available(_nm_dhcp_manager_factories[i]); if (client_factory) break; } } } g_return_if_fail(client_factory); nm_log_info(LOGD_DHCP, "dhcp-init: Using DHCP client '%s'", client_factory->name); /* NOTE: currently the DHCP plugin is chosen once at start. It's not * possible to reload that configuration. If that ever becomes possible, * beware that the "dhcp-plugin" device spec made decisions based on * the previous plugin and may need reevaluation. */ priv->client_factory = client_factory; } static void dispose(GObject *object) { NMDhcpManager * self = NM_DHCP_MANAGER(object); NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE(self); NMDhcpClient * client, *client_safe; c_list_for_each_entry_safe (client, client_safe, &priv->dhcp_client_lst_head, dhcp_client_lst) remove_client_unref(self, client); G_OBJECT_CLASS(nm_dhcp_manager_parent_class)->dispose(object); nm_clear_g_free(&priv->default_hostname); } static void nm_dhcp_manager_class_init(NMDhcpManagerClass *manager_class) { GObjectClass *object_class = G_OBJECT_CLASS(manager_class); object_class->dispose = dispose; }