diff options
| author | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2021-02-11 18:11:46 +0100 |
| commit | 80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch) | |
| tree | e3b229aa94e8dcf0590f2317664176e7b8f7607b /src/core/dhcp | |
| parent | 65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff) | |
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'src/core/dhcp')
26 files changed, 11478 insertions, 0 deletions
diff --git a/src/core/dhcp/meson.build b/src/core/dhcp/meson.build new file mode 100644 index 00000000..1bb004cd --- /dev/null +++ b/src/core/dhcp/meson.build @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executable( + 'nm-dhcp-helper', + 'nm-dhcp-helper.c', + dependencies: glib_nm_default_dep, + c_args: [ + '-DG_LOG_DOMAIN="nm-dhcp-helper"', + ], + link_args: ldflags_linker_script_binary, + link_depends: linker_script_binary, + install: true, + install_dir: nm_libexecdir, +) + +if enable_tests + subdir('tests') +endif diff --git a/src/core/dhcp/nm-dhcp-client-logging.h b/src/core/dhcp/nm-dhcp-client-logging.h new file mode 100644 index 00000000..d69b3ebd --- /dev/null +++ b/src/core/dhcp/nm-dhcp-client-logging.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2016 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_CLIENT_LOGGING_H__ +#define __NETWORKMANAGER_DHCP_CLIENT_LOGGING_H__ + +#include "nm-dhcp-client.h" + +static inline NMLogDomain +_nm_dhcp_client_get_domain(NMDhcpClient *self) +{ + if (self) { + switch (nm_dhcp_client_get_addr_family(self)) { + case AF_INET: + return LOGD_DHCP4; + case AF_INET6: + return LOGD_DHCP6; + default: + nm_assert_not_reached(); + break; + } + } + return LOGD_DHCP; +} + +#define _NMLOG_PREFIX_NAME "dhcp" +#define _NMLOG_DOMAIN LOGD_DHCP +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + /* we check first for LOGD_DHCP instead of the correct domain. + * In the worst case, we guess wrong and enter the block. + * + * Same for the _NMLOG_ENABLED() macro. Probably it would be more + * expensive to determine the correct value then what we could + * safe. */ \ + if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) { \ + NMDhcpClient * _self = (NMDhcpClient *) (self); \ + const char * __ifname = _self ? nm_dhcp_client_get_iface(_self) : NULL; \ + const NMLogDomain _domain = _nm_dhcp_client_get_domain(_self); \ + \ + nm_log(_level, \ + _domain, \ + __ifname, \ + NULL, \ + "%s%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + (_domain == LOGD_DHCP4 ? "4" : (_domain == LOGD_DHCP6 ? "6" : "")), \ + NM_PRINT_FMT_QUOTED(__ifname, " (", __ifname, ")", "") \ + _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +#define _NMLOG2(level, domain, ifname, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + const NMLogDomain _domain = (domain); \ + \ + /* we check first for LOGD_DHCP instead of the correct domain. + * In the worst case, we guess wrong and enter the block. + * + * Same for the _NMLOG_ENABLED() macro. Probably it would be more + * expensive to determine the correct value then what we could + * safe. */ \ + if (nm_logging_enabled(_level, _domain)) { \ + const char *__ifname = (ifname); \ + \ + nm_log(_level, \ + _domain, \ + __ifname, \ + NULL, \ + "%s%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + _NMLOG_PREFIX_NAME, \ + (_domain == LOGD_DHCP4 ? "4" : (_domain == LOGD_DHCP6 ? "6" : "")), \ + NM_PRINT_FMT_QUOTED(__ifname, " (", __ifname, ")", "") \ + _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +#endif /* __NETWORKMANAGER_DHCP_CLIENT_LOGGING_H__ */ diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c new file mode 100644 index 00000000..c38c814e --- /dev/null +++ b/src/core/dhcp/nm-dhcp-client.c @@ -0,0 +1,1368 @@ +/* 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 <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <linux/rtnetlink.h> + +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-glib-aux/nm-random-utils.h" + +#include "NetworkManagerUtils.h" +#include "nm-utils.h" +#include "nm-dhcp-utils.h" +#include "nm-dhcp-options.h" +#include "platform/nm-platform.h" + +#include "nm-dhcp-client-logging.h" + +/*****************************************************************************/ + +enum { SIGNAL_STATE_CHANGED, SIGNAL_PREFIX_DELEGATED, LAST_SIGNAL }; + +static guint signals[LAST_SIGNAL] = {0}; + +NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient, + PROP_ADDR_FAMILY, + PROP_FLAGS, + PROP_HWADDR, + PROP_BROADCAST_HWADDR, + PROP_IFACE, + PROP_IFINDEX, + PROP_MULTI_IDX, + PROP_ROUTE_METRIC, + PROP_ROUTE_TABLE, + PROP_TIMEOUT, + PROP_UUID, + PROP_IAID, + PROP_IAID_EXPLICIT, + PROP_HOSTNAME, + PROP_HOSTNAME_FLAGS, + PROP_MUD_URL, + PROP_VENDOR_CLASS_IDENTIFIER, + PROP_REJECT_SERVERS, ); + +typedef struct _NMDhcpClientPrivate { + NMDedupMultiIndex * multi_idx; + char * iface; + GBytes * hwaddr; + GBytes * bcast_hwaddr; + char * uuid; + GBytes * client_id; + char * hostname; + const char ** reject_servers; + char * mud_url; + GBytes * vendor_class_identifier; + pid_t pid; + guint timeout_id; + guint watch_id; + int addr_family; + int ifindex; + guint32 route_table; + guint32 route_metric; + guint32 timeout; + guint32 iaid; + NMDhcpState state; + NMDhcpHostnameFlags hostname_flags; + bool info_only : 1; + bool use_fqdn : 1; + bool iaid_explicit : 1; +} 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) + +/*****************************************************************************/ + +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; +} + +NMDedupMultiIndex * +nm_dhcp_client_get_multi_idx(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->multi_idx; +} + +const char * +nm_dhcp_client_get_iface(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->iface; +} + +int +nm_dhcp_client_get_ifindex(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->ifindex; +} + +int +nm_dhcp_client_get_addr_family(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), AF_UNSPEC); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->addr_family; +} + +const char * +nm_dhcp_client_get_uuid(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->uuid; +} + +GBytes * +nm_dhcp_client_get_hw_addr(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->hwaddr; +} + +GBytes * +nm_dhcp_client_get_broadcast_hw_addr(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->bcast_hwaddr; +} + +guint32 +nm_dhcp_client_get_route_table(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), RT_TABLE_MAIN); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->route_table; +} + +void +nm_dhcp_client_set_route_table(NMDhcpClient *self, guint32 route_table) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + if (route_table != priv->route_table) { + priv->route_table = route_table; + _notify(self, PROP_ROUTE_TABLE); + } +} + +guint32 +nm_dhcp_client_get_route_metric(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), G_MAXUINT32); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->route_metric; +} + +void +nm_dhcp_client_set_route_metric(NMDhcpClient *self, guint32 route_metric) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + if (route_metric != priv->route_metric) { + priv->route_metric = route_metric; + _notify(self, PROP_ROUTE_METRIC); + } +} + +guint32 +nm_dhcp_client_get_timeout(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 0); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->timeout; +} + +guint32 +nm_dhcp_client_get_iaid(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 0); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->iaid; +} + +gboolean +nm_dhcp_client_get_iaid_explicit(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->iaid_explicit; +} + +GBytes * +nm_dhcp_client_get_client_id(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->client_id; +} + +static void +_set_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->client_id == client_id + || (priv->client_id && client_id && g_bytes_equal(priv->client_id, client_id))) { + if (take && client_id) + g_bytes_unref(client_id); + return; + } + + if (priv->client_id) + g_bytes_unref(priv->client_id); + priv->client_id = client_id; + if (!take && client_id) + g_bytes_ref(client_id); + + { + gs_free char *s = NULL; + + _LOGT("%s: set %s", + nm_dhcp_client_get_addr_family(self) == AF_INET6 ? "duid" : "client-id", + priv->client_id ? (s = nm_dhcp_utils_duid_to_string(priv->client_id)) : "default"); + } +} + +void +nm_dhcp_client_set_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_client_id(self, client_id, FALSE); +} + +void +nm_dhcp_client_set_client_id_bin(NMDhcpClient *self, + guint8 type, + const guint8 *client_id, + gsize len) +{ + guint8 *buf; + GBytes *b; + + g_return_if_fail(NM_IS_DHCP_CLIENT(self)); + g_return_if_fail(client_id); + g_return_if_fail(len > 0); + + buf = g_malloc(len + 1); + buf[0] = type; + memcpy(buf + 1, client_id, len); + b = g_bytes_new_take(buf, len + 1); + _set_client_id(self, b, TRUE); +} + +const char * +nm_dhcp_client_get_hostname(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->hostname; +} + +NMDhcpHostnameFlags +nm_dhcp_client_get_hostname_flags(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NM_DHCP_HOSTNAME_FLAG_NONE); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->hostname_flags; +} + +gboolean +nm_dhcp_client_get_info_only(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->info_only; +} + +gboolean +nm_dhcp_client_get_use_fqdn(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->use_fqdn; +} + +const char * +nm_dhcp_client_get_mud_url(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->mud_url; +} + +GBytes * +nm_dhcp_client_get_vendor_class_identifier(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE(self)->vendor_class_identifier; +} + +const char *const * +nm_dhcp_client_get_reject_servers(NMDhcpClient *self) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL); + + return (const char *const *) NM_DHCP_CLIENT_GET_PRIVATE(self)->reject_servers; +} + +/*****************************************************************************/ + +static const char *state_table[NM_DHCP_STATE_MAX + 1] = { + [NM_DHCP_STATE_UNKNOWN] = "unknown", + [NM_DHCP_STATE_BOUND] = "bound", + [NM_DHCP_STATE_EXTENDED] = "extended", + [NM_DHCP_STATE_TIMEOUT] = "timeout", + [NM_DHCP_STATE_EXPIRE] = "expire", + [NM_DHCP_STATE_DONE] = "done", + [NM_DHCP_STATE_FAIL] = "fail", + [NM_DHCP_STATE_TERMINATED] = "terminated", +}; + +static const char * +state_to_string(NMDhcpState state) +{ + if ((gsize) state < G_N_ELEMENTS(state_table)) + return state_table[state]; + return NULL; +} + +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) + 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 +timeout_cleanup(NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + nm_clear_g_source(&priv->timeout_id); +} + +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->iface); + } + priv->pid = -1; +} + +void +nm_dhcp_client_set_state(NMDhcpClient *self, + NMDhcpState new_state, + NMIPConfig * ip_config, + GHashTable * options) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) { + g_return_if_fail(NM_IS_IP_CONFIG_ADDR_FAMILY(ip_config, priv->addr_family)); + g_return_if_fail(options); + } else { + g_return_if_fail(!ip_config); + g_return_if_fail(!options); + } + + if (new_state >= NM_DHCP_STATE_BOUND) + timeout_cleanup(self); + if (new_state >= NM_DHCP_STATE_TIMEOUT) + watch_cleanup(self); + + /* The client may send same-state transitions for RENEW/REBIND events and + * the lease may have changed, so handle same-state transitions for the + * EXTENDED and BOUND states. Ignore same-state transitions for other + * events since the lease won't have changed and the state was already handled. + */ + if ((priv->state == new_state) + && !NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) + return; + + if (_LOGD_ENABLED()) { + gs_free const char **keys = NULL; + guint i, nkeys; + + keys = nm_utils_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 (_LOGT_ENABLED() && priv->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 = + NM_IS_IPv4(priv->addr_family) + ? 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%s", + state_to_string(priv->state), + state_to_string(new_state), + NM_PRINT_FMT_QUOTED(addr, ", address=", addr, "", "")); + } + + priv->state = new_state; + g_signal_emit(G_OBJECT(self), signals[SIGNAL_STATE_CHANGED], 0, new_state, ip_config, options); +} + +static gboolean +transaction_timeout(gpointer user_data) +{ + NMDhcpClient * self = NM_DHCP_CLIENT(user_data); + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + priv->timeout_id = 0; + _LOGW("request timed out"); + nm_dhcp_client_set_state(self, NM_DHCP_STATE_TIMEOUT, NULL, NULL); + return G_SOURCE_REMOVE; +} + +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); + + g_return_if_fail(priv->watch_id); + priv->watch_id = 0; + + if (WIFEXITED(status)) + _LOGI("client pid %d exited with status %d", pid, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + _LOGI("client pid %d killed by signal %d", pid, WTERMSIG(status)); + else if (WIFSTOPPED(status)) + _LOGI("client pid %d stopped by signal %d", pid, WSTOPSIG(status)); + else if (WIFCONTINUED(status)) + _LOGI("client pid %d resumed (by SIGCONT)", pid); + else + _LOGW("client died abnormally"); + + priv->pid = -1; + + nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL, NULL); +} + +void +nm_dhcp_client_start_timeout(NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + g_return_if_fail(priv->timeout_id == 0); + + /* Set up a timeout on the transaction to kill it after the timeout */ + + if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY) + return; + + priv->timeout_id = g_timeout_add_seconds(priv->timeout, transaction_timeout, self); +} + +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; + + nm_dhcp_client_start_timeout(self); + + 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); + timeout_cleanup(self); +} + +gboolean +nm_dhcp_client_start_ip4(NMDhcpClient *self, + GBytes * client_id, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + 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->addr_family == AF_INET, FALSE); + g_return_val_if_fail(priv->uuid != NULL, FALSE); + + if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY) + _LOGI("activation: beginning transaction (no timeout)"); + else + _LOGI("activation: beginning transaction (timeout in %u seconds)", (guint) priv->timeout); + + nm_dhcp_client_set_client_id(self, client_id); + + return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, + dhcp_anycast_addr, + last_ip4_address, + error); +} + +gboolean +nm_dhcp_client_accept(NMDhcpClient *self, GError **error) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 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_decline(NMDhcpClient *self, const char *error_message, GError **error) +{ + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 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; +} + +gboolean +nm_dhcp_client_start_ip6(NMDhcpClient * self, + GBytes * client_id, + gboolean enforce_duid, + const char * dhcp_anycast_addr, + const struct in6_addr * ll_addr, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes, + GError ** error) +{ + NMDhcpClientPrivate *priv; + gs_unref_bytes GBytes *own_client_id = NULL; + + g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE); + g_return_val_if_fail(client_id, FALSE); + + priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + g_return_val_if_fail(priv->pid == -1, FALSE); + g_return_val_if_fail(priv->addr_family == AF_INET6, FALSE); + g_return_val_if_fail(priv->uuid != NULL, FALSE); + g_return_val_if_fail(!priv->client_id, FALSE); + + if (!enforce_duid) + own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self); + + _set_client_id(self, own_client_id ?: client_id, FALSE); + + if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY) + _LOGI("activation: beginning transaction (no timeout)"); + else + _LOGI("activation: beginning transaction (timeout in %u seconds)", (guint) priv->timeout); + + return NM_DHCP_CLIENT_GET_CLASS(self) + ->ip6_start(self, dhcp_anycast_addr, ll_addr, privacy, needed_prefixes, 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); + + /* 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_DONE, NULL, 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) +{ + g_signal_emit(G_OBJECT(self), signals[SIGNAL_PREFIX_DELEGATED], 0, prefix); +} + +gboolean +nm_dhcp_client_handle_event(gpointer unused, + const char * iface, + int pid, + GVariant * options, + const char * reason, + NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv; + guint32 old_state; + guint32 new_state; + gs_unref_hashtable GHashTable *str_options = NULL; + gs_unref_object NMIPConfig *ip_config = 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 (g_strcmp0(priv->iface, iface) != 0) + return FALSE; + if (priv->pid != pid) + return FALSE; + + old_state = priv->state; + new_state = reason_to_state(self, priv->iface, reason); + _LOGD("DHCP state '%s' -> '%s' (reason: '%s')", + state_to_string(old_state), + state_to_string(new_state), + reason); + + if (new_state == NM_DHCP_STATE_NOOP) + return TRUE; + + if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) { + 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->addr_family == AF_INET) { + ip_config = NM_IP_CONFIG_CAST( + nm_dhcp_utils_ip4_config_from_options(nm_dhcp_client_get_multi_idx(self), + priv->ifindex, + priv->iface, + str_options, + priv->route_table, + priv->route_metric)); + } else { + prefix = nm_dhcp_utils_ip6_prefix_from_options(str_options); + ip_config = NM_IP_CONFIG_CAST( + nm_dhcp_utils_ip6_config_from_options(nm_dhcp_client_get_multi_idx(self), + priv->ifindex, + priv->iface, + str_options, + priv->info_only)); + } + } else + g_warn_if_reached(); + } + + 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); + } else { + /* Fail if no valid IP config was received */ + if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED) && !ip_config) { + _LOGW("client bound but IP config not received"); + new_state = NM_DHCP_STATE_FAIL; + nm_clear_pointer(&str_options, g_hash_table_unref); + } + + nm_dhcp_client_set_state(self, new_state, ip_config, str_options); + } + + 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->addr_family == AF_INET); + + if (!priv->reject_servers || !priv->reject_servers[0]) + return FALSE; + + for (i = 0; priv->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->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 +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(object); + + switch (prop_id) { + case PROP_IFACE: + g_value_set_string(value, priv->iface); + break; + case PROP_IFINDEX: + g_value_set_int(value, priv->ifindex); + break; + case PROP_HWADDR: + g_value_set_boxed(value, priv->hwaddr); + break; + case PROP_BROADCAST_HWADDR: + g_value_set_boxed(value, priv->bcast_hwaddr); + break; + case PROP_ADDR_FAMILY: + g_value_set_int(value, priv->addr_family); + break; + case PROP_UUID: + g_value_set_string(value, priv->uuid); + break; + case PROP_IAID: + g_value_set_uint(value, priv->iaid); + break; + case PROP_IAID_EXPLICIT: + g_value_set_boolean(value, priv->iaid_explicit); + break; + case PROP_HOSTNAME: + g_value_set_string(value, priv->hostname); + break; + case PROP_ROUTE_METRIC: + g_value_set_uint(value, priv->route_metric); + break; + case PROP_ROUTE_TABLE: + g_value_set_uint(value, priv->route_table); + break; + case PROP_TIMEOUT: + g_value_set_uint(value, priv->timeout); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void +set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(object); + guint flags; + + switch (prop_id) { + case PROP_FLAGS: + /* construct-only */ + flags = g_value_get_uint(value); + nm_assert( + (flags & ~((guint)(NM_DHCP_CLIENT_FLAGS_INFO_ONLY | NM_DHCP_CLIENT_FLAGS_USE_FQDN))) + == 0); + priv->info_only = NM_FLAGS_HAS(flags, NM_DHCP_CLIENT_FLAGS_INFO_ONLY); + priv->use_fqdn = NM_FLAGS_HAS(flags, NM_DHCP_CLIENT_FLAGS_USE_FQDN); + break; + case PROP_MULTI_IDX: + /* construct-only */ + priv->multi_idx = g_value_get_pointer(value); + if (!priv->multi_idx) + g_return_if_reached(); + nm_dedup_multi_index_ref(priv->multi_idx); + break; + case PROP_IFACE: + /* construct-only */ + priv->iface = g_value_dup_string(value); + g_return_if_fail(priv->iface); + nm_assert(nm_utils_ifname_valid_kernel(priv->iface, NULL)); + break; + case PROP_IFINDEX: + /* construct-only */ + priv->ifindex = g_value_get_int(value); + g_return_if_fail(priv->ifindex > 0); + break; + case PROP_HWADDR: + /* construct-only */ + priv->hwaddr = g_value_dup_boxed(value); + break; + case PROP_BROADCAST_HWADDR: + /* construct-only */ + priv->bcast_hwaddr = g_value_dup_boxed(value); + break; + case PROP_ADDR_FAMILY: + /* construct-only */ + priv->addr_family = g_value_get_int(value); + if (!NM_IN_SET(priv->addr_family, AF_INET, AF_INET6)) + g_return_if_reached(); + break; + case PROP_UUID: + /* construct-only */ + priv->uuid = g_value_dup_string(value); + break; + case PROP_IAID: + /* construct-only */ + priv->iaid = g_value_get_uint(value); + break; + case PROP_IAID_EXPLICIT: + /* construct-only */ + priv->iaid_explicit = g_value_get_boolean(value); + break; + case PROP_HOSTNAME: + /* construct-only */ + priv->hostname = g_value_dup_string(value); + break; + case PROP_HOSTNAME_FLAGS: + /* construct-only */ + priv->hostname_flags = g_value_get_uint(value); + break; + case PROP_MUD_URL: + /* construct-only */ + priv->mud_url = g_value_dup_string(value); + break; + case PROP_ROUTE_TABLE: + priv->route_table = g_value_get_uint(value); + break; + case PROP_ROUTE_METRIC: + priv->route_metric = g_value_get_uint(value); + break; + case PROP_TIMEOUT: + /* construct-only */ + priv->timeout = g_value_get_uint(value); + break; + case PROP_VENDOR_CLASS_IDENTIFIER: + /* construct-only */ + priv->vendor_class_identifier = g_value_dup_boxed(value); + break; + case PROP_REJECT_SERVERS: + /* construct-only */ + priv->reject_servers = nm_utils_strv_dup_packed(g_value_get_boxed(value), -1); + 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; + + c_list_init(&self->dhcp_client_lst); + + priv->pid = -1; +} + +static void +dispose(GObject *object) +{ + NMDhcpClient * self = NM_DHCP_CLIENT(object); + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self); + + /* Stopping the client is left up to the controlling device + * explicitly since we may want to quit NetworkManager but not terminate + * the DHCP client. + */ + + nm_assert(c_list_is_empty(&self->dhcp_client_lst)); + + watch_cleanup(self); + timeout_cleanup(self); + + nm_clear_g_free(&priv->iface); + nm_clear_g_free(&priv->hostname); + nm_clear_g_free(&priv->uuid); + nm_clear_g_free(&priv->mud_url); + nm_clear_g_free(&priv->reject_servers); + nm_clear_pointer(&priv->client_id, g_bytes_unref); + nm_clear_pointer(&priv->hwaddr, g_bytes_unref); + nm_clear_pointer(&priv->bcast_hwaddr, g_bytes_unref); + nm_clear_pointer(&priv->vendor_class_identifier, g_bytes_unref); + + G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object); + + priv->multi_idx = nm_dedup_multi_index_unref(priv->multi_idx); +} + +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->get_property = get_property; + object_class->set_property = set_property; + + client_class->stop = stop; + client_class->get_duid = get_duid; + + obj_properties[PROP_MULTI_IDX] = + g_param_spec_pointer(NM_DHCP_CLIENT_MULTI_IDX, + "", + "", + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IFACE] = + g_param_spec_string(NM_DHCP_CLIENT_INTERFACE, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IFINDEX] = + g_param_spec_int(NM_DHCP_CLIENT_IFINDEX, + "", + "", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_HWADDR] = + g_param_spec_boxed(NM_DHCP_CLIENT_HWADDR, + "", + "", + G_TYPE_BYTES, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_BROADCAST_HWADDR] = + g_param_spec_boxed(NM_DHCP_CLIENT_BROADCAST_HWADDR, + "", + "", + G_TYPE_BYTES, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ADDR_FAMILY] = + g_param_spec_int(NM_DHCP_CLIENT_ADDR_FAMILY, + "", + "", + 0, + G_MAXINT, + AF_UNSPEC, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_UUID] = + g_param_spec_string(NM_DHCP_CLIENT_UUID, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IAID] = + g_param_spec_uint(NM_DHCP_CLIENT_IAID, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IAID_EXPLICIT] = + g_param_spec_boolean(NM_DHCP_CLIENT_IAID_EXPLICIT, + "", + "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_HOSTNAME] = + g_param_spec_string(NM_DHCP_CLIENT_HOSTNAME, + "", + "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_HOSTNAME_FLAGS] = + g_param_spec_uint(NM_DHCP_CLIENT_HOSTNAME_FLAGS, + "", + "", + 0, + G_MAXUINT32, + NM_DHCP_HOSTNAME_FLAG_NONE, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_MUD_URL] = + g_param_spec_string(NM_DHCP_CLIENT_MUD_URL, + "", + "", + NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ROUTE_TABLE] = + g_param_spec_uint(NM_DHCP_CLIENT_ROUTE_TABLE, + "", + "", + 0, + G_MAXUINT32, + RT_TABLE_MAIN, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ROUTE_METRIC] = + g_param_spec_uint(NM_DHCP_CLIENT_ROUTE_METRIC, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + G_STATIC_ASSERT_EXPR(G_MAXINT32 == NM_DHCP_TIMEOUT_INFINITY); + obj_properties[PROP_TIMEOUT] = + g_param_spec_uint(NM_DHCP_CLIENT_TIMEOUT, + "", + "", + 1, + G_MAXINT32, + NM_DHCP_TIMEOUT_DEFAULT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_FLAGS] = + g_param_spec_uint(NM_DHCP_CLIENT_FLAGS, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_VENDOR_CLASS_IDENTIFIER] = + g_param_spec_boxed(NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER, + "", + "", + G_TYPE_BYTES, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_REJECT_SERVERS] = + g_param_spec_boxed(NM_DHCP_CLIENT_REJECT_SERVERS, + "", + "", + G_TYPE_STRV, + 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_STATE_CHANGED] = g_signal_new(NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 3, + G_TYPE_UINT, + G_TYPE_OBJECT, + G_TYPE_HASH_TABLE); + + signals[SIGNAL_PREFIX_DELEGATED] = g_signal_new(NM_DHCP_CLIENT_SIGNAL_PREFIX_DELEGATED, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_POINTER); +} diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h new file mode 100644 index 00000000..72ab477d --- /dev/null +++ b/src/core/dhcp/nm-dhcp-client.h @@ -0,0 +1,233 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2010 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_CLIENT_H__ +#define __NETWORKMANAGER_DHCP_CLIENT_H__ + +#include "nm-setting-ip4-config.h" +#include "nm-setting-ip6-config.h" +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" +#include "nm-dhcp-utils.h" + +#define NM_DHCP_TIMEOUT_DEFAULT ((guint32) 45) /* default DHCP timeout, in seconds */ +#define NM_DHCP_TIMEOUT_INFINITY ((guint32) G_MAXINT32) + +#define NM_TYPE_DHCP_CLIENT (nm_dhcp_client_get_type()) +#define NM_DHCP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_CLIENT, NMDhcpClient)) +#define NM_DHCP_CLIENT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_CLIENT, NMDhcpClientClass)) +#define NM_IS_DHCP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_CLIENT)) +#define NM_IS_DHCP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_CLIENT)) +#define NM_DHCP_CLIENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_CLIENT, NMDhcpClientClass)) + +#define NM_DHCP_CLIENT_ADDR_FAMILY "addr-family" +#define NM_DHCP_CLIENT_FLAGS "flags" +#define NM_DHCP_CLIENT_HWADDR "hwaddr" +#define NM_DHCP_CLIENT_BROADCAST_HWADDR "broadcast-hwaddr" +#define NM_DHCP_CLIENT_IFINDEX "ifindex" +#define NM_DHCP_CLIENT_INTERFACE "iface" +#define NM_DHCP_CLIENT_MULTI_IDX "multi-idx" +#define NM_DHCP_CLIENT_HOSTNAME "hostname" +#define NM_DHCP_CLIENT_MUD_URL "mud-url" +#define NM_DHCP_CLIENT_ROUTE_METRIC "route-metric" +#define NM_DHCP_CLIENT_ROUTE_TABLE "route-table" +#define NM_DHCP_CLIENT_TIMEOUT "timeout" +#define NM_DHCP_CLIENT_UUID "uuid" +#define NM_DHCP_CLIENT_IAID "iaid" +#define NM_DHCP_CLIENT_IAID_EXPLICIT "iaid-explicit" +#define NM_DHCP_CLIENT_HOSTNAME_FLAGS "hostname-flags" +#define NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER "vendor-class-identifier" +#define NM_DHCP_CLIENT_REJECT_SERVERS "reject-servers" + +#define NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED "state-changed" +#define NM_DHCP_CLIENT_SIGNAL_PREFIX_DELEGATED "prefix-delegated" + +typedef enum { + NM_DHCP_STATE_UNKNOWN = 0, + NM_DHCP_STATE_BOUND, /* new lease */ + NM_DHCP_STATE_EXTENDED, /* lease extended */ + NM_DHCP_STATE_TIMEOUT, /* timed out contacting server */ + NM_DHCP_STATE_DONE, /* client quit or stopped */ + NM_DHCP_STATE_EXPIRE, /* lease expired or NAKed */ + NM_DHCP_STATE_FAIL, /* failed for some reason */ + NM_DHCP_STATE_TERMINATED, /* client is no longer running */ + NM_DHCP_STATE_NOOP, /* state is a non operation for NetworkManager */ + __NM_DHCP_STATE_MAX, + NM_DHCP_STATE_MAX = __NM_DHCP_STATE_MAX - 1, +} NMDhcpState; + +struct _NMDhcpClientPrivate; + +typedef struct { + GObject parent; + struct _NMDhcpClientPrivate *_priv; + CList dhcp_client_lst; +} NMDhcpClient; + +typedef enum { + NM_DHCP_CLIENT_FLAGS_INFO_ONLY = (1LL << 0), + NM_DHCP_CLIENT_FLAGS_USE_FQDN = (1LL << 1), +} NMDhcpClientFlags; + +typedef struct { + GObjectClass parent; + + gboolean (*ip4_start)(NMDhcpClient *self, + const char * anycast_addr, + const char * last_ip4_address, + GError ** error); + + gboolean (*accept)(NMDhcpClient *self, GError **error); + + gboolean (*decline)(NMDhcpClient *self, const char *error_message, GError **error); + + gboolean (*ip6_start)(NMDhcpClient * self, + const char * anycast_addr, + const struct in6_addr * ll_addr, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes, + GError ** error); + + void (*stop)(NMDhcpClient *self, gboolean release); + + /** + * get_duid: + * @self: the #NMDhcpClient + * + * Attempts to find an existing DHCPv6 DUID for this client in the DHCP + * client's persistent configuration. Returned DUID should be the binary + * representation of the DUID. If no DUID is found, %NULL should be + * returned. + */ + GBytes *(*get_duid)(NMDhcpClient *self); +} NMDhcpClientClass; + +GType nm_dhcp_client_get_type(void); + +struct _NMDedupMultiIndex *nm_dhcp_client_get_multi_idx(NMDhcpClient *self); + +pid_t nm_dhcp_client_get_pid(NMDhcpClient *self); + +int nm_dhcp_client_get_addr_family(NMDhcpClient *self); + +const char *nm_dhcp_client_get_iface(NMDhcpClient *self); + +int nm_dhcp_client_get_ifindex(NMDhcpClient *self); + +const char *nm_dhcp_client_get_uuid(NMDhcpClient *self); + +GBytes *nm_dhcp_client_get_duid(NMDhcpClient *self); + +GBytes *nm_dhcp_client_get_hw_addr(NMDhcpClient *self); + +GBytes *nm_dhcp_client_get_broadcast_hw_addr(NMDhcpClient *self); + +guint32 nm_dhcp_client_get_route_table(NMDhcpClient *self); + +void nm_dhcp_client_set_route_table(NMDhcpClient *self, guint32 route_table); + +guint32 nm_dhcp_client_get_route_metric(NMDhcpClient *self); + +void nm_dhcp_client_set_route_metric(NMDhcpClient *self, guint32 route_metric); + +guint32 nm_dhcp_client_get_timeout(NMDhcpClient *self); + +guint32 nm_dhcp_client_get_iaid(NMDhcpClient *self); + +gboolean nm_dhcp_client_get_iaid_explicit(NMDhcpClient *self); + +GBytes *nm_dhcp_client_get_client_id(NMDhcpClient *self); + +const char * nm_dhcp_client_get_hostname(NMDhcpClient *self); +const char * nm_dhcp_client_get_mud_url(NMDhcpClient *self); +const char *const *nm_dhcp_client_get_reject_servers(NMDhcpClient *self); + +NMDhcpHostnameFlags nm_dhcp_client_get_hostname_flags(NMDhcpClient *self); + +gboolean nm_dhcp_client_get_info_only(NMDhcpClient *self); + +gboolean nm_dhcp_client_get_use_fqdn(NMDhcpClient *self); + +GBytes *nm_dhcp_client_get_vendor_class_identifier(NMDhcpClient *self); + +gboolean nm_dhcp_client_start_ip4(NMDhcpClient *self, + GBytes * client_id, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error); + +gboolean nm_dhcp_client_start_ip6(NMDhcpClient * self, + GBytes * client_id, + gboolean enforce_duid, + const char * dhcp_anycast_addr, + const struct in6_addr * ll_addr, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes, + GError ** error); + +gboolean nm_dhcp_client_accept(NMDhcpClient *self, GError **error); + +gboolean nm_dhcp_client_decline(NMDhcpClient *self, const char *error_message, GError **error); + +void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release); + +/* Backend helpers for subclasses */ +void nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name); + +void nm_dhcp_client_stop_pid(pid_t pid, const char *iface); + +void nm_dhcp_client_start_timeout(NMDhcpClient *self); + +void nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid); + +void nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid); + +void nm_dhcp_client_set_state(NMDhcpClient *self, + NMDhcpState new_state, + NMIPConfig * ip_config, + GHashTable * options); /* str:str hash */ + +gboolean nm_dhcp_client_handle_event(gpointer unused, + const char * iface, + int pid, + GVariant * options, + const char * reason, + NMDhcpClient *self); + +void nm_dhcp_client_set_client_id(NMDhcpClient *self, GBytes *client_id); +void nm_dhcp_client_set_client_id_bin(NMDhcpClient *self, + guint8 type, + const guint8 *client_id, + gsize len); + +void nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient * self, + const NMPlatformIP6Address *prefix); + +gboolean nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr); + +/***************************************************************************** + * Client data + *****************************************************************************/ + +typedef struct { + GType (*get_type)(void); + GType (*get_type_per_addr_family)(int addr_family); + const char *name; + const char *(*get_path)(void); + bool experimental : 1; +} NMDhcpClientFactory; + +GType nm_dhcp_nettools_get_type(void); + +extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcanon; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcd; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_internal; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_systemd; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_nettools; + +#endif /* __NETWORKMANAGER_DHCP_CLIENT_H__ */ diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.c b/src/core/dhcp/nm-dhcp-dhclient-utils.c new file mode 100644 index 00000000..ad1e097f --- /dev/null +++ b/src/core/dhcp/nm-dhcp-dhclient-utils.c @@ -0,0 +1,714 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-dhcp-dhclient-utils.h" + +#include <ctype.h> +#include <arpa/inet.h> +#include <net/if.h> +#include <linux/if_ether.h> + +#include "nm-glib-aux/nm-dedup-multi.h" + +#include "nm-dhcp-utils.h" +#include "nm-ip4-config.h" +#include "nm-utils.h" +#include "platform/nm-platform.h" +#include "NetworkManagerUtils.h" + +#define TIMEOUT_TAG "timeout " +#define RETRY_TAG "retry " +#define CLIENTID_TAG "send dhcp-client-identifier" + +#define HOSTNAME4_TAG "send host-name" +#define HOSTNAME4_FORMAT HOSTNAME4_TAG " \"%s\"; # added by NetworkManager" + +#define FQDN_TAG_PREFIX "send fqdn." +#define FQDN_TAG FQDN_TAG_PREFIX "fqdn" +#define FQDN_FORMAT FQDN_TAG " \"%s\"; # added by NetworkManager" + +#define ALSOREQ_TAG "also request " +#define REQ_TAG "request " + +#define MUDURLv4_DEF "option mudurl code 161 = text;\n" +#define MUDURLv4_FMT "send mudurl \"%s\";\n" + +#define MUDURLv6_DEF "option dhcp6.mudurl code 112 = text;\n" +#define MUDURLv6_FMT "send dhcp6.mudurl \"%s\";\n" + +static void +add_request(GPtrArray *array, const char *item) +{ + guint i; + + for (i = 0; i < array->len; i++) { + if (nm_streq(array->pdata[i], item)) + return; + } + g_ptr_array_add(array, g_strdup(item)); +} + +static gboolean +grab_request_options(GPtrArray *store, const char *line) +{ + gs_free const char **line_v = NULL; + gsize i; + + /* Grab each 'request' or 'also request' option and save for later */ + line_v = nm_utils_strsplit_set(line, "\t ,"); + for (i = 0; line_v && line_v[i]; i++) { + const char *ss = nm_str_skip_leading_spaces(line_v[i]); + gsize l; + gboolean end = FALSE; + + if (!ss[0]) + continue; + if (ss[0] == ';') { + /* all done */ + return TRUE; + } + + if (!g_ascii_isalnum(ss[0])) + continue; + + l = strlen(ss); + + while (l > 0 && g_ascii_isspace(ss[l - 1])) { + ((char *) ss)[l - 1] = '\0'; + l--; + } + if (l > 0 && ss[l - 1] == ';') { + /* Remove the EOL marker */ + ((char *) ss)[l - 1] = '\0'; + end = TRUE; + } + + if (ss[0]) + add_request(store, ss); + + if (end) + return TRUE; + } + + return FALSE; +} + +static void +add_ip4_config(GString * str, + GBytes * client_id, + const char * hostname, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags) +{ + if (client_id) { + const char *p; + gsize l; + guint i; + + p = g_bytes_get_data(client_id, &l); + nm_assert(p); + + /* Allow type 0 (non-hardware address) to be represented as a string + * as long as all the characters are printable. + */ + for (i = 1; (p[0] == 0) && i < l; i++) { + if (!g_ascii_isprint(p[i]) || p[i] == '\\' || p[i] == '"') + break; + } + + g_string_append(str, CLIENTID_TAG " "); + if (i < l) { + /* Unprintable; convert to a hex string */ + for (i = 0; i < l; i++) { + if (i > 0) + g_string_append_c(str, ':'); + g_string_append_printf(str, "%02x", (guint8) p[i]); + } + } else { + /* Printable; just add to the line with type 0 */ + g_string_append_c(str, '"'); + g_string_append(str, "\\x00"); + g_string_append_len(str, p + 1, l - 1); + g_string_append_c(str, '"'); + } + g_string_append(str, "; # added by NetworkManager\n"); + } + + if (hostname) { + if (use_fqdn) { + g_string_append_printf(str, FQDN_FORMAT "\n", hostname); + + g_string_append_printf(str, + FQDN_TAG_PREFIX "encoded %s;\n", + (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) ? "on" + : "off"); + + g_string_append_printf( + str, + FQDN_TAG_PREFIX "server-update %s;\n", + (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE) ? "on" : "off"); + + g_string_append_printf(str, + FQDN_TAG_PREFIX "no-client-update %s;\n", + (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE) ? "on" + : "off"); + } else + g_string_append_printf(str, HOSTNAME4_FORMAT "\n", hostname); + } + + g_string_append_c(str, '\n'); + + /* Define options for classless static routes */ + g_string_append( + str, + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"); + g_string_append(str, + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"); + /* Web Proxy Auto-Discovery option (bgo #368423) */ + g_string_append(str, "option wpad code 252 = string;\n"); + + g_string_append_c(str, '\n'); +} + +static void +add_hostname6(GString *str, const char *hostname, NMDhcpHostnameFlags hostname_flags) +{ + if (hostname) { + g_string_append_printf(str, FQDN_FORMAT "\n", hostname); + if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE) + g_string_append(str, FQDN_TAG_PREFIX "server-update on;\n"); + if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE) + g_string_append(str, FQDN_TAG_PREFIX "no-client-update on;\n"); + g_string_append_c(str, '\n'); + } +} + +static void +add_mud_url_config(GString *str, const char *mud_url, int addr_family) +{ + if (mud_url) { + if (addr_family == AF_INET) { + g_string_append(str, MUDURLv4_DEF); + g_string_append_printf(str, MUDURLv4_FMT, mud_url); + } else { + g_string_append(str, MUDURLv6_DEF); + g_string_append_printf(str, MUDURLv6_FMT, mud_url); + } + } +} + +static GBytes * +read_client_id(const char *str) +{ + gs_free char *s = NULL; + char * p; + int i = 0, j = 0; + + nm_assert(!strncmp(str, CLIENTID_TAG, NM_STRLEN(CLIENTID_TAG))); + str += NM_STRLEN(CLIENTID_TAG); + + if (!g_ascii_isspace(*str)) + return NULL; + while (g_ascii_isspace(*str)) + str++; + + if (*str == '"') { + /* Parse string literal with escape sequences */ + s = g_strdup(str + 1); + p = strrchr(s, '"'); + if (p) + *p = '\0'; + else + return NULL; + + if (!s[0]) + return NULL; + + while (s[i]) { + if (s[i] == '\\' && s[i + 1] == 'x' && g_ascii_isxdigit(s[i + 2]) + && g_ascii_isxdigit(s[i + 3])) { + s[j++] = (g_ascii_xdigit_value(s[i + 2]) << 4) + g_ascii_xdigit_value(s[i + 3]); + i += 4; + continue; + } + if (s[i] == '\\' && s[i + 1] >= '0' && s[i + 1] <= '7' && s[1 + 2] >= '0' + && s[i + 2] <= '7' && s[1 + 3] >= '0' && s[i + 3] <= '7') { + s[j++] = ((s[i + 1] - '0') << 6) + ((s[i + 2] - '0') << 3) + (s[i + 3] - '0'); + i += 4; + continue; + } + s[j++] = s[i++]; + } + return g_bytes_new_take(g_steal_pointer(&s), j); + } + + /* Otherwise, try to read a hexadecimal sequence */ + s = g_strdup(str); + g_strchomp(s); + if (s[strlen(s) - 1] == ';') + s[strlen(s) - 1] = '\0'; + + return nm_utils_hexstr2bin(s); +} + +static gboolean +read_interface(const char *line, char *interface, guint size) +{ + gs_free char *dup = g_strdup(line + NM_STRLEN("interface")); + char * ptr = dup, *end; + + while (g_ascii_isspace(*ptr)) + ptr++; + + if (*ptr == '"') { + ptr++; + end = strchr(ptr, '"'); + if (!end) + return FALSE; + *end = '\0'; + } else { + end = strchr(ptr, ' '); + if (!end) + end = strchr(ptr, '{'); + if (!end) + return FALSE; + *end = '\0'; + } + + if (ptr[0] == '\0' || strlen(ptr) + 1 > size) + return FALSE; + + snprintf(interface, size, "%s", ptr); + + return TRUE; +} + +char * +nm_dhcp_dhclient_create_config(const char * interface, + int addr_family, + GBytes * client_id, + const char * anycast_addr, + const char * hostname, + guint32 timeout, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags, + const char * mud_url, + const char *const * reject_servers, + const char * orig_path, + const char * orig_contents, + GBytes ** out_new_client_id) +{ + nm_auto_free_gstring GString *new_contents = NULL; + gs_unref_ptrarray GPtrArray *fqdn_opts = NULL; + gs_unref_ptrarray GPtrArray *reqs = NULL; + gboolean reset_reqlist = FALSE; + int i; + + g_return_val_if_fail(!anycast_addr || nm_utils_hwaddr_valid(anycast_addr, ETH_ALEN), NULL); + g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), NULL); + g_return_val_if_fail(!reject_servers || addr_family == AF_INET, NULL); + nm_assert(!out_new_client_id || !*out_new_client_id); + + new_contents = g_string_new(_("# Created by NetworkManager\n")); + reqs = g_ptr_array_new_full(5, g_free); + + if (orig_contents) { + gs_free const char **lines = NULL; + gsize line_i; + nm_auto_free_gstring GString *blocks_stack = NULL; + guint blocks_skip = 0; + gboolean in_alsoreq = FALSE; + gboolean in_req = FALSE; + char intf[IFNAMSIZ]; + + blocks_stack = g_string_new(NULL); + g_string_append_printf(new_contents, _("# Merged from %s\n\n"), orig_path); + intf[0] = '\0'; + + lines = nm_utils_strsplit_set(orig_contents, "\n\r"); + for (line_i = 0; lines && lines[line_i]; line_i++) { + const char *line = nm_str_skip_leading_spaces(lines[line_i]); + const char *p; + + if (line[0] == '\0') + continue; + + g_strchomp((char *) line); + + p = line; + if (in_req) { + /* pass */ + } else if (strchr(p, '{')) { + if (NM_STR_HAS_PREFIX(p, "lease") || NM_STR_HAS_PREFIX(p, "alias") + || NM_STR_HAS_PREFIX(p, "interface") || NM_STR_HAS_PREFIX(p, "pseudo")) { + /* skip over these blocks, except 'interface' when it + * matches the current interface */ + blocks_skip++; + g_string_append_c(blocks_stack, 'b'); + if (!intf[0] && NM_STR_HAS_PREFIX(p, "interface")) { + if (read_interface(p, intf, sizeof(intf))) + continue; + } + } else { + /* allow other blocks (conditionals) */ + if (!strchr(p, '}')) /* '} else {' */ + g_string_append_c(blocks_stack, 'c'); + } + } else if (strchr(p, '}')) { + if (blocks_stack->len > 0) { + if (blocks_stack->str[blocks_stack->len - 1] == 'b') { + g_string_truncate(blocks_stack, blocks_stack->len - 1); + nm_assert(blocks_skip > 0); + blocks_skip--; + intf[0] = '\0'; + continue; + } + g_string_truncate(blocks_stack, blocks_stack->len - 1); + } + } + + if (blocks_skip > 0 && !intf[0]) + continue; + + if (intf[0] && !nm_streq(intf, interface)) + continue; + + /* Some timing parameters in dhclient should not be imported (timeout, retry). + * The retry parameter will be simply not used as we will exit on first failure. + * The timeout one instead may affect NetworkManager behavior: if the timeout + * elapses before dhcp-timeout dhclient will report failure and cause NM to + * fail the dhcp process before dhcp-timeout. So, always skip importing timeout + * as we will need to add one greater than dhcp-timeout. + */ + if (!strncmp(p, TIMEOUT_TAG, strlen(TIMEOUT_TAG)) + || !strncmp(p, RETRY_TAG, strlen(RETRY_TAG))) + continue; + + if (!strncmp(p, CLIENTID_TAG, strlen(CLIENTID_TAG))) { + /* Override config file "dhcp-client-id" and use one from the connection */ + if (client_id) + continue; + + /* Otherwise, capture and return the existing client id */ + if (out_new_client_id) + nm_clear_pointer(out_new_client_id, g_bytes_unref); + NM_SET_OUT(out_new_client_id, read_client_id(p)); + } + + /* Override config file hostname and use one from the connection */ + if (hostname) { + if (strncmp(p, HOSTNAME4_TAG, strlen(HOSTNAME4_TAG)) == 0) + continue; + if (strncmp(p, FQDN_TAG, strlen(FQDN_TAG)) == 0) + continue; + } + + /* To let user's FQDN options (except "fqdn.fqdn") override the + * default ones set by NM, add them later + */ + if (!strncmp(p, FQDN_TAG_PREFIX, NM_STRLEN(FQDN_TAG_PREFIX))) { + if (!fqdn_opts) + fqdn_opts = g_ptr_array_new_full(5, g_free); + g_ptr_array_add(fqdn_opts, g_strdup(p + NM_STRLEN(FQDN_TAG_PREFIX))); + continue; + } + + /* Ignore 'script' since we pass our own */ + if (g_str_has_prefix(p, "script ")) + continue; + + /* Check for "request" */ + if (!strncmp(p, REQ_TAG, strlen(REQ_TAG))) { + in_req = TRUE; + p += strlen(REQ_TAG); + g_ptr_array_set_size(reqs, 0); + reset_reqlist = TRUE; + } + + /* Save all request options for later use */ + if (in_req) { + in_req = !grab_request_options(reqs, p); + continue; + } + + /* Check for "also require" */ + if (!strncmp(p, ALSOREQ_TAG, strlen(ALSOREQ_TAG))) { + in_alsoreq = TRUE; + p += strlen(ALSOREQ_TAG); + } + + if (in_alsoreq) { + in_alsoreq = !grab_request_options(reqs, p); + continue; + } + + /* Existing configuration line is OK, add it to new configuration */ + g_string_append(new_contents, line); + g_string_append_c(new_contents, '\n'); + } + } else + g_string_append_c(new_contents, '\n'); + + /* ensure dhclient timeout is greater than dhcp-timeout: as dhclient timeout default value is + * 60 seconds, we need this only if dhcp-timeout is greater than 60. + */ + if (timeout >= 60) { + timeout = timeout < G_MAXINT32 ? timeout + 1 : G_MAXINT32; + g_string_append_printf(new_contents, "timeout %u;\n", timeout); + } + + add_mud_url_config(new_contents, mud_url, addr_family); + + if (reject_servers && reject_servers[0]) { + g_string_append(new_contents, "reject "); + for (i = 0; reject_servers[i]; i++) { + if (i != 0) + g_string_append(new_contents, ", "); + g_string_append(new_contents, reject_servers[i]); + } + g_string_append(new_contents, ";\n"); + } + + if (addr_family == AF_INET) { + add_ip4_config(new_contents, client_id, hostname, use_fqdn, hostname_flags); + add_request(reqs, "rfc3442-classless-static-routes"); + add_request(reqs, "ms-classless-static-routes"); + add_request(reqs, "static-routes"); + add_request(reqs, "wpad"); + add_request(reqs, "ntp-servers"); + add_request(reqs, "root-path"); + } else { + add_hostname6(new_contents, hostname, hostname_flags); + add_request(reqs, "dhcp6.name-servers"); + add_request(reqs, "dhcp6.domain-search"); + + /* FIXME: internal client does not support requesting client-id option. Does this even work? */ + add_request(reqs, "dhcp6.client-id"); + } + + if (reset_reqlist) + g_string_append(new_contents, "request; # override dhclient defaults\n"); + /* And add it to the dhclient configuration */ + for (i = 0; i < reqs->len; i++) + g_string_append_printf(new_contents, "also request %s;\n", (char *) reqs->pdata[i]); + + if (fqdn_opts) { + for (i = 0; i < fqdn_opts->len; i++) { + const char *t = fqdn_opts->pdata[i]; + + if (i == 0) + g_string_append_printf(new_contents, "\n# FQDN options from %s\n", orig_path); + g_string_append_printf(new_contents, FQDN_TAG_PREFIX "%s\n", t); + } + } + + g_string_append_c(new_contents, '\n'); + + if (anycast_addr) { + g_string_append_printf(new_contents, + "interface \"%s\" {\n" + " initial-interval 1; \n" + " anycast-mac ethernet %s;\n" + "}\n", + interface, + anycast_addr); + } + + return g_string_free(g_steal_pointer(&new_contents), FALSE); +} + +/* Roughly follow what dhclient's quotify_buf() and pretty_escape() functions do */ +char * +nm_dhcp_dhclient_escape_duid(GBytes *duid) +{ + char * escaped; + const guint8 *s, *s0; + gsize len; + char * d; + + g_return_val_if_fail(duid, NULL); + + s0 = g_bytes_get_data(duid, &len); + s = s0; + + d = escaped = g_malloc((len * 4) + 1); + while (s < (s0 + len)) { + if (!g_ascii_isprint(*s)) { + *d++ = '\\'; + *d++ = '0' + ((*s >> 6) & 0x7); + *d++ = '0' + ((*s >> 3) & 0x7); + *d++ = '0' + (*s++ & 0x7); + } else if (*s == '"' || *s == '\'' || *s == '$' || *s == '`' || *s == '\\' || *s == '|' + || *s == '&') { + *d++ = '\\'; + *d++ = *s++; + } else + *d++ = *s++; + } + *d++ = '\0'; + return escaped; +} + +static gboolean +isoctal(const guint8 *p) +{ + return (p[0] >= '0' && p[0] <= '3' && p[1] >= '0' && p[1] <= '7' && p[2] >= '0' && p[2] <= '7'); +} + +GBytes * +nm_dhcp_dhclient_unescape_duid(const char *duid) +{ + GByteArray * unescaped; + const guint8 *p = (const guint8 *) duid; + guint i, len; + guint8 octal; + + /* FIXME: it's wrong to have an "unescape-duid" function. dhclient + * defines a file format with escaping. So we need a general unescape + * function that can handle dhclient syntax. */ + + len = strlen(duid); + unescaped = g_byte_array_sized_new(len); + for (i = 0; i < len; i++) { + if (p[i] == '\\') { + i++; + if (isdigit(p[i])) { + /* Octal escape sequence */ + if (i + 2 >= len || !isoctal(p + i)) + goto error; + octal = ((p[i] - '0') << 6) + ((p[i + 1] - '0') << 3) + (p[i + 2] - '0'); + g_byte_array_append(unescaped, &octal, 1); + i += 2; + } else { + /* FIXME: don't warn on untrusted data. Either signal an error, or accept + * it silently. */ + + /* One of ", ', $, `, \, |, or & */ + g_warn_if_fail(p[i] == '"' || p[i] == '\'' || p[i] == '$' || p[i] == '`' + || p[i] == '\\' || p[i] == '|' || p[i] == '&'); + g_byte_array_append(unescaped, &p[i], 1); + } + } else + g_byte_array_append(unescaped, &p[i], 1); + } + + return g_byte_array_free_to_bytes(unescaped); + +error: + g_byte_array_free(unescaped, TRUE); + return NULL; +} + +#define DUID_PREFIX "default-duid \"" + +/* Beware: @error may be unset even if the function returns %NULL. */ +GBytes * +nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error) +{ + gs_free char * contents = NULL; + gs_free const char **contents_v = NULL; + gsize i; + + if (!g_file_test(leasefile, G_FILE_TEST_EXISTS)) + return NULL; + + if (!g_file_get_contents(leasefile, &contents, NULL, error)) + return NULL; + + contents_v = nm_utils_strsplit_set(contents, "\n\r"); + for (i = 0; contents_v && contents_v[i]; i++) { + const char *p = nm_str_skip_leading_spaces(contents_v[i]); + GBytes * duid; + + if (!NM_STR_HAS_PREFIX(p, DUID_PREFIX)) + continue; + + p += NM_STRLEN(DUID_PREFIX); + + g_strchomp((char *) p); + + if (!NM_STR_HAS_SUFFIX(p, "\";")) + continue; + + ((char *) p)[strlen(p) - 2] = '\0'; + + duid = nm_dhcp_dhclient_unescape_duid(p); + if (duid) + return duid; + } + + return NULL; +} + +gboolean +nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error) +{ + gs_free char * escaped_duid = NULL; + gs_free const char **lines = NULL; + nm_auto_free_gstring GString *s = NULL; + const char *const * iter; + gsize len = 0; + + g_return_val_if_fail(leasefile != NULL, FALSE); + if (!duid) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing duid"); + g_return_val_if_reached(FALSE); + } + + escaped_duid = nm_dhcp_dhclient_escape_duid(duid); + nm_assert(escaped_duid); + + if (g_file_test(leasefile, G_FILE_TEST_EXISTS)) { + gs_free char *contents = NULL; + + if (!g_file_get_contents(leasefile, &contents, &len, error)) { + g_prefix_error(error, "failed to read lease file %s: ", leasefile); + return FALSE; + } + + lines = nm_utils_strsplit_set_with_empty(contents, "\n\r"); + } + + s = g_string_sized_new(len + 50); + g_string_append_printf(s, DUID_PREFIX "%s\";\n", escaped_duid); + + /* Preserve existing leasefile contents */ + if (lines) { + for (iter = lines; *iter; iter++) { + const char *str = *iter; + const char *l; + + /* If we find an uncommented DUID in the file, check if + * equal to the one we are going to write: if so, no need + * to update the lease file, otherwise skip the old DUID. + */ + l = nm_str_skip_leading_spaces(str); + if (g_str_has_prefix(l, DUID_PREFIX)) { + gs_strfreev char **split = NULL; + + split = g_strsplit(l, "\"", -1); + if (split[0] && nm_streq0(split[1], escaped_duid)) + return TRUE; + + continue; + } + + if (str) + g_string_append(s, str); + /* avoid to add an extra '\n' at the end of file */ + if ((iter[1]) != NULL) + g_string_append_c(s, '\n'); + } + } + + if (!g_file_set_contents(leasefile, s->str, -1, error)) { + g_prefix_error(error, "failed to set DUID in lease file %s: ", leasefile); + return FALSE; + } + + return TRUE; +} diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.h b/src/core/dhcp/nm-dhcp-dhclient-utils.h new file mode 100644 index 00000000..ed7c1c7c --- /dev/null +++ b/src/core/dhcp/nm-dhcp-dhclient-utils.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2010 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ +#define __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ + +#include "nm-setting-ip4-config.h" +#include "nm-setting-ip6-config.h" + +char *nm_dhcp_dhclient_create_config(const char * interface, + int addr_family, + GBytes * client_id, + const char * anycast_addr, + const char * hostname, + guint32 timeout, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags, + const char * mud_url, + const char *const * reject_servers, + const char * orig_path, + const char * orig_contents, + GBytes ** out_new_client_id); + +char *nm_dhcp_dhclient_escape_duid(GBytes *duid); + +GBytes *nm_dhcp_dhclient_unescape_duid(const char *duid); + +GBytes *nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error); + +gboolean nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error); + +#endif /* __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ */ diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c new file mode 100644 index 00000000..c42a0ba5 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-dhclient.c @@ -0,0 +1,733 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2012 Red Hat, Inc. + */ + +#include <config.h> +#define __CONFIG_H__ + +#define _XOPEN_SOURCE +#include <time.h> +#undef _XOPEN_SOURCE + +#include "src/core/nm-default-daemon.h" + +#if WITH_DHCLIENT + + #include <stdlib.h> + #include <unistd.h> + #include <stdio.h> + #include <netinet/in.h> + #include <arpa/inet.h> + #include <ctype.h> + + #include "nm-glib-aux/nm-dedup-multi.h" + + #include "nm-utils.h" + #include "nm-dhcp-dhclient-utils.h" + #include "nm-dhcp-manager.h" + #include "NetworkManagerUtils.h" + #include "nm-dhcp-listener.h" + #include "nm-dhcp-client-logging.h" + +/*****************************************************************************/ + +static const char * +_addr_family_to_path_part(int addr_family) +{ + nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6)); + return (addr_family == AF_INET6) ? "6" : ""; +} + +/*****************************************************************************/ + + #define NM_TYPE_DHCP_DHCLIENT (nm_dhcp_dhclient_get_type()) + #define NM_DHCP_DHCLIENT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclient)) + #define NM_DHCP_DHCLIENT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclientClass)) + #define NM_IS_DHCP_DHCLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCLIENT)) + #define NM_IS_DHCP_DHCLIENT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_DHCLIENT)) + #define NM_DHCP_DHCLIENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_DHCLIENT, NMDhcpDhclientClass)) + +typedef struct _NMDhcpDhclient NMDhcpDhclient; +typedef struct _NMDhcpDhclientClass NMDhcpDhclientClass; + +static GType nm_dhcp_dhclient_get_type(void); + +/*****************************************************************************/ + +typedef struct { + char * conf_file; + const char * def_leasefile; + char * lease_file; + char * pid_file; + NMDhcpListener *dhcp_listener; +} NMDhcpDhclientPrivate; + +struct _NMDhcpDhclient { + NMDhcpClient parent; + NMDhcpDhclientPrivate _priv; +}; + +struct _NMDhcpDhclientClass { + NMDhcpClientClass parent; +}; + +G_DEFINE_TYPE(NMDhcpDhclient, nm_dhcp_dhclient, NM_TYPE_DHCP_CLIENT) + + #define NM_DHCP_DHCLIENT_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDhcpDhclient, NM_IS_DHCP_DHCLIENT) + +/*****************************************************************************/ + +static const char * +nm_dhcp_dhclient_get_path(void) +{ + return nm_utils_find_helper("dhclient", DHCLIENT_PATH, NULL); +} + +/** + * get_dhclient_leasefile(): + * @addr_family: AF_INET or AF_INET6 + * @iface: the interface name of the device on which DHCP will be done + * @uuid: the connection UUID to which the returned lease should belong + * @out_preferred_path: on return, the "most preferred" leasefile path + * + * Returns the path of an existing leasefile (if any) for this interface and + * connection UUID. Also returns the "most preferred" leasefile path, which + * may be different than any found leasefile. + * + * Returns: an existing leasefile, or %NULL if no matching leasefile could be found + */ +static char * +get_dhclient_leasefile(int addr_family, + const char *iface, + const char *uuid, + char ** out_preferred_path) +{ + gs_free char *path = NULL; + + if (nm_dhcp_utils_get_leasefile_path(addr_family, "dhclient", iface, uuid, &path)) { + NM_SET_OUT(out_preferred_path, g_strdup(path)); + return g_steal_pointer(&path); + } + + NM_SET_OUT(out_preferred_path, g_steal_pointer(&path)); + + /* If the leasefile we're looking for doesn't exist yet in the new location + * (eg, /var/lib/NetworkManager) then look in old locations to maintain + * backwards compatibility with external tools (like dracut) that put + * leasefiles there. + */ + + /* Old Debian, SUSE, and Mandriva location */ + g_free(path); + path = g_strdup_printf(LOCALSTATEDIR "/lib/dhcp/dhclient%s-%s-%s.lease", + _addr_family_to_path_part(addr_family), + uuid, + iface); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return g_steal_pointer(&path); + + /* Old Red Hat and Fedora location */ + g_free(path); + path = g_strdup_printf(LOCALSTATEDIR "/lib/dhclient/dhclient%s-%s-%s.lease", + _addr_family_to_path_part(addr_family), + uuid, + iface); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return g_steal_pointer(&path); + + /* Fail */ + return NULL; +} + +static gboolean +merge_dhclient_config(NMDhcpDhclient * self, + int addr_family, + const char * iface, + const char * conf_file, + GBytes * client_id, + const char * anycast_addr, + const char * hostname, + guint32 timeout, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags, + const char * mud_url, + const char *const * reject_servers, + const char * orig_path, + GBytes ** out_new_client_id, + GError ** error) +{ + gs_free char *orig = NULL; + gs_free char *new = NULL; + + g_return_val_if_fail(iface, FALSE); + g_return_val_if_fail(conf_file, FALSE); + + if (orig_path && g_file_test(orig_path, G_FILE_TEST_EXISTS)) { + GError *read_error = NULL; + + if (!g_file_get_contents(orig_path, &orig, NULL, &read_error)) { + _LOGW("error reading dhclient configuration %s: %s", orig_path, read_error->message); + g_error_free(read_error); + } + } + + new = nm_dhcp_dhclient_create_config(iface, + addr_family, + client_id, + anycast_addr, + hostname, + timeout, + use_fqdn, + hostname_flags, + mud_url, + reject_servers, + orig_path, + orig, + out_new_client_id); + nm_assert(new); + + return g_file_set_contents(conf_file, new, -1, error); +} + +static char * +find_existing_config(NMDhcpDhclient *self, int addr_family, const char *iface, const char *uuid) +{ + char *path; + + /* NetworkManager-overridden configuration can be used to ship DHCP config + * with NetworkManager itself. It can be uuid-specific, device-specific + * or generic. + */ + if (uuid) { + path = g_strdup_printf(NMCONFDIR "/dhclient%s-%s.conf", + _addr_family_to_path_part(addr_family), + uuid); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + } + + path = g_strdup_printf(NMCONFDIR "/dhclient%s-%s.conf", + _addr_family_to_path_part(addr_family), + iface); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + path = g_strdup_printf(NMCONFDIR "/dhclient%s.conf", _addr_family_to_path_part(addr_family)); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + /* Distribution's dhclient configuration is used so that we can use + * configuration shipped with dhclient (if any). + * + * This replaces conditional compilation based on distribution name. Fedora + * and Debian store the configs in /etc/dhcp while upstream defaults to /etc + * which is then used by many other distributions. Some distributions + * (including Fedora) don't even provide a default configuration file. + */ + path = g_strdup_printf(SYSCONFDIR "/dhcp/dhclient%s-%s.conf", + _addr_family_to_path_part(addr_family), + iface); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + path = g_strdup_printf(SYSCONFDIR "/dhclient%s-%s.conf", + _addr_family_to_path_part(addr_family), + iface); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + path = + g_strdup_printf(SYSCONFDIR "/dhcp/dhclient%s.conf", _addr_family_to_path_part(addr_family)); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + path = g_strdup_printf(SYSCONFDIR "/dhclient%s.conf", _addr_family_to_path_part(addr_family)); + _LOGD("looking for existing config %s", path); + if (g_file_test(path, G_FILE_TEST_EXISTS)) + return path; + g_free(path); + + return NULL; +} + +/* NM provides interface-specific options; thus the same dhclient config + * file cannot be used since DHCP transactions can happen in parallel. + * Since some distros don't have default per-interface dhclient config files, + * read their single config file and merge that into a custom per-interface + * config file along with the NM options. + */ +static char * +create_dhclient_config(NMDhcpDhclient * self, + int addr_family, + const char * iface, + const char * uuid, + GBytes * client_id, + const char * dhcp_anycast_addr, + const char * hostname, + guint32 timeout, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags, + const char * mud_url, + const char *const * reject_servers, + GBytes ** out_new_client_id) +{ + gs_free char *orig = NULL; + char *new = NULL; + GError *error = NULL; + + g_return_val_if_fail(iface != NULL, NULL); + + new = g_strdup_printf(NMSTATEDIR "/dhclient%s-%s.conf", + _addr_family_to_path_part(addr_family), + iface); + + _LOGD("creating composite dhclient config %s", new); + + orig = find_existing_config(self, addr_family, iface, uuid); + if (orig) + _LOGD("merging existing dhclient config %s", orig); + else + _LOGD("no existing dhclient configuration to merge"); + + if (!merge_dhclient_config(self, + addr_family, + iface, + new, + client_id, + dhcp_anycast_addr, + hostname, + timeout, + use_fqdn, + hostname_flags, + mud_url, + reject_servers, + orig, + out_new_client_id, + &error)) { + _LOGW("error creating dhclient configuration: %s", error->message); + g_clear_error(&error); + } + + return new; +} + +static gboolean +dhclient_start(NMDhcpClient *client, + const char * mode_opt, + gboolean release, + pid_t * out_pid, + int prefixes, + GError ** error) +{ + NMDhcpDhclient * self = NM_DHCP_DHCLIENT(client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *argv = NULL; + pid_t pid; + gs_free_error GError *local = NULL; + const char * iface; + const char * uuid; + const char * system_bus_address; + const char * dhclient_path; + char * binary_name; + gs_free char * cmd_str = NULL; + gs_free char * pid_file = NULL; + gs_free char * system_bus_address_env = NULL; + gs_free char * preferred_leasefile_path = NULL; + const int addr_family = nm_dhcp_client_get_addr_family(client); + + g_return_val_if_fail(!priv->pid_file, FALSE); + + NM_SET_OUT(out_pid, 0); + + dhclient_path = nm_dhcp_dhclient_get_path(); + if (!dhclient_path) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "dhclient binary not found"); + return FALSE; + } + + iface = nm_dhcp_client_get_iface(client); + uuid = nm_dhcp_client_get_uuid(client); + + pid_file = g_strdup_printf(NMRUNDIR "/dhclient%s-%s.pid", + _addr_family_to_path_part(addr_family), + iface); + + /* Kill any existing dhclient from the pidfile */ + binary_name = g_path_get_basename(dhclient_path); + nm_dhcp_client_stop_existing(pid_file, binary_name); + g_free(binary_name); + + if (release) { + /* release doesn't use the pidfile after killing an old client */ + nm_clear_g_free(&pid_file); + } + + g_free(priv->lease_file); + priv->lease_file = get_dhclient_leasefile(addr_family, iface, uuid, &preferred_leasefile_path); + nm_assert(preferred_leasefile_path); + if (!priv->lease_file) { + /* No existing leasefile, dhclient will create one at the preferred path */ + priv->lease_file = g_steal_pointer(&preferred_leasefile_path); + } else if (!nm_streq0(priv->lease_file, preferred_leasefile_path)) { + gs_unref_object GFile *src = g_file_new_for_path(priv->lease_file); + gs_unref_object GFile *dst = g_file_new_for_path(preferred_leasefile_path); + + /* Try to copy the existing leasefile to the preferred location */ + if (!g_file_copy(src, dst, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &local)) { + gs_free char *s_path = NULL; + gs_free char *d_path = NULL; + + /* Failure; just use the existing leasefile */ + _LOGW("failed to copy leasefile %s to %s: %s", + (s_path = g_file_get_path(src)), + (d_path = g_file_get_path(dst)), + local->message); + g_clear_error(&local); + } else { + /* Success; use the preferred leasefile path */ + g_free(priv->lease_file); + priv->lease_file = g_file_get_path(dst); + } + } + + /* Save the DUID to the leasefile dhclient will actually use */ + if (addr_family == AF_INET6) { + if (!nm_dhcp_dhclient_save_duid(priv->lease_file, + nm_dhcp_client_get_client_id(client), + &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "failed to save DUID to '%s': %s", + priv->lease_file, + local->message); + return FALSE; + } + } + + argv = g_ptr_array_new(); + g_ptr_array_add(argv, (gpointer) dhclient_path); + + g_ptr_array_add(argv, (gpointer) "-d"); + + /* Be quiet. dhclient logs to syslog anyway. And we duplicate the syslog + * to stderr in case of NM running with --debug. + */ + g_ptr_array_add(argv, (gpointer) "-q"); + + if (release) + g_ptr_array_add(argv, (gpointer) "-r"); + + if (addr_family == AF_INET6) { + g_ptr_array_add(argv, (gpointer) "-6"); + + if (prefixes > 0 && nm_streq0(mode_opt, "-S")) { + /* -S is incompatible with -P, only use the latter */ + mode_opt = NULL; + } + + if (mode_opt) + g_ptr_array_add(argv, (gpointer) mode_opt); + while (prefixes--) + g_ptr_array_add(argv, (gpointer) "-P"); + } + g_ptr_array_add(argv, (gpointer) "-sf"); /* Set script file */ + g_ptr_array_add(argv, (gpointer) nm_dhcp_helper_path); + + if (pid_file) { + g_ptr_array_add(argv, (gpointer) "-pf"); /* Set pid file */ + g_ptr_array_add(argv, (gpointer) pid_file); + } + + g_ptr_array_add(argv, (gpointer) "-lf"); /* Set lease file */ + g_ptr_array_add(argv, (gpointer) priv->lease_file); + + if (priv->conf_file) { + g_ptr_array_add(argv, (gpointer) "-cf"); /* Set interface config file */ + g_ptr_array_add(argv, (gpointer) priv->conf_file); + } + + /* Usually the system bus address is well-known; but if it's supposed + * to be something else, we need to push it to dhclient, since dhclient + * sanitizes the environment it gives the action scripts. + */ + system_bus_address = getenv("DBUS_SYSTEM_BUS_ADDRESS"); + if (system_bus_address) { + system_bus_address_env = g_strdup_printf("DBUS_SYSTEM_BUS_ADDRESS=%s", system_bus_address); + g_ptr_array_add(argv, (gpointer) "-e"); + g_ptr_array_add(argv, (gpointer) system_bus_address_env); + } + + g_ptr_array_add(argv, (gpointer) iface); + g_ptr_array_add(argv, NULL); + + _LOGD("running: %s", (cmd_str = g_strjoinv(" ", (char **) argv->pdata))); + + if (!g_spawn_async(NULL, + (char **) argv->pdata, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_STDOUT_TO_DEV_NULL + | G_SPAWN_STDERR_TO_DEV_NULL, + nm_utils_setpgid, + NULL, + &pid, + &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "dhclient failed to start: %s", + local->message); + return FALSE; + } + + _LOGI("dhclient started with pid %lld", (long long int) pid); + + if (!release) + nm_dhcp_client_watch_child(client, pid); + + priv->pid_file = g_steal_pointer(&pid_file); + + NM_SET_OUT(out_pid, pid); + return TRUE; +} + +static gboolean +ip4_start(NMDhcpClient *client, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error) +{ + NMDhcpDhclient * self = NM_DHCP_DHCLIENT(client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + GBytes * client_id; + gs_unref_bytes GBytes *new_client_id = NULL; + + client_id = nm_dhcp_client_get_client_id(client); + + priv->conf_file = create_dhclient_config(self, + AF_INET, + nm_dhcp_client_get_iface(client), + nm_dhcp_client_get_uuid(client), + client_id, + dhcp_anycast_addr, + nm_dhcp_client_get_hostname(client), + nm_dhcp_client_get_timeout(client), + nm_dhcp_client_get_use_fqdn(client), + nm_dhcp_client_get_hostname_flags(client), + nm_dhcp_client_get_mud_url(client), + nm_dhcp_client_get_reject_servers(client), + &new_client_id); + if (!priv->conf_file) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "error creating dhclient configuration file"); + return FALSE; + } + + if (new_client_id) { + nm_assert(!client_id); + nm_dhcp_client_set_client_id(client, new_client_id); + } + return dhclient_start(client, NULL, FALSE, NULL, 0, error); +} + +static gboolean +ip6_start(NMDhcpClient * client, + const char * dhcp_anycast_addr, + const struct in6_addr * ll_addr, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes, + GError ** error) +{ + NMDhcpDhclient * self = NM_DHCP_DHCLIENT(client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + + if (nm_dhcp_client_get_iaid_explicit(client)) + _LOGW("dhclient does not support specifying an IAID for DHCPv6, it will be ignored"); + + priv->conf_file = create_dhclient_config(self, + AF_INET6, + nm_dhcp_client_get_iface(client), + nm_dhcp_client_get_uuid(client), + NULL, + dhcp_anycast_addr, + nm_dhcp_client_get_hostname(client), + nm_dhcp_client_get_timeout(client), + TRUE, + nm_dhcp_client_get_hostname_flags(client), + nm_dhcp_client_get_mud_url(client), + NULL, + NULL); + if (!priv->conf_file) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "error creating dhclient configuration file"); + return FALSE; + } + + return dhclient_start(client, + nm_dhcp_client_get_info_only(NM_DHCP_CLIENT(self)) ? "-S" : "-N", + FALSE, + NULL, + needed_prefixes, + error); +} + +static void +stop(NMDhcpClient *client, gboolean release) +{ + NMDhcpDhclient * self = NM_DHCP_DHCLIENT(client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + int errsv; + + NM_DHCP_CLIENT_CLASS(nm_dhcp_dhclient_parent_class)->stop(client, release); + + if (priv->conf_file) + if (remove(priv->conf_file) == -1) { + errsv = errno; + _LOGD("could not remove dhcp config file \"%s\": %d (%s)", + priv->conf_file, + errsv, + nm_strerror_native(errsv)); + } + if (priv->pid_file) { + if (remove(priv->pid_file) == -1) { + errsv = errno; + _LOGD("could not remove dhcp pid file \"%s\": %s (%d)", + priv->pid_file, + nm_strerror_native(errsv), + errsv); + } + nm_clear_g_free(&priv->pid_file); + } + + if (release) { + pid_t rpid = -1; + + if (dhclient_start(client, NULL, TRUE, &rpid, 0, NULL)) { + /* Wait a few seconds for the release to happen */ + nm_dhcp_client_stop_pid(rpid, nm_dhcp_client_get_iface(client)); + } + } +} + +static GBytes * +get_duid(NMDhcpClient *client) +{ + NMDhcpDhclient * self = NM_DHCP_DHCLIENT(client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + GBytes * duid = NULL; + gs_free char * leasefile = NULL; + GError * error = NULL; + + /* Look in interface-specific leasefile first for backwards compat */ + leasefile = get_dhclient_leasefile(AF_INET6, + nm_dhcp_client_get_iface(client), + nm_dhcp_client_get_uuid(client), + NULL); + if (leasefile) { + _LOGD("looking for DUID in '%s'", leasefile); + duid = nm_dhcp_dhclient_read_duid(leasefile, &error); + if (error) { + _LOGW("failed to read leasefile '%s': %s", leasefile, error->message); + g_clear_error(&error); + } + if (duid) + return duid; + } + + /* Otherwise, read the default machine-wide DUID */ + _LOGD("looking for default DUID in '%s'", priv->def_leasefile); + duid = nm_dhcp_dhclient_read_duid(priv->def_leasefile, &error); + if (error) { + _LOGW("failed to read leasefile '%s': %s", priv->def_leasefile, error->message); + g_clear_error(&error); + } + + return duid; +} + +/*****************************************************************************/ + +static void +nm_dhcp_dhclient_init(NMDhcpDhclient *self) +{ + static const char *const FILES[] = { + SYSCONFDIR "/dhclient6.leases", /* default */ + LOCALSTATEDIR "/lib/dhcp/dhclient6.leases", + LOCALSTATEDIR "/lib/dhclient/dhclient6.leases", + }; + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self); + int i; + + priv->def_leasefile = FILES[0]; + for (i = 0; i < G_N_ELEMENTS(FILES); i++) { + if (g_file_test(FILES[i], G_FILE_TEST_EXISTS)) { + priv->def_leasefile = FILES[i]; + break; + } + } + + priv->dhcp_listener = g_object_ref(nm_dhcp_listener_get()); + g_signal_connect(priv->dhcp_listener, + NM_DHCP_LISTENER_EVENT, + G_CALLBACK(nm_dhcp_client_handle_event), + self); +} + +static void +dispose(GObject *object) +{ + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(object); + + if (priv->dhcp_listener) { + g_signal_handlers_disconnect_by_func(priv->dhcp_listener, + G_CALLBACK(nm_dhcp_client_handle_event), + NM_DHCP_DHCLIENT(object)); + g_clear_object(&priv->dhcp_listener); + } + + nm_clear_g_free(&priv->pid_file); + nm_clear_g_free(&priv->conf_file); + nm_clear_g_free(&priv->lease_file); + + G_OBJECT_CLASS(nm_dhcp_dhclient_parent_class)->dispose(object); +} + +static void +nm_dhcp_dhclient_class_init(NMDhcpDhclientClass *dhclient_class) +{ + NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhclient_class); + GObjectClass * object_class = G_OBJECT_CLASS(dhclient_class); + + object_class->dispose = dispose; + + client_class->ip4_start = ip4_start; + client_class->ip6_start = ip6_start; + client_class->stop = stop; + client_class->get_duid = get_duid; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient = { + .name = "dhclient", + .get_type = nm_dhcp_dhclient_get_type, + .get_path = nm_dhcp_dhclient_get_path, +}; + +#endif /* WITH_DHCLIENT */ diff --git a/src/core/dhcp/nm-dhcp-dhcpcanon.c b/src/core/dhcp/nm-dhcp-dhcpcanon.c new file mode 100644 index 00000000..3504a048 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-dhcpcanon.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2017 juga <juga at riseup dot net> + */ + +#include "src/core/nm-default-daemon.h" + +#if WITH_DHCPCANON + + #include <stdlib.h> + #include <unistd.h> + + #include "nm-utils.h" + #include "nm-dhcp-manager.h" + #include "NetworkManagerUtils.h" + #include "nm-dhcp-listener.h" + #include "nm-dhcp-client-logging.h" + + #define NM_TYPE_DHCP_DHCPCANON (nm_dhcp_dhcpcanon_get_type()) + #define NM_DHCP_DHCPCANON(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanon)) + #define NM_DHCP_DHCPCANON_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanonClass)) + #define NM_IS_DHCP_DHCPCANON(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCPCANON)) + #define NM_IS_DHCP_DHCPCANON_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_DHCPCANON)) + #define NM_DHCP_DHCPCANON_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_DHCPCANON, NMDhcpDhcpcanonClass)) + +typedef struct _NMDhcpDhcpcanon NMDhcpDhcpcanon; +typedef struct _NMDhcpDhcpcanonClass NMDhcpDhcpcanonClass; + +static GType nm_dhcp_dhcpcanon_get_type(void); + +/*****************************************************************************/ + +typedef struct { + char * conf_file; + const char * def_leasefile; + char * lease_file; + char * pid_file; + NMDhcpListener *dhcp_listener; +} NMDhcpDhcpcanonPrivate; + +struct _NMDhcpDhcpcanon { + NMDhcpClient parent; + NMDhcpDhcpcanonPrivate _priv; +}; + +struct _NMDhcpDhcpcanonClass { + NMDhcpClientClass parent; +}; + +G_DEFINE_TYPE(NMDhcpDhcpcanon, nm_dhcp_dhcpcanon, NM_TYPE_DHCP_CLIENT) + + #define NM_DHCP_DHCPCANON_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDhcpDhcpcanon, NM_IS_DHCP_DHCPCANON) + +/*****************************************************************************/ + +static const char * +nm_dhcp_dhcpcanon_get_path(void) +{ + return nm_utils_find_helper("dhcpcanon", DHCPCANON_PATH, NULL); +} + +static gboolean +dhcpcanon_start(NMDhcpClient *client, + const char * mode_opt, + GBytes * duid, + gboolean release, + pid_t * out_pid, + guint needed_prefixes, + GError ** error) +{ + NMDhcpDhcpcanon * self = NM_DHCP_DHCPCANON(client); + NMDhcpDhcpcanonPrivate *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(self); + gs_unref_ptrarray GPtrArray *argv = NULL; + pid_t pid; + gs_free_error GError *local = NULL; + const char * iface; + const char * system_bus_address; + const char * dhcpcanon_path; + gs_free char * binary_name = NULL; + gs_free char * pid_file = NULL; + gs_free char * system_bus_address_env = NULL; + int addr_family; + + g_return_val_if_fail(!priv->pid_file, FALSE); + + iface = nm_dhcp_client_get_iface(client); + + addr_family = nm_dhcp_client_get_addr_family(client); + + dhcpcanon_path = nm_dhcp_dhcpcanon_get_path(); + if (!dhcpcanon_path) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "dhcpcanon binary not found"); + return FALSE; + } + + _LOGD("dhcpcanon_path: %s", dhcpcanon_path); + + pid_file = g_strdup_printf(RUNSTATEDIR "/dhcpcanon%c-%s.pid", + nm_utils_addr_family_to_char(addr_family), + iface); + _LOGD("pid_file: %s", pid_file); + + /* Kill any existing dhcpcanon from the pidfile */ + binary_name = g_path_get_basename(dhcpcanon_path); + nm_dhcp_client_stop_existing(pid_file, binary_name); + + argv = g_ptr_array_new(); + g_ptr_array_add(argv, (gpointer) dhcpcanon_path); + + g_ptr_array_add(argv, (gpointer) "-sf"); /* Set script file */ + g_ptr_array_add(argv, (gpointer) nm_dhcp_helper_path); + + g_ptr_array_add(argv, (gpointer) "-pf"); /* Set pid file */ + g_ptr_array_add(argv, (gpointer) pid_file); + + if (priv->conf_file) { + g_ptr_array_add(argv, (gpointer) "-cf"); /* Set interface config file */ + g_ptr_array_add(argv, (gpointer) priv->conf_file); + } + + /* Usually the system bus address is well-known; but if it's supposed + * to be something else, we need to push it to dhcpcanon, since dhcpcanon + * sanitizes the environment it gives the action scripts. + */ + system_bus_address = getenv("DBUS_SYSTEM_BUS_ADDRESS"); + if (system_bus_address) { + system_bus_address_env = g_strdup_printf("DBUS_SYSTEM_BUS_ADDRESS=%s", system_bus_address); + g_ptr_array_add(argv, (gpointer) "-e"); + g_ptr_array_add(argv, (gpointer) system_bus_address_env); + } + + g_ptr_array_add(argv, (gpointer) iface); + g_ptr_array_add(argv, NULL); + + if (!g_spawn_async(NULL, + (char **) argv->pdata, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_STDOUT_TO_DEV_NULL + | G_SPAWN_STDERR_TO_DEV_NULL, + nm_utils_setpgid, + NULL, + &pid, + &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "dhcpcanon failed to start: %s", + local->message); + return FALSE; + } + + nm_assert(pid > 0); + _LOGI("dhcpcanon started with pid %d", pid); + nm_dhcp_client_watch_child(client, pid); + priv->pid_file = g_steal_pointer(&pid_file); + return TRUE; +} + +static gboolean +ip4_start(NMDhcpClient *client, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error) +{ + return dhcpcanon_start(client, NULL, NULL, FALSE, NULL, 0, error); +} + +static void +stop(NMDhcpClient *client, gboolean release) +{ + NMDhcpDhcpcanon * self = NM_DHCP_DHCPCANON(client); + NMDhcpDhcpcanonPrivate *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(self); + int errsv; + + NM_DHCP_CLIENT_CLASS(nm_dhcp_dhcpcanon_parent_class)->stop(client, release); + + if (priv->pid_file) { + if (remove(priv->pid_file) == -1) { + errsv = errno; + _LOGD("could not remove dhcp pid file \"%s\": %d (%s)", + priv->pid_file, + errsv, + nm_strerror_native(errsv)); + } + g_free(priv->pid_file); + priv->pid_file = NULL; + } +} + +/*****************************************************************************/ + +static void +nm_dhcp_dhcpcanon_init(NMDhcpDhcpcanon *self) +{ + NMDhcpDhcpcanonPrivate *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(self); + + priv->dhcp_listener = g_object_ref(nm_dhcp_listener_get()); + g_signal_connect(priv->dhcp_listener, + NM_DHCP_LISTENER_EVENT, + G_CALLBACK(nm_dhcp_client_handle_event), + self); +} + +static void +dispose(GObject *object) +{ + NMDhcpDhcpcanonPrivate *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(object); + + if (priv->dhcp_listener) { + g_signal_handlers_disconnect_by_func(priv->dhcp_listener, + G_CALLBACK(nm_dhcp_client_handle_event), + NM_DHCP_DHCPCANON(object)); + g_clear_object(&priv->dhcp_listener); + } + + nm_clear_g_free(&priv->pid_file); + + G_OBJECT_CLASS(nm_dhcp_dhcpcanon_parent_class)->dispose(object); +} + +static void +nm_dhcp_dhcpcanon_class_init(NMDhcpDhcpcanonClass *dhcpcanon_class) +{ + NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhcpcanon_class); + GObjectClass * object_class = G_OBJECT_CLASS(dhcpcanon_class); + + object_class->dispose = dispose; + + client_class->ip4_start = ip4_start; + client_class->stop = stop; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcanon = { + .name = "dhcpcanon", + .get_type = nm_dhcp_dhcpcanon_get_type, + .get_path = nm_dhcp_dhcpcanon_get_path, +}; + +#endif /* WITH_DHCPCANON */ diff --git a/src/core/dhcp/nm-dhcp-dhcpcd.c b/src/core/dhcp/nm-dhcp-dhcpcd.c new file mode 100644 index 00000000..cf9fe5c2 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-dhcpcd.c @@ -0,0 +1,242 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008,2020 Roy Marples <roy@marples.name> + * Copyright (C) 2010 Dan Williams <dcbw@redhat.com> + */ + +#include "src/core/nm-default-daemon.h" + +#if WITH_DHCPCD + + #include <stdlib.h> + #include <unistd.h> + #include <stdio.h> + #include <netinet/in.h> + #include <arpa/inet.h> + + #include "nm-dhcp-manager.h" + #include "nm-utils.h" + #include "NetworkManagerUtils.h" + #include "nm-dhcp-listener.h" + #include "nm-dhcp-client-logging.h" + +/*****************************************************************************/ + + #define NM_TYPE_DHCP_DHCPCD (nm_dhcp_dhcpcd_get_type()) + #define NM_DHCP_DHCPCD(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcd)) + #define NM_DHCP_DHCPCD_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcdClass)) + #define NM_IS_DHCP_DHCPCD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_DHCPCD)) + #define NM_IS_DHCP_DHCPCD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_DHCPCD)) + #define NM_DHCP_DHCPCD_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_DHCPCD, NMDhcpDhcpcdClass)) + +typedef struct _NMDhcpDhcpcd NMDhcpDhcpcd; +typedef struct _NMDhcpDhcpcdClass NMDhcpDhcpcdClass; + +static GType nm_dhcp_dhcpcd_get_type(void); + +/*****************************************************************************/ + +typedef struct { + NMDhcpListener *dhcp_listener; +} NMDhcpDhcpcdPrivate; + +struct _NMDhcpDhcpcd { + NMDhcpClient parent; + NMDhcpDhcpcdPrivate _priv; +}; + +struct _NMDhcpDhcpcdClass { + NMDhcpClientClass parent; +}; + +G_DEFINE_TYPE(NMDhcpDhcpcd, nm_dhcp_dhcpcd, NM_TYPE_DHCP_CLIENT) + + #define NM_DHCP_DHCPCD_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDhcpDhcpcd, NM_IS_DHCP_DHCPCD) + +/*****************************************************************************/ + +static const char * +nm_dhcp_dhcpcd_get_path(void) +{ + return nm_utils_find_helper("dhcpcd", DHCPCD_PATH, NULL); +} + +static gboolean +ip4_start(NMDhcpClient *client, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error) +{ + NMDhcpDhcpcd * self = NM_DHCP_DHCPCD(client); + gs_unref_ptrarray GPtrArray *argv = NULL; + pid_t pid; + GError * local; + gs_free char * cmd_str = NULL; + const char * iface; + const char * dhcpcd_path; + const char * hostname; + + pid = nm_dhcp_client_get_pid(client); + g_return_val_if_fail(pid == -1, FALSE); + + iface = nm_dhcp_client_get_iface(client); + + dhcpcd_path = nm_dhcp_dhcpcd_get_path(); + if (!dhcpcd_path) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "dhcpcd binary not found"); + return FALSE; + } + + argv = g_ptr_array_new(); + g_ptr_array_add(argv, (gpointer) dhcpcd_path); + + /* Don't configure anything, we will do that instead. + * This requires dhcpcd-9.3.3 or newer. + * Older versions only had an option not to install a default route, + * dhcpcd still added addresses and other routes so we no longer support that + * as it doesn't fit how NetworkManager wants to work. + */ + g_ptr_array_add(argv, (gpointer) "--noconfigure"); + + g_ptr_array_add(argv, (gpointer) "-B"); /* Don't background on lease (disable fork()) */ + + g_ptr_array_add(argv, (gpointer) "-K"); /* Disable built-in carrier detection */ + + g_ptr_array_add(argv, (gpointer) "-L"); /* Disable built-in IPv4LL */ + + /* --noarp. Don't request or claim the address by ARP; this also disables IPv4LL. */ + g_ptr_array_add(argv, (gpointer) "-A"); + + g_ptr_array_add(argv, (gpointer) "-c"); /* Set script file */ + g_ptr_array_add(argv, (gpointer) nm_dhcp_helper_path); + + /* IPv4-only for now. NetworkManager knows better than dhcpcd when to + * run IPv6, and dhcpcd's automatic Router Solicitations cause problems + * with devices that don't expect them. + */ + g_ptr_array_add(argv, (gpointer) "-4"); + + hostname = nm_dhcp_client_get_hostname(client); + + if (hostname) { + if (nm_dhcp_client_get_use_fqdn(client)) { + g_ptr_array_add(argv, (gpointer) "-h"); + g_ptr_array_add(argv, (gpointer) hostname); + g_ptr_array_add(argv, (gpointer) "-F"); + g_ptr_array_add(argv, (gpointer) "both"); + } else { + g_ptr_array_add(argv, (gpointer) "-h"); + g_ptr_array_add(argv, (gpointer) hostname); + } + } + + g_ptr_array_add(argv, (gpointer) iface); + g_ptr_array_add(argv, NULL); + + _LOGD("running: %s", (cmd_str = g_strjoinv(" ", (char **) argv->pdata))); + + if (!g_spawn_async(NULL, + (char **) argv->pdata, + NULL, + G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL + | G_SPAWN_DO_NOT_REAP_CHILD, + nm_utils_setpgid, + NULL, + &pid, + &local)) { + nm_utils_error_set(error, + NM_UTILS_ERROR_UNKNOWN, + "dhcpcd failed to start: %s", + local->message); + g_error_free(local); + return FALSE; + } + + nm_assert(pid > 0); + _LOGI("dhcpcd started with pid %d", pid); + nm_dhcp_client_watch_child(client, pid); + return TRUE; +} + +static void +stop(NMDhcpClient *client, gboolean release) +{ + NMDhcpDhcpcd *self = NM_DHCP_DHCPCD(client); + pid_t pid; + int sig, errsv; + + pid = nm_dhcp_client_get_pid(client); + sig = release ? SIGALRM : SIGTERM; + _LOGD("sending %s to dhcpcd pid %d", sig == SIGALRM ? "SIGALRM" : "SIGTERM", pid); + + /* dhcpcd-9.x features privilege separation. + * It's not our job to track all these processes so we rely on dhcpcd + * to always cleanup after itself. + * Because it also re-parents itself to PID 1, the process cannot be + * reaped or waited for. + * As such, just send the correct signal. + */ + if (kill(pid, sig) == -1) { + errsv = errno; + _LOGE("failed to kill dhcpcd %d:%s", errsv, strerror(errsv)); + } + + /* When this function exits NM expects the PID to be -1. + * This means we also need to stop watching the pid. + * If we need to know the exit status then we need to refactor NM + * to allow a non -1 to mean we're waiting to exit still. + */ + nm_dhcp_client_stop_watch_child(client, pid); +} + +/*****************************************************************************/ + +static void +nm_dhcp_dhcpcd_init(NMDhcpDhcpcd *self) +{ + NMDhcpDhcpcdPrivate *priv = NM_DHCP_DHCPCD_GET_PRIVATE(self); + + priv->dhcp_listener = g_object_ref(nm_dhcp_listener_get()); + g_signal_connect(priv->dhcp_listener, + NM_DHCP_LISTENER_EVENT, + G_CALLBACK(nm_dhcp_client_handle_event), + self); +} + +static void +dispose(GObject *object) +{ + NMDhcpDhcpcdPrivate *priv = NM_DHCP_DHCPCD_GET_PRIVATE(object); + + if (priv->dhcp_listener) { + g_signal_handlers_disconnect_by_func(priv->dhcp_listener, + G_CALLBACK(nm_dhcp_client_handle_event), + NM_DHCP_DHCPCD(object)); + g_clear_object(&priv->dhcp_listener); + } + + G_OBJECT_CLASS(nm_dhcp_dhcpcd_parent_class)->dispose(object); +} + +static void +nm_dhcp_dhcpcd_class_init(NMDhcpDhcpcdClass *dhcpcd_class) +{ + NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhcpcd_class); + GObjectClass * object_class = G_OBJECT_CLASS(dhcpcd_class); + + object_class->dispose = dispose; + + client_class->ip4_start = ip4_start; + client_class->stop = stop; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcd = { + .name = "dhcpcd", + .get_type = nm_dhcp_dhcpcd_get_type, + .get_path = nm_dhcp_dhcpcd_get_path, +}; + +#endif /* WITH_DHCPCD */ diff --git a/src/core/dhcp/nm-dhcp-helper-api.h b/src/core/dhcp/nm-dhcp-helper-api.h new file mode 100644 index 00000000..ffbf3f72 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-helper-api.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2016 Red Hat, Inc. + */ + +#ifndef __NM_DHCP_HELPER_API_H__ +#define __NM_DHCP_HELPER_API_H__ + +/*****************************************************************************/ + +#define NM_DHCP_CLIENT_DBUS_IFACE "org.freedesktop.nm_dhcp_client" + +#define NM_DHCP_HELPER_SERVER_BUS_NAME "org.freedesktop.nm_dhcp_server" +#define NM_DHCP_HELPER_SERVER_OBJECT_PATH "/org/freedesktop/nm_dhcp_server" +#define NM_DHCP_HELPER_SERVER_INTERFACE_NAME "org.freedesktop.nm_dhcp_server" +#define NM_DHCP_HELPER_SERVER_METHOD_NOTIFY "Notify" + +/*****************************************************************************/ + +#endif /* __NM_DHCP_HELPER_API_H__ */ diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c new file mode 100644 index 00000000..0f98add1 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-helper.c @@ -0,0 +1,242 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2007 - 2013 Red Hat, Inc. + */ + +#include "nm-glib-aux/nm-default-glib.h" + +#include <unistd.h> +#include <stdlib.h> +#include <signal.h> + +#include "nm-utils/nm-vpn-plugin-macros.h" + +#include "nm-dhcp-helper-api.h" + +/*****************************************************************************/ + +#if NM_MORE_LOGGING + #define _NMLOG_ENABLED(level) TRUE +#else + #define _NMLOG_ENABLED(level) ((level) <= LOG_ERR) +#endif + +#define _NMLOG(always_enabled, level, ...) \ + G_STMT_START \ + { \ + if ((always_enabled) || _NMLOG_ENABLED(level)) { \ + GTimeVal _tv; \ + \ + g_get_current_time(&_tv); \ + g_print( \ + "nm-dhcp-helper[%ld] %-7s [%ld.%04ld] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) "\n", \ + (long) getpid(), \ + nm_utils_syslog_to_str(level), \ + _tv.tv_sec, \ + _tv.tv_usec / 100 _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +#define _LOGD(...) _NMLOG(TRUE, LOG_INFO, __VA_ARGS__) +#define _LOGI(...) _NMLOG(TRUE, LOG_NOTICE, __VA_ARGS__) +#define _LOGW(...) _NMLOG(TRUE, LOG_WARNING, __VA_ARGS__) +#define _LOGE(...) _NMLOG(TRUE, LOG_ERR, __VA_ARGS__) + +#define _LOGd(...) _NMLOG(FALSE, LOG_INFO, __VA_ARGS__) +#define _LOGi(...) _NMLOG(FALSE, LOG_NOTICE, __VA_ARGS__) +#define _LOGw(...) _NMLOG(FALSE, LOG_WARNING, __VA_ARGS__) + +/*****************************************************************************/ + +static GVariant * +build_signal_parameters(void) +{ + const char *const *environ_iter; + GVariantBuilder builder; + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + /* List environment and format for dbus dict */ + for (environ_iter = (const char *const *) environ; *environ_iter; environ_iter++) { + static const char *const ignore_with_prefix_list[] = + {"PATH", "SHLVL", "_", "PWD", "dhc_dbus", NULL}; + const char * item = *environ_iter; + gs_free char * name = NULL; + const char * val; + const char *const *p; + + val = strchr(item, '='); + if (!val || item == val) + continue; + + name = g_strndup(item, val - item); + val += 1; + + /* Ignore non-DHCP-related environment variables */ + for (p = ignore_with_prefix_list; *p; p++) { + if (strncmp(name, *p, strlen(*p)) == 0) + goto next; + } + + if (!g_utf8_validate(name, -1, NULL)) + continue; + + /* Value passed as a byte array rather than a string, because there are + * no character encoding guarantees with DHCP, and D-Bus requires + * strings to be UTF-8. + * + * Note that we can't use g_variant_new_bytestring() here, because that + * includes the trailing '\0'. (??!?) + */ + g_variant_builder_add(&builder, + "{sv}", + name, + g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, val, strlen(val), 1)); + +next:; + } + + return g_variant_ref_sink(g_variant_new("(a{sv})", &builder)); +} + +static void +kill_pid(void) +{ + const char *pid_str; + pid_t pid = 0; + + pid_str = getenv("pid"); + if (pid_str) + pid = strtol(pid_str, NULL, 10); + if (pid) { + _LOGI("a fatal error occurred, kill dhclient instance with pid %d", pid); + kill(pid, SIGTERM); + } +} + +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; + guint try_count; + gint64 time_start; + gint64 time_end; + + /* 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 + * before reaching backlog capacity. Ideally the server should increase + * the backlog length, but GLib doesn't provide a way to change it for a + * GDBus server. Retry for up to 5 seconds in case of failure. */ + time_start = g_get_monotonic_time(); + time_end = time_start + (5000 * 1000L); + try_count = 0; + +do_connect: + try_count++; + connection = + g_dbus_connection_new_for_address_sync("unix:path=" NMRUNDIR "/private-dhcp", + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, + NULL, + &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; + + 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); + interval = NM_CLAMP((gint64)(100L * (1L << NM_MIN(try_count, 31))), 5000, 100000); + g_usleep(NM_MIN(interval, time_remaining)); + g_clear_error(&error); + goto do_connect; + } + } + + g_dbus_error_strip_remote_error(error); + _LOGE("could not connect to NetworkManager D-Bus socket: %s", error->message); + goto out; + } + + parameters = build_signal_parameters(); + time_end = g_get_monotonic_time() + (200 * 1000L); /* retry for at most 200 milliseconds */ + try_count = 0; + +do_notify: + try_count++; + result = g_dbus_connection_call_sync(connection, + NULL, + NM_DHCP_HELPER_SERVER_OBJECT_PATH, + NM_DHCP_HELPER_SERVER_INTERFACE_NAME, + NM_DHCP_HELPER_SERVER_METHOD_NOTIFY, + parameters, + NULL, + G_DBUS_CALL_FLAGS_NONE, + 1000, + 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 + success = TRUE; + + 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); + success = FALSE; + goto out; + } + +out: + if (!success) + kill_pid(); + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/core/dhcp/nm-dhcp-listener.c b/src/core/dhcp/nm-dhcp-listener.c new file mode 100644 index 00000000..1673bbb7 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-listener.c @@ -0,0 +1,318 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 - 2016 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-dhcp-listener.h" + +#include <sys/socket.h> +#include <sys/wait.h> +#include <signal.h> +#include <stdlib.h> +#include <unistd.h> + +#include "nm-dhcp-helper-api.h" +#include "nm-dhcp-client.h" +#include "nm-dhcp-manager.h" +#include "nm-core-internal.h" +#include "nm-dbus-manager.h" +#include "NetworkManagerUtils.h" + +#define PRIV_SOCK_PATH NMRUNDIR "/private-dhcp" +#define PRIV_SOCK_TAG "dhcp" + +/*****************************************************************************/ + +const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6] = { +/* the order here matters, as we will try the plugins in this order to find + * the first available plugin. */ + +#if WITH_DHCPCANON + &_nm_dhcp_client_factory_dhcpcanon, +#endif +#if WITH_DHCLIENT + &_nm_dhcp_client_factory_dhclient, +#endif +#if WITH_DHCPCD + &_nm_dhcp_client_factory_dhcpcd, +#endif + &_nm_dhcp_client_factory_internal, + &_nm_dhcp_client_factory_systemd, + &_nm_dhcp_client_factory_nettools, +}; + +/*****************************************************************************/ + +typedef struct { + NMDBusManager *dbus_mgr; + gulong new_conn_id; + gulong dis_conn_id; + GHashTable * connections; +} NMDhcpListenerPrivate; + +struct _NMDhcpListener { + GObject parent; + NMDhcpListenerPrivate _priv; +}; + +struct _NMDhcpListenerClass { + GObjectClass parent; +}; + +enum { EVENT, LAST_SIGNAL }; +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE(NMDhcpListener, nm_dhcp_listener, G_TYPE_OBJECT) + +#define NM_DHCP_LISTENER_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDhcpListener, NM_IS_DHCP_LISTENER) + +NM_DEFINE_SINGLETON_GETTER(NMDhcpListener, nm_dhcp_listener_get, NM_TYPE_DHCP_LISTENER); + +/*****************************************************************************/ + +#define _NMLOG_PREFIX_NAME "dhcp-listener" +#define _NMLOG_DOMAIN LOGD_DHCP +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMDhcpListener *_self = (self); \ + char _prefix[64]; \ + \ + nm_log((level), \ + (_NMLOG_DOMAIN), \ + NULL, \ + NULL, \ + "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + (_self != singleton_instance \ + ? nm_sprintf_buf(_prefix, "%s[%p]", _NMLOG_PREFIX_NAME, _self) \ + : _NMLOG_PREFIX_NAME) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + G_STMT_END + +/*****************************************************************************/ + +static char * +get_option(GVariant *options, const char *key) +{ + GVariant * value; + const guchar *bytes, *s; + gsize len; + char * converted, *d; + + if (!g_variant_lookup(options, key, "@ay", &value)) + return NULL; + + bytes = g_variant_get_fixed_array(value, &len, 1); + + /* Since the DHCP options come through environment variables, they should + * already be UTF-8 safe, but just make sure. + */ + converted = g_malloc(len + 1); + for (s = bytes, d = converted; s < bytes + len; s++, d++) { + /* Convert NULLs to spaces and non-ASCII characters to ? */ + if (*s == '\0') + *d = ' '; + else if (*s > 127) + *d = '?'; + else + *d = *s; + } + *d = '\0'; + g_variant_unref(value); + + return converted; +} + +static void +_method_call_handle(NMDhcpListener *self, GVariant *parameters) +{ + gs_free char * iface = NULL; + gs_free char * pid_str = NULL; + gs_free char * reason = NULL; + gs_unref_variant GVariant *options = NULL; + int pid; + gboolean handled = FALSE; + + g_variant_get(parameters, "(@a{sv})", &options); + + iface = get_option(options, "interface"); + if (iface == NULL) { + _LOGW("dhcp-event: didn't have associated interface."); + return; + } + + pid_str = get_option(options, "pid"); + pid = _nm_utils_ascii_str_to_int64(pid_str, 10, 0, G_MAXINT32, -1); + if (pid == -1) { + _LOGW("dhcp-event: couldn't convert PID '%s' to an integer", pid_str ?: "(null)"); + return; + } + + reason = get_option(options, "reason"); + if (reason == NULL) { + _LOGW("dhcp-event: (pid %d) DHCP event didn't have a reason", pid); + return; + } + + g_signal_emit(self, signals[EVENT], 0, iface, pid, options, reason, &handled); + if (!handled) { + if (g_ascii_strcasecmp(reason, "RELEASE") == 0) { + /* Ignore event when the dhcp client gets killed and we receive its last message */ + _LOGD("dhcp-event: (pid %d) unhandled RELEASE DHCP event for interface %s", pid, iface); + } else + _LOGW("dhcp-event: (pid %d) unhandled DHCP event for interface %s", pid, iface); + } +} + +static void +_method_call(GDBusConnection * connection, + const char * sender, + const char * object_path, + const char * interface_name, + const char * method_name, + GVariant * parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + NMDhcpListener *self = NM_DHCP_LISTENER(user_data); + + if (!nm_streq(interface_name, NM_DHCP_HELPER_SERVER_INTERFACE_NAME) + || !nm_streq(method_name, NM_DHCP_HELPER_SERVER_METHOD_NOTIFY)) { + g_dbus_method_invocation_return_error(invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_METHOD, + "Unknown method %s", + method_name); + return; + } + + _method_call_handle(self, parameters); + g_dbus_method_invocation_return_value(invocation, NULL); +} + +static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO( + NM_DHCP_HELPER_SERVER_INTERFACE_NAME, + .methods = NM_DEFINE_GDBUS_METHOD_INFOS( + NM_DEFINE_GDBUS_METHOD_INFO(NM_DHCP_HELPER_SERVER_METHOD_NOTIFY, + .in_args = NM_DEFINE_GDBUS_ARG_INFOS( + NM_DEFINE_GDBUS_ARG_INFO("data", "a{sv}"), ), ), ), ); + +static guint +_dbus_connection_register_object(NMDhcpListener *self, GDBusConnection *connection, GError **error) +{ + static const GDBusInterfaceVTable interface_vtable = { + .method_call = _method_call, + }; + + return g_dbus_connection_register_object( + connection, + NM_DHCP_HELPER_SERVER_OBJECT_PATH, + interface_info, + NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable), + self, + NULL, + error); +} + +static void +new_connection_cb(NMDBusManager * mgr, + GDBusConnection * connection, + GDBusObjectManager *manager, + NMDhcpListener * self) +{ + NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self); + guint registration_id; + GError * error = NULL; + + /* it is important to register the object during the new-connection signal, + * as this avoids races with the connecting object. */ + registration_id = _dbus_connection_register_object(self, connection, &error); + if (!registration_id) { + _LOGE("failure to register %s for connection %p: %s", + NM_DHCP_HELPER_SERVER_OBJECT_PATH, + connection, + error->message); + g_error_free(error); + return; + } + + g_hash_table_insert(priv->connections, connection, GUINT_TO_POINTER(registration_id)); +} + +static void +dis_connection_cb(NMDBusManager *mgr, GDBusConnection *connection, NMDhcpListener *self) +{ + NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self); + guint id; + + id = GPOINTER_TO_UINT(g_hash_table_lookup(priv->connections, connection)); + if (id) { + g_dbus_connection_unregister_object(connection, id); + g_hash_table_remove(priv->connections, connection); + } +} + +/*****************************************************************************/ + +static void +nm_dhcp_listener_init(NMDhcpListener *self) +{ + NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self); + + /* Maps GDBusConnection :: signal-id */ + priv->connections = g_hash_table_new(nm_direct_hash, NULL); + + priv->dbus_mgr = g_object_ref(nm_dbus_manager_get()); + + /* Register the socket our DHCP clients will return lease info on */ + nm_dbus_manager_private_server_register(priv->dbus_mgr, PRIV_SOCK_PATH, PRIV_SOCK_TAG); + priv->new_conn_id = g_signal_connect(priv->dbus_mgr, + NM_DBUS_MANAGER_PRIVATE_CONNECTION_NEW "::" PRIV_SOCK_TAG, + G_CALLBACK(new_connection_cb), + self); + priv->dis_conn_id = + g_signal_connect(priv->dbus_mgr, + NM_DBUS_MANAGER_PRIVATE_CONNECTION_DISCONNECTED "::" PRIV_SOCK_TAG, + G_CALLBACK(dis_connection_cb), + self); +} + +static void +dispose(GObject *object) +{ + NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(object); + + nm_clear_g_signal_handler(priv->dbus_mgr, &priv->new_conn_id); + nm_clear_g_signal_handler(priv->dbus_mgr, &priv->dis_conn_id); + + nm_clear_pointer(&priv->connections, g_hash_table_destroy); + + g_clear_object(&priv->dbus_mgr); + + G_OBJECT_CLASS(nm_dhcp_listener_parent_class)->dispose(object); +} + +static void +nm_dhcp_listener_class_init(NMDhcpListenerClass *listener_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS(listener_class); + + object_class->dispose = dispose; + + signals[EVENT] = g_signal_new(NM_DHCP_LISTENER_EVENT, + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_LAST, + 0, + g_signal_accumulator_true_handled, + NULL, + NULL, + G_TYPE_BOOLEAN, /* listeners return TRUE if handled */ + 4, + G_TYPE_STRING, /* iface */ + G_TYPE_INT, /* pid */ + G_TYPE_VARIANT, /* options */ + G_TYPE_STRING); /* reason */ +} diff --git a/src/core/dhcp/nm-dhcp-listener.h b/src/core/dhcp/nm-dhcp-listener.h new file mode 100644 index 00000000..5f3c952c --- /dev/null +++ b/src/core/dhcp/nm-dhcp-listener.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_LISTENER_H__ +#define __NETWORKMANAGER_DHCP_LISTENER_H__ + +#define NM_TYPE_DHCP_LISTENER (nm_dhcp_listener_get_type()) +#define NM_DHCP_LISTENER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_LISTENER, NMDhcpListener)) +#define NM_IS_DHCP_LISTENER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_LISTENER)) +#define NM_DHCP_LISTENER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_LISTENER, NMDhcpListenerClass)) + +#define NM_DHCP_LISTENER_EVENT "event" + +typedef struct _NMDhcpListener NMDhcpListener; +typedef struct _NMDhcpListenerClass NMDhcpListenerClass; + +GType nm_dhcp_listener_get_type(void); + +NMDhcpListener *nm_dhcp_listener_get(void); + +#endif /* __NETWORKMANAGER_DHCP_LISTENER_H__ */ diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c new file mode 100644 index 00000000..bc114ad8 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-manager.c @@ -0,0 +1,682 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2013 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-dhcp-manager.h" + +#include <sys/socket.h> +#include <sys/wait.h> +#include <signal.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdio.h> + +#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; +} diff --git a/src/core/dhcp/nm-dhcp-manager.h b/src/core/dhcp/nm-dhcp-manager.h new file mode 100644 index 00000000..f7aba8a0 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-manager.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2010 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_MANAGER_H__ +#define __NETWORKMANAGER_DHCP_MANAGER_H__ + +#include "nm-dhcp-client.h" +#include "nm-ip4-config.h" +#include "nm-dhcp-config.h" + +#define NM_TYPE_DHCP_MANAGER (nm_dhcp_manager_get_type()) +#define NM_DHCP_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_MANAGER, NMDhcpManager)) +#define NM_DHCP_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_MANAGER, NMDhcpManagerClass)) +#define NM_IS_DHCP_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_MANAGER)) +#define NM_IS_DHCP_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_MANAGER)) +#define NM_DHCP_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_MANAGER, NMDhcpManagerClass)) + +typedef struct _NMDhcpManager NMDhcpManager; +typedef struct _NMDhcpManagerClass NMDhcpManagerClass; + +GType nm_dhcp_manager_get_type(void); + +NMDhcpManager *nm_dhcp_manager_get(void); + +const char *nm_dhcp_manager_get_config(NMDhcpManager *self); + +void nm_dhcp_manager_set_default_hostname(NMDhcpManager *manager, const char *hostname); + +NMDhcpClient *nm_dhcp_manager_start_ip4(NMDhcpManager * manager, + struct _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); + +NMDhcpClient *nm_dhcp_manager_start_ip6(NMDhcpManager * manager, + struct _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); + +/* For testing only */ +extern const char *nm_dhcp_helper_path; + +extern const NMDhcpClientFactory *const _nm_dhcp_manager_factories[6]; + +void nmtst_dhcp_manager_unget(gpointer singleton_instance); + +#endif /* __NETWORKMANAGER_DHCP_MANAGER_H__ */ diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c new file mode 100644 index 00000000..116e1bdb --- /dev/null +++ b/src/core/dhcp/nm-dhcp-nettools.c @@ -0,0 +1,1247 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2014 - 2019 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <stdlib.h> +#include <unistd.h> +#include <stdio.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <ctype.h> +#include <net/if_arp.h> + +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-std-aux/unaligned.h" +#include "nm-glib-aux/nm-str-buf.h" + +#include "nm-utils.h" +#include "nm-config.h" +#include "nm-dhcp-utils.h" +#include "nm-dhcp-options.h" +#include "nm-core-utils.h" +#include "NetworkManagerUtils.h" +#include "platform/nm-platform.h" +#include "nm-dhcp-client-logging.h" +#include "n-dhcp4/src/n-dhcp4.h" +#include "systemd/nm-sd-utils-shared.h" +#include "systemd/nm-sd-utils-dhcp.h" + +/*****************************************************************************/ + +#define NM_TYPE_DHCP_NETTOOLS (nm_dhcp_nettools_get_type()) +#define NM_DHCP_NETTOOLS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettools)) +#define NM_DHCP_NETTOOLS_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettoolsClass)) +#define NM_IS_DHCP_NETTOOLS(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_NETTOOLS)) +#define NM_IS_DHCP_NETTOOLS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_NETTOOLS)) +#define NM_DHCP_NETTOOLS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettoolsClass)) + +typedef struct _NMDhcpNettools NMDhcpNettools; +typedef struct _NMDhcpNettoolsClass NMDhcpNettoolsClass; + +/*****************************************************************************/ + +typedef struct { + NDhcp4Client * client; + NDhcp4ClientProbe *probe; + NDhcp4ClientLease *lease; + GSource * event_source; + char * lease_file; +} NMDhcpNettoolsPrivate; + +struct _NMDhcpNettools { + NMDhcpClient parent; + NMDhcpNettoolsPrivate _priv; +}; + +struct _NMDhcpNettoolsClass { + NMDhcpClientClass parent; +}; + +G_DEFINE_TYPE(NMDhcpNettools, nm_dhcp_nettools, NM_TYPE_DHCP_CLIENT) + +#define NM_DHCP_NETTOOLS_GET_PRIVATE(self) \ + _NM_GET_PRIVATE(self, NMDhcpNettools, NM_IS_DHCP_NETTOOLS) + +/*****************************************************************************/ + +static void +set_error_nettools(GError **error, int r, const char *message) +{ + /* the error code returned from n_dhcp4_* API is either a negative + * errno, or a positive internal error code. Generate different messages + * for these. */ + if (r < 0) + nm_utils_error_set_errno(error, r, "%s: %s", message); + else + nm_utils_error_set(error, r, "%s (code %d)", message, r); +} + +static inline int +_client_lease_query(NDhcp4ClientLease *lease, + uint8_t option, + const uint8_t ** datap, + size_t * n_datap) +{ + return n_dhcp4_client_lease_query(lease, option, (guint8 **) datap, n_datap); +} + +/*****************************************************************************/ + +#define DHCP_MAX_FQDN_LENGTH 255 + +/*****************************************************************************/ + +static gboolean +lease_option_consume_route(const uint8_t **datap, + size_t * n_datap, + gboolean classless, + in_addr_t * destp, + uint8_t * plenp, + in_addr_t * gatewayp) +{ + in_addr_t dest; + in_addr_t gateway; + const uint8_t *data = *datap; + size_t n_data = *n_datap; + uint8_t plen; + + if (classless) { + uint8_t bytes; + + if (!nm_dhcp_lease_data_consume(&data, &n_data, &plen, sizeof(plen))) + return FALSE; + + if (plen > 32) + return FALSE; + + bytes = plen == 0 ? 0 : ((plen - 1) / 8) + 1; + + dest = 0; + if (!nm_dhcp_lease_data_consume(&data, &n_data, &dest, bytes)) + return FALSE; + } else { + if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &dest)) + return FALSE; + + plen = _nm_utils_ip4_get_default_prefix0(dest); + if (plen == 0) + return FALSE; + } + + dest = nm_utils_ip4_address_clear_host_address(dest, plen); + + if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &gateway)) + return FALSE; + + *destp = dest; + *plenp = plen; + *gatewayp = gateway; + *datap = data; + *n_datap = n_data; + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +lease_parse_address(NDhcp4ClientLease *lease, + NMIP4Config * ip4_config, + GHashTable * options, + GError ** error) +{ + struct in_addr a_address; + in_addr_t a_netmask; + struct in_addr a_next_server; + guint32 a_plen; + guint64 nettools_lifetime; + guint32 a_lifetime; + guint32 a_timestamp; + guint64 a_expiry; + const guint8 * l_data; + gsize l_data_len; + int r; + + n_dhcp4_client_lease_get_yiaddr(lease, &a_address); + if (a_address.s_addr == INADDR_ANY) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get address from lease"); + return FALSE; + } + + n_dhcp4_client_lease_get_lifetime(lease, &nettools_lifetime); + + if (nettools_lifetime == G_MAXUINT64) { + a_timestamp = 0; + a_lifetime = NM_PLATFORM_LIFETIME_PERMANENT; + a_expiry = G_MAXUINT64; + } else { + guint64 nettools_basetime; + guint64 lifetime; + gint64 ts; + + n_dhcp4_client_lease_get_basetime(lease, &nettools_basetime); + + /* usually we shouldn't assert against external libraries like n-dhcp4. + * Here we still do it... it seems safe enough. */ + nm_assert(nettools_basetime > 0); + nm_assert(nettools_lifetime >= nettools_basetime); + nm_assert(((nettools_lifetime - nettools_basetime) % NM_UTILS_NSEC_PER_SEC) == 0); + nm_assert((nettools_lifetime - nettools_basetime) / NM_UTILS_NSEC_PER_SEC <= G_MAXUINT32); + + if (nettools_lifetime <= nettools_basetime) { + /* A lease time of 0 is allowed on some dhcp servers, so, let's accept it. */ + lifetime = 0; + } else { + lifetime = nettools_lifetime - nettools_basetime; + + /* we "ceil" the value to the next second. In practice, we don't expect any sub-second values + * from n-dhcp4 anyway, so this should have no effect. */ + lifetime += NM_UTILS_NSEC_PER_SEC - 1; + } + + ts = nm_utils_monotonic_timestamp_from_boottime(nettools_basetime, 1); + + /* the timestamp must be positive, because we only started nettools DHCP client + * after obtaining the first monotonic timestamp. Hence, the lease must have been + * received afterwards. */ + nm_assert(ts >= NM_UTILS_NSEC_PER_SEC); + + a_timestamp = ts / NM_UTILS_NSEC_PER_SEC; + a_lifetime = NM_MIN(lifetime / NM_UTILS_NSEC_PER_SEC, NM_PLATFORM_LIFETIME_PERMANENT - 1); + a_expiry = time(NULL) + + ((lifetime - (nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME) - nettools_basetime)) + / NM_UTILS_NSEC_PER_SEC); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &l_data, &l_data_len); + if (r != 0 || !nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &a_netmask)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get netmask from lease"); + return FALSE; + } + + a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask); + + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, + a_address.s_addr); + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_SUBNET_MASK, + a_netmask); + + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME, + (guint64) a_lifetime); + + if (a_expiry != G_MAXUINT64) { + nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_EXPIRY, a_expiry); + } + + n_dhcp4_client_lease_get_siaddr(lease, &a_next_server); + if (a_next_server.s_addr != INADDR_ANY) { + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, + a_next_server.s_addr); + } + + nm_ip4_config_add_address(ip4_config, + &((const NMPlatformIP4Address){ + .address = a_address.s_addr, + .peer_address = a_address.s_addr, + .plen = a_plen, + .addr_source = NM_IP_CONFIG_SOURCE_DHCP, + .timestamp = a_timestamp, + .lifetime = a_lifetime, + .preferred = a_lifetime, + })); + + return TRUE; +} + +static void +lease_parse_address_list(NDhcp4ClientLease * lease, + NMIP4Config * ip4_config, + NMDhcpOptionDhcp4Options option, + GHashTable * options, + NMStrBuf * sbuf) +{ + const guint8 *l_data; + gsize l_data_len; + int r; + + r = _client_lease_query(lease, option, &l_data, &l_data_len); + if (r != 0) + return; + + if (l_data_len == 0 || l_data_len % 4 != 0) + return; + + nm_str_buf_reset(sbuf); + + for (; l_data_len > 0; l_data_len -= 4, l_data += 4) { + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + const in_addr_t addr = unaligned_read_ne32(l_data); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(addr, addr_str)); + + switch (option) { + case NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER: + if (addr == 0 || nm_ip4_addr_is_localhost(addr)) { + /* Skip localhost addresses, like also networkd does. + * See https://github.com/systemd/systemd/issues/4524. */ + continue; + } + nm_ip4_config_add_nameserver(ip4_config, addr); + break; + case NM_DHCP_OPTION_DHCP4_NIS_SERVERS: + nm_ip4_config_add_nis_server(ip4_config, addr); + break; + case NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER: + nm_ip4_config_add_wins(ip4_config, addr); + break; + case NM_DHCP_OPTION_DHCP4_NTP_SERVER: + break; + default: + nm_assert_not_reached(); + } + } + + nm_dhcp_option_add_option(options, AF_INET, option, nm_str_buf_get_str(sbuf)); +} + +static void +lease_parse_routes(NDhcp4ClientLease *lease, + NMIP4Config * ip4_config, + GHashTable * options, + guint32 route_table, + guint32 route_metric, + NMStrBuf * sbuf) +{ + char dest_str[NM_UTILS_INET_ADDRSTRLEN]; + char gateway_str[NM_UTILS_INET_ADDRSTRLEN]; + in_addr_t dest; + in_addr_t gateway; + uint8_t plen; + guint32 m; + gboolean has_router_from_classless = FALSE; + gboolean has_classless = FALSE; + guint32 default_route_metric = route_metric; + const guint8 *l_data; + gsize l_data_len; + int r; + + r = _client_lease_query(lease, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + &l_data, + &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + has_classless = TRUE; + + while (lease_option_consume_route(&l_data, &l_data_len, TRUE, &dest, &plen, &gateway)) { + _nm_utils_inet4_ntop(dest, dest_str); + _nm_utils_inet4_ntop(gateway, gateway_str); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append_printf(sbuf, "%s/%d %s", dest_str, (int) plen, gateway_str); + + if (plen == 0) { + /* if there are multiple default routes, we add them with differing + * metrics. */ + m = default_route_metric; + if (default_route_metric < G_MAXUINT32) + default_route_metric++; + + has_router_from_classless = TRUE; + } else { + m = route_metric; + } + + nm_ip4_config_add_route( + ip4_config, + &((const NMPlatformIP4Route){ + .network = dest, + .plen = plen, + .gateway = gateway, + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .metric = m, + .table_coerced = nm_platform_route_table_coerce(route_table), + }), + NULL); + } + + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + nm_str_buf_get_str(sbuf)); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, &l_data, &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + while (lease_option_consume_route(&l_data, &l_data_len, FALSE, &dest, &plen, &gateway)) { + _nm_utils_inet4_ntop(dest, dest_str); + _nm_utils_inet4_ntop(gateway, gateway_str); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append_printf(sbuf, "%s/%d %s", dest_str, (int) plen, gateway_str); + + if (has_classless) { + /* RFC 3443: if the DHCP server returns both a Classless Static Routes + * option and a Static Routes option, the DHCP client MUST ignore the + * Static Routes option. */ + continue; + } + + if (plen == 0) { + /* for option 33 (static route), RFC 2132 says: + * + * The default route (0.0.0.0) is an illegal destination for a static + * route. */ + continue; + } + + nm_ip4_config_add_route( + ip4_config, + &((const NMPlatformIP4Route){ + .network = dest, + .plen = plen, + .gateway = gateway, + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .metric = route_metric, + .table_coerced = nm_platform_route_table_coerce(route_table), + }), + NULL); + } + + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, + nm_str_buf_get_str(sbuf)); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROUTER, &l_data, &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + while (nm_dhcp_lease_data_consume_in_addr(&l_data, &l_data_len, &gateway)) { + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(gateway, gateway_str)); + + if (gateway == 0) { + /* silently skip 0.0.0.0 */ + continue; + } + + if (has_router_from_classless) { + /* If the DHCP server returns both a Classless Static Routes option and a + * Router option, the DHCP client MUST ignore the Router option [RFC 3442]. + * + * Be more lenient and ignore the Router option only if Classless Static + * Routes contain a default gateway (as other DHCP backends do). + */ + continue; + } + + /* if there are multiple default routes, we add them with differing + * metrics. */ + m = default_route_metric; + if (default_route_metric < G_MAXUINT32) + default_route_metric++; + + nm_ip4_config_add_route( + ip4_config, + &((const NMPlatformIP4Route){ + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .gateway = gateway, + .table_coerced = nm_platform_route_table_coerce(route_table), + .metric = m, + }), + NULL); + } + + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_ROUTER, + nm_str_buf_get_str(sbuf)); + } +} + +static void +lease_parse_search_domains(NDhcp4ClientLease *lease, NMIP4Config *ip4_config, GHashTable *options) +{ + gs_strfreev char **domains = NULL; + const guint8 * l_data; + gsize l_data_len; + guint i; + int r; + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, &l_data, &l_data_len); + if (r != 0) + return; + + domains = nm_dhcp_lease_data_parse_search_list(l_data, l_data_len); + + if (!domains || !domains[0]) + return; + + for (i = 0; domains[i]; i++) + nm_ip4_config_add_search(ip4_config, domains[i]); + + nm_dhcp_option_take_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, + g_strjoinv(" ", domains)); +} + +static void +lease_parse_private_options(NDhcp4ClientLease *lease, GHashTable *options) +{ + int i; + + for (i = NM_DHCP_OPTION_DHCP4_PRIVATE_224; i <= NM_DHCP_OPTION_DHCP4_PRIVATE_254; i++) { + gs_free char *option_string = NULL; + const guint8 *l_data; + gsize l_data_len; + int r; + + /* We manage private options 249 (private classless static route) and 252 (wpad) in a special + * way, so skip them as we here just manage all (the other) private options as raw data */ + if (NM_IN_SET(i, + NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY)) + continue; + + r = _client_lease_query(lease, i, &l_data, &l_data_len); + if (r) + continue; + + option_string = nm_utils_bin2hexstr_full(l_data, l_data_len, ':', FALSE, NULL); + nm_dhcp_option_take_option(options, AF_INET, i, g_steal_pointer(&option_string)); + } +} + +static NMIP4Config * +lease_to_ip4_config(NMDedupMultiIndex *multi_idx, + const char * iface, + int ifindex, + NDhcp4ClientLease *lease, + guint32 route_table, + guint32 route_metric, + GHashTable ** out_options, + GError ** error) +{ + nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(0, FALSE); + gs_unref_object NMIP4Config *ip4_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + const guint8 * l_data; + gsize l_data_len; + const char * v_str; + guint16 v_u16; + gboolean v_bool; + in_addr_t v_inaddr; + struct in_addr v_inaddr_s; + int r; + + g_return_val_if_fail(lease != NULL, NULL); + + ip4_config = nm_ip4_config_new(multi_idx, ifindex); + options = nm_dhcp_option_create_options_dict(); + + if (!lease_parse_address(lease, ip4_config, options, error)) + return NULL; + + r = n_dhcp4_client_lease_get_server_identifier(lease, &v_inaddr_s); + if (r == 0) { + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_SERVER_ID, + v_inaddr_s.s_addr); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BROADCAST, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &v_inaddr)) { + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_BROADCAST, + v_inaddr); + } + + lease_parse_routes(lease, ip4_config, options, route_table, route_metric, &sbuf); + + lease_parse_address_list(lease, + ip4_config, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, + options, + &sbuf); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + gs_free const char **domains = NULL; + + nm_str_buf_reset(&sbuf); + nm_str_buf_append_len0(&sbuf, (const char *) l_data, l_data_len); + + /* Multiple domains sometimes stuffed into option 15 "Domain Name". */ + domains = nm_utils_strsplit_set(nm_str_buf_get_str(&sbuf), " "); + + nm_str_buf_reset(&sbuf); + if (domains) { + gsize i; + + for (i = 0; domains[i]; i++) { + gs_free char *s = NULL; + + s = nm_dhcp_lease_data_parse_domain_validate(domains[i]); + if (!s) + continue; + + nm_str_buf_append_required_delimiter(&sbuf, ' '); + nm_str_buf_append(&sbuf, s); + nm_ip4_config_add_domain(ip4_config, s); + } + } + + if (sbuf.len > 0) { + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, + nm_str_buf_get_str(&sbuf)); + } + } + + lease_parse_search_domains(lease, ip4_config, options); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_mtu(l_data, l_data_len, &v_u16)) { + nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, v_u16); + nm_ip4_config_set_mtu(ip4_config, v_u16, NM_IP_CONFIG_SOURCE_DHCP); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC, &l_data, &l_data_len); + v_bool = + (r == 0) && memmem(l_data, l_data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")); + nm_ip4_config_set_metered(ip4_config, v_bool); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_HOST_NAME, &l_data, &l_data_len); + if (r == 0) { + gs_free char *s = NULL; + + if (nm_dhcp_lease_data_parse_domain(l_data, l_data_len, &s)) { + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s); + } + } + + lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROOT_PATH, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + /* https://tools.ietf.org/html/rfc2132#section-3.19 + * + * The path is formatted as a character string consisting of + * characters from the NVT ASCII character set. + * + * We still accept any character set and backslash escape it! */ + if (l_data_len == 0) { + /* "Its minimum length is 1." */ + } else { + nm_dhcp_option_add_option_utf8safe_escape(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_ROOT_PATH, + l_data, + l_data_len); + } + } + + r = _client_lease_query(lease, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, + &l_data, + &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + /* https://tools.ietf.org/html/draft-ietf-wrec-wpad-01#section-4.4.1 + * + * We reject NUL characters inside the string (except trailing NULs). + * Otherwise, we allow any encoding and backslash-escape the result to + * UTF-8. */ + nm_dhcp_option_add_option_utf8safe_escape(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, + l_data, + l_data_len); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + gs_free char *to_free = NULL; + + /* https://tools.ietf.org/html/rfc2132#section-8.1 */ + + v_str = nm_utils_buf_utf8safe_escape((char *) l_data, l_data_len, 0, &to_free); + + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, v_str); + nm_ip4_config_set_nis_domain(ip4_config, v_str); + } + + lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf); + + lease_parse_address_list(lease, + ip4_config, + NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, + options, + &sbuf); + + lease_parse_private_options(lease, options); + + NM_SET_OUT(out_options, g_steal_pointer(&options)); + return g_steal_pointer(&ip4_config); +} + +/*****************************************************************************/ + +static void +lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_file) +{ + struct in_addr a_address; + nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE); + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + gs_free_error GError *error = NULL; + + nm_assert(lease); + nm_assert(lease_file); + + n_dhcp4_client_lease_get_yiaddr(lease, &a_address); + if (a_address.s_addr == INADDR_ANY) + return; + + nm_str_buf_append(&sbuf, "# This is private data. Do not parse.\n"); + nm_str_buf_append_printf(&sbuf, + "ADDRESS=%s\n", + _nm_utils_inet4_ntop(a_address.s_addr, addr_str)); + + if (!g_file_set_contents(lease_file, nm_str_buf_get_str_unsafe(&sbuf), sbuf.len, &error)) + _LOGW("error saving lease to %s: %s", lease_file, error->message); +} + +static void +bound4_handle(NMDhcpNettools *self, NDhcp4ClientLease *lease, gboolean extended) +{ + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + const char * iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self)); + gs_unref_object NMIP4Config *ip4_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + GError * error = NULL; + + _LOGT("lease available (%s)", extended ? "extended" : "new"); + + ip4_config = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)), + iface, + nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)), + lease, + nm_dhcp_client_get_route_table(NM_DHCP_CLIENT(self)), + nm_dhcp_client_get_route_metric(NM_DHCP_CLIENT(self)), + &options, + &error); + if (!ip4_config) { + _LOGW("%s", error->message); + g_clear_error(&error); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + nm_dhcp_option_add_requests_to_options(options, AF_INET); + lease_save(self, lease, priv->lease_file); + + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), + extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND, + NM_IP_CONFIG_CAST(ip4_config), + options); +} + +static void +dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event) +{ + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + struct in_addr server_id; + char addr_str[INET_ADDRSTRLEN]; + int r; + + _LOGT("client event %d", event->event); + + switch (event->event) { + case N_DHCP4_CLIENT_EVENT_OFFER: + r = n_dhcp4_client_lease_get_server_identifier(event->offer.lease, &server_id); + if (r) { + _LOGW("selecting lease failed: %d", r); + return; + } + + if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) { + _LOGD("server-id %s is in the reject-list, ignoring", + nm_utils_inet_ntop(AF_INET, &server_id, addr_str)); + return; + } + + r = n_dhcp4_client_lease_select(event->offer.lease); + if (r) { + _LOGW("selecting lease failed: %d", r); + return; + } + break; + case N_DHCP4_CLIENT_EVENT_RETRACTED: + case N_DHCP4_CLIENT_EVENT_EXPIRED: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_EXPIRE, NULL, NULL); + break; + case N_DHCP4_CLIENT_EVENT_CANCELLED: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + break; + case N_DHCP4_CLIENT_EVENT_GRANTED: + priv->lease = n_dhcp4_client_lease_ref(event->granted.lease); + bound4_handle(self, event->granted.lease, FALSE); + break; + case N_DHCP4_CLIENT_EVENT_EXTENDED: + bound4_handle(self, event->extended.lease, TRUE); + break; + case N_DHCP4_CLIENT_EVENT_DOWN: + /* ignore down events, they are purely informational */ + break; + case N_DHCP4_CLIENT_EVENT_LOG: + { + NMLogLevel nm_level; + + nm_level = nm_log_level_from_syslog(event->log.level); + if (nm_logging_enabled(nm_level, LOGD_DHCP4)) { + nm_log(nm_level, + LOGD_DHCP4, + NULL, + NULL, + "dhcp4 (%s): %s", + nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self)), + event->log.message); + } + } break; + default: + _LOGW("unhandled DHCP event %d", event->event); + break; + } +} + +static gboolean +dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMDhcpNettools * self = user_data; + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + NDhcp4ClientEvent * event; + int r; + + r = n_dhcp4_client_dispatch(priv->client); + if (r < 0) { + /* FIXME: if any operation (e.g. send()) fails during the + * dispatch, n-dhcp4 returns an error without arming timers + * or progressing state, so the only reasonable thing to do + * is to move to failed state so that the client will be + * restarted. Ideally n-dhcp4 should retry failed operations + * a predefined number of times (possibly infinite). + */ + _LOGE("error %d dispatching events", r); + nm_clear_g_source_inst(&priv->event_source); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return G_SOURCE_REMOVE; + } + + while (!n_dhcp4_client_pop_event(priv->client, &event) && event) { + dhcp4_event_handle(self, event); + } + + return G_SOURCE_CONTINUE; +} + +static gboolean +nettools_create(NMDhcpNettools *self, const char *dhcp_anycast_addr, GError **error) +{ + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + nm_auto(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL; + nm_auto(n_dhcp4_client_unrefp) NDhcp4Client * client = NULL; + GBytes * hwaddr; + GBytes * bcast_hwaddr; + const uint8_t * hwaddr_arr; + const uint8_t * bcast_hwaddr_arr; + gsize hwaddr_len; + gsize bcast_hwaddr_len; + GBytes * client_id; + gs_unref_bytes GBytes *client_id_new = NULL; + const uint8_t * client_id_arr; + size_t client_id_len; + int r, fd, arp_type, transport; + + g_return_val_if_fail(!priv->client, FALSE); + + hwaddr = nm_dhcp_client_get_hw_addr(NM_DHCP_CLIENT(self)); + if (!hwaddr || !(hwaddr_arr = g_bytes_get_data(hwaddr, &hwaddr_len)) + || (arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len)) < 0) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address"); + return FALSE; + } + + bcast_hwaddr = nm_dhcp_client_get_broadcast_hw_addr(NM_DHCP_CLIENT(self)); + bcast_hwaddr_arr = g_bytes_get_data(bcast_hwaddr, &bcast_hwaddr_len); + + switch (arp_type) { + case ARPHRD_ETHER: + transport = N_DHCP4_TRANSPORT_ETHERNET; + break; + case ARPHRD_INFINIBAND: + transport = N_DHCP4_TRANSPORT_INFINIBAND; + break; + default: + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "unsupported ARP type"); + return FALSE; + } + + /* Note that we always set a client-id. In particular for infiniband that is necessary, + * see https://tools.ietf.org/html/rfc4390#section-2.1 . */ + client_id = nm_dhcp_client_get_client_id(NM_DHCP_CLIENT(self)); + if (!client_id) { + client_id_new = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_arr, hwaddr_len); + client_id = client_id_new; + } + + if (!(client_id_arr = g_bytes_get_data(client_id, &client_id_len)) || client_id_len < 2) { + /* invalid client-ids are not expected. */ + nm_assert_not_reached(); + + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id"); + return FALSE; + } + + r = n_dhcp4_client_config_new(&config); + if (r) { + set_error_nettools(error, r, "failed to create client-config"); + return FALSE; + } + + n_dhcp4_client_config_set_ifindex(config, nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self))); + n_dhcp4_client_config_set_transport(config, transport); + n_dhcp4_client_config_set_mac(config, hwaddr_arr, hwaddr_len); + n_dhcp4_client_config_set_broadcast_mac(config, bcast_hwaddr_arr, bcast_hwaddr_len); + r = n_dhcp4_client_config_set_client_id(config, + client_id_arr, + NM_MIN(client_id_len, 1 + _NM_SD_MAX_CLIENT_ID_LEN)); + if (r) { + set_error_nettools(error, r, "failed to set client-id"); + return FALSE; + } + + r = n_dhcp4_client_new(&client, config); + if (r) { + set_error_nettools(error, r, "failed to create client"); + return FALSE; + } + + priv->client = client; + client = NULL; + + n_dhcp4_client_set_log_level(priv->client, + nm_log_level_to_syslog(nm_logging_get_level(LOGD_DHCP4))); + + n_dhcp4_client_get_fd(priv->client, &fd); + + priv->event_source = + nm_g_unix_fd_source_new(fd, G_IO_IN, G_PRIORITY_DEFAULT, dhcp4_event_cb, self, NULL); + g_source_attach(priv->event_source, NULL); + + return TRUE; +} + +static gboolean +_accept(NMDhcpClient *client, GError **error) +{ + NMDhcpNettools * self = NM_DHCP_NETTOOLS(client); + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + int r; + + g_return_val_if_fail(priv->lease, FALSE); + + _LOGT("accept"); + + r = n_dhcp4_client_lease_accept(priv->lease); + if (r) { + set_error_nettools(error, r, "failed to accept lease"); + return FALSE; + } + + priv->lease = n_dhcp4_client_lease_unref(priv->lease); + + return TRUE; +} + +static gboolean +decline(NMDhcpClient *client, const char *error_message, GError **error) +{ + NMDhcpNettools * self = NM_DHCP_NETTOOLS(client); + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + int r; + + g_return_val_if_fail(priv->lease, FALSE); + + _LOGT("dhcp4-client: decline"); + + r = n_dhcp4_client_lease_decline(priv->lease, error_message); + if (r) { + set_error_nettools(error, r, "failed to decline lease"); + return FALSE; + } + + priv->lease = n_dhcp4_client_lease_unref(priv->lease); + + return TRUE; +} + +static guint8 +fqdn_flags_to_wire(NMDhcpHostnameFlags flags) +{ + guint r = 0; + + /* RFC 4702 section 2.1 */ + if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE) + r |= (1 << 0); + if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) + r |= (1 << 2); + if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE) + r |= (1 << 3); + + return r; +} + +static gboolean +ip4_start(NMDhcpClient *client, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error) +{ + nm_auto(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL; + NMDhcpNettools * self = NM_DHCP_NETTOOLS(client); + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + gs_free char * lease_file = NULL; + struct in_addr last_addr = {0}; + const char * hostname; + const char * mud_url; + GBytes * vendor_class_identifier; + int r, i; + + g_return_val_if_fail(!priv->probe, FALSE); + + if (!nettools_create(self, dhcp_anycast_addr, error)) + return FALSE; + + r = n_dhcp4_client_probe_config_new(&config); + if (r) { + set_error_nettools(error, r, "failed to create dhcp-client-probe-config"); + return FALSE; + } + + /* + * FIXME: + * Select, or configure, a reasonable start delay, to protect poor servers being flooded. + */ + n_dhcp4_client_probe_config_set_start_delay(config, 1); + + nm_dhcp_utils_get_leasefile_path(AF_INET, + "internal", + nm_dhcp_client_get_iface(client), + nm_dhcp_client_get_uuid(client), + &lease_file); + + if (last_ip4_address) + inet_pton(AF_INET, last_ip4_address, &last_addr); + else { + /* + * TODO: we stick to the systemd-networkd lease file format. Quite easy for now to + * just use the functions in systemd code. Anyway, as in the end we just use the + * ip address from all the options found in the lease, write a function that parses + * the lease file just for the assigned address and returns it in &last_address. + * Then drop reference to systemd-networkd structures and functions. + */ + nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + + dhcp_lease_load(&lease, lease_file); + if (lease) + sd_dhcp_lease_get_address(lease, &last_addr); + } + + if (last_addr.s_addr) { + n_dhcp4_client_probe_config_set_requested_ip(config, last_addr); + n_dhcp4_client_probe_config_set_init_reboot(config, TRUE); + } + + /* Add requested options */ + for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options); i++) { + if (_nm_dhcp_option_dhcp4_options[i].include) { + nm_assert(_nm_dhcp_option_dhcp4_options[i].option_num <= 255); + n_dhcp4_client_probe_config_request_option(config, + _nm_dhcp_option_dhcp4_options[i].option_num); + } + } + + mud_url = nm_dhcp_client_get_mud_url(client); + if (mud_url) { + r = n_dhcp4_client_probe_config_append_option(config, + NM_DHCP_OPTION_DHCP4_MUD_URL, + mud_url, + strlen(mud_url)); + if (r) { + set_error_nettools(error, r, "failed to set MUD URL"); + return FALSE; + } + } + hostname = nm_dhcp_client_get_hostname(client); + if (hostname) { + if (nm_dhcp_client_get_use_fqdn(client)) { + uint8_t buffer[255]; + NMDhcpHostnameFlags flags; + size_t fqdn_len; + + flags = nm_dhcp_client_get_hostname_flags(client); + buffer[0] = fqdn_flags_to_wire(flags); + buffer[1] = 0; /* RCODE1 (deprecated) */ + buffer[2] = 0; /* RCODE2 (deprecated) */ + + if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) { + r = nm_sd_dns_name_to_wire_format(hostname, buffer + 3, sizeof(buffer) - 3, FALSE); + if (r <= 0) { + if (r < 0) + nm_utils_error_set_errno(error, r, "failed to convert DHCP FQDN: %s"); + else + nm_utils_error_set(error, r, "failed to convert DHCP FQDN"); + return FALSE; + } + fqdn_len = r; + } else { + fqdn_len = strlen(hostname); + if (fqdn_len > sizeof(buffer) - 3) { + nm_utils_error_set(error, r, "failed to set DHCP FQDN: name too long"); + return FALSE; + } + memcpy(buffer + 3, hostname, fqdn_len); + } + + r = n_dhcp4_client_probe_config_append_option(config, + NM_DHCP_OPTION_DHCP4_CLIENT_FQDN, + buffer, + 3 + fqdn_len); + if (r) { + set_error_nettools(error, r, "failed to set DHCP FQDN"); + return FALSE; + } + } else { + r = n_dhcp4_client_probe_config_append_option(config, + NM_DHCP_OPTION_DHCP4_HOST_NAME, + hostname, + strlen(hostname)); + if (r) { + set_error_nettools(error, r, "failed to set DHCP hostname"); + return FALSE; + } + } + } + + vendor_class_identifier = nm_dhcp_client_get_vendor_class_identifier(client); + if (vendor_class_identifier) { + const void *option_data; + gsize option_size; + + option_data = g_bytes_get_data(vendor_class_identifier, &option_size); + nm_assert(option_data); + nm_assert(option_size <= 255); + + r = n_dhcp4_client_probe_config_append_option(config, + NM_DHCP_OPTION_DHCP4_VENDOR_CLASS_IDENTIFIER, + option_data, + option_size); + if (r) { + set_error_nettools(error, r, "failed to set vendor class identifier"); + return FALSE; + } + } + + g_free(priv->lease_file); + priv->lease_file = g_steal_pointer(&lease_file); + + r = n_dhcp4_client_probe(priv->client, &priv->probe, config); + if (r) { + set_error_nettools(error, r, "failed to start DHCP client"); + return FALSE; + } + + _LOGT("dhcp-client4: start %p", (gpointer) priv->client); + + nm_dhcp_client_start_timeout(client); + return TRUE; +} + +static void +stop(NMDhcpClient *client, gboolean release) +{ + NMDhcpNettools * self = NM_DHCP_NETTOOLS(client); + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + + NM_DHCP_CLIENT_CLASS(nm_dhcp_nettools_parent_class)->stop(client, release); + + _LOGT("dhcp-client4: stop %p", (gpointer) priv->client); + + priv->probe = n_dhcp4_client_probe_free(priv->probe); +} + +/*****************************************************************************/ + +static void +nm_dhcp_nettools_init(NMDhcpNettools *self) +{} + +static void +dispose(GObject *object) +{ + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(object); + + nm_clear_g_free(&priv->lease_file); + nm_clear_g_source_inst(&priv->event_source); + nm_clear_pointer(&priv->lease, n_dhcp4_client_lease_unref); + nm_clear_pointer(&priv->probe, n_dhcp4_client_probe_free); + nm_clear_pointer(&priv->client, n_dhcp4_client_unref); + + G_OBJECT_CLASS(nm_dhcp_nettools_parent_class)->dispose(object); +} + +static void +nm_dhcp_nettools_class_init(NMDhcpNettoolsClass *class) +{ + NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(class); + GObjectClass * object_class = G_OBJECT_CLASS(class); + + object_class->dispose = dispose; + + client_class->ip4_start = ip4_start; + client_class->accept = _accept; + client_class->decline = decline; + client_class->stop = stop; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_nettools = { + .name = "nettools", + .get_type = nm_dhcp_nettools_get_type, + .experimental = TRUE, +}; diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c new file mode 100644 index 00000000..3537cd14 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-options.c @@ -0,0 +1,451 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2019 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-dhcp-options.h" + +#include "nm-glib-aux/nm-str-buf.h" + +/*****************************************************************************/ + +#define REQ(_num, _name, _include) \ + { \ + .name = NM_DHCP_OPTION_REQPREFIX ""_name, .option_num = _num, .include = _include, \ + } + +const NMDhcpOption _nm_dhcp_option_dhcp4_options[] = { + REQ(NM_DHCP_OPTION_DHCP4_SUBNET_MASK, "subnet_mask", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_TIME_OFFSET, "time_offset", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, "domain_name_servers", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_HOST_NAME, "host_name", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, "domain_name", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, "interface_mtu", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_BROADCAST, "broadcast_address", TRUE), + /* RFC 3442: The Classless Static Routes option code MUST appear in the parameter + * request list prior to both the Router option code and the Static + * Routes option code, if present. */ + REQ(NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, "rfc3442_classless_static_routes", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_ROUTER, "routers", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, "static_routes", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, "nis_domain", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_NIS_SERVERS, "nis_servers", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_NTP_SERVER, "ntp_servers", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_SERVER_ID, "dhcp_server_identifier", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, "domain_search", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE, "ms_classless_static_routes", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, "wpad", TRUE), + REQ(NM_DHCP_OPTION_DHCP4_ROOT_PATH, "root_path", TRUE), + + REQ(NM_DHCP_OPTION_DHCP4_TIME_SERVERS, "time_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IEN116_NAME_SERVERS, "ien116_name_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_LOG_SERVERS, "log_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_COOKIE_SERVERS, "cookie_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_LPR_SERVERS, "lpr_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IMPRESS_SERVERS, "impress_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_RESOURCE_LOCATION_SERVERS, "resource_location_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_BOOT_FILE_SIZE, "boot_size", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_MERIT_DUMP, "merit_dump", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SWAP_SERVER, "swap_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_EXTENSIONS_PATH, "extensions_path", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ENABLE_IP_FORWARDING, "ip_forwarding", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ENABLE_SRC_ROUTING, "non_local_source_routing", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_POLICY_FILTER, "policy_filter", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_INTERFACE_MDR, "max_dgram_reassembly", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_INTERFACE_TTL, "default_ip_ttl", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_INTERFACE_MTU_AGING_TIMEOUT, "path_mtu_aging_timeout", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PATH_MTU_PLATEAU_TABLE, "path_mtu_plateau_table", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ALL_SUBNETS_LOCAL, "all_subnets_local", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PERFORM_MASK_DISCOVERY, "perform_mask_discovery", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_MASK_SUPPLIER, "mask_supplier", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ROUTER_DISCOVERY, "router_discovery", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ROUTER_SOLICITATION_ADDR, "router_solicitation_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_TRAILER_ENCAPSULATION, "trailer_encapsulation", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ARP_CACHE_TIMEOUT, "arp_cache_timeout", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IEEE802_3_ENCAPSULATION, "ieee802_3_encapsulation", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_DEFAULT_TCP_TTL, "default_tcp_ttl", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_TCP_KEEPALIVE_INTERVAL, "tcp_keepalive_internal", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_TCP_KEEPALIVE_GARBAGE, "tcp_keepalive_garbage", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC, "vendor_encapsulated_options", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, "netbios_name_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NETBIOS_DD_SERVER, "netbios_dd_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_FONT_SERVERS, "font_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_X_DISPLAY_MANAGER, "x_display_manager", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME, "dhcp_lease_time", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_RENEWAL_T1_TIME, "dhcp_renewal_time", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_REBINDING_T2_TIME, "dhcp_rebinding_time", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_CLIENT_ID, "dhcp_client_identifier", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE, "tcode", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NWIP_DOMAIN, "nwip_domain", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NWIP_SUBOPTIONS, "nwip_suboptions", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NISPLUS_DOMAIN, "nisplus_domain", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NISPLUS_SERVERS, "nisplus_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_TFTP_SERVER_NAME, "tftp_server_name", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME, "bootfile_name", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_MOBILE_IP_HOME_AGENT, "mobile_ip_home_agent", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SMTP_SERVER, "smtp_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_POP_SERVER, "pop_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NNTP_SERVER, "nntp_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_WWW_SERVER, "www_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_FINGER_SERVER, "finger_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IRC_SERVER, "irc_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_STREETTALK_SERVER, "streettalk_server", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_STREETTALK_DIR_ASSIST_SERVER, + "streettalk_directory_assistance_server", + FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SLP_DIRECTORY_AGENT, "slp_directory_agent", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SLP_SERVICE_SCOPE, "slp_service_scope", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_CLIENT_FQDN, "fqdn", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_RELAY_AGENT_INFORMATION, "relay_agent_information", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NDS_SERVERS, "nds_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NDS_TREE_NAME, "nds_tree_name", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NDS_CONTEXT, "nds_context", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_BCMS_CONTROLLER_NAMES, "bcms_controller_names", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_BCMS_CONTROLLER_ADDRESS, "bcms_controller_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_CLIENT_LAST_TRANSACTION, "client_last_transaction_time", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_ASSOCIATED_IP, "associated_ip", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PXE_SYSTEM_TYPE, "pxe_system_type", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PXE_INTERFACE_ID, "pxe_interface_id", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PXE_CLIENT_ID, "pxe_client_id", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_UAP_SERVERS, "uap_servers", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC, "geoconf_civic", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS, "netinfo_server_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG, "netinfo_server_tag", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_DEFAULT_URL, "default_url", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_AUTO_CONFIG, "auto_config", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NAME_SERVICE_SEARCH, "name_service_search", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SUBNET_SELECTION, "subnet_selection", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_VIVCO, "vivco", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_VIVSO, "vivso", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PANA_AGENT, "pana_agent", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_V4_LOST, "v4_lost", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_SIP_UA_CS_DOMAINS, "sip_ua_cs_domains", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_IPV4_ADDRESS_ANDSF, "ipv4_address_andsf", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_RDNSS_SELECTION, "rndss_selection", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_TFTP_SERVER_ADDRESS, "tftp_server_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_V4_PORTPARAMS, "v4_portparams", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_V4_CAPTIVE_PORTAL, "v4_captive_portal", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_MUD_URL, "mud_url", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_LOADER_CONFIGFILE, "loader_configfile", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_LOADER_PATHPREFIX, "loader_pathprefix", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_LOADER_REBOOTTIME, "loader_reboottime", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_OPTION_6RD, "option_6rd", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_V4_ACCESS_DOMAIN, "v4_access_domain", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_224, "private_224", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_225, "private_225", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_226, "private_226", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_227, "private_227", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_228, "private_228", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_229, "private_229", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_230, "private_230", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_231, "private_231", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_232, "private_232", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_233, "private_233", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_234, "private_234", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_235, "private_235", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_236, "private_236", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_237, "private_237", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_238, "private_238", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_239, "private_239", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_240, "private_240", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_241, "private_241", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_242, "private_242", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_243, "private_243", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_244, "private_244", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_245, "private_245", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_246, "private_246", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_247, "private_247", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_248, "private_248", FALSE), + /* NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE */ + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_250, "private_250", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_251, "private_251", FALSE), + /* NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY */ + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_253, "private_253", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_PRIVATE_254, "private_254", FALSE), + + /* Internal values */ + REQ(NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, "ip_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NM_EXPIRY, "expiry", FALSE), + REQ(NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, "next_server", FALSE), +}; + +static const NMDhcpOption *const _sorted_options_4[G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options)] = { +#define A(idx) (&_nm_dhcp_option_dhcp4_options[(idx)]) + A(0), A(1), A(8), A(18), A(19), A(2), A(20), A(21), A(22), A(23), A(24), A(3), + A(25), A(26), A(4), A(27), A(17), A(28), A(29), A(30), A(31), A(32), A(33), A(34), + A(35), A(5), A(36), A(6), A(37), A(38), A(39), A(40), A(9), A(41), A(42), A(43), + A(44), A(45), A(46), A(10), A(11), A(12), A(47), A(48), A(49), A(50), A(51), A(52), + A(13), A(53), A(54), A(55), A(57), A(58), A(59), A(60), A(61), A(62), A(63), A(64), + A(65), A(66), A(67), A(68), A(69), A(70), A(71), A(72), A(73), A(74), A(75), A(76), + A(77), A(78), A(79), A(80), A(81), A(82), A(83), A(84), A(85), A(86), A(87), A(56), + A(88), A(89), A(90), A(91), A(92), A(93), A(14), A(7), A(94), A(95), A(96), A(97), + A(98), A(99), A(100), A(101), A(102), A(103), A(104), A(105), A(106), A(107), A(108), A(109), + A(110), A(111), A(112), A(113), A(114), A(115), A(116), A(117), A(118), A(119), A(120), A(121), + A(122), A(123), A(124), A(125), A(126), A(127), A(128), A(129), A(130), A(131), A(132), A(133), + A(134), A(15), A(135), A(136), A(16), A(137), A(138), A(139), A(140), A(141), +#undef A +}; + +const NMDhcpOption _nm_dhcp_option_dhcp6_options[] = { + REQ(NM_DHCP_OPTION_DHCP6_CLIENTID, "dhcp6_client_id", FALSE), + + /* Don't request server ID by default; some servers don't reply to + * Information Requests that request the Server ID. + */ + REQ(NM_DHCP_OPTION_DHCP6_SERVERID, "dhcp6_server_id", FALSE), + + REQ(NM_DHCP_OPTION_DHCP6_DNS_SERVERS, "dhcp6_name_servers", TRUE), + REQ(NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, "dhcp6_domain_search", TRUE), + REQ(NM_DHCP_OPTION_DHCP6_SNTP_SERVERS, "dhcp6_sntp_servers", TRUE), + REQ(NM_DHCP_OPTION_DHCP6_FQDN, "fqdn_fqdn", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_MUD_URL, "dhcp6_mud_url", FALSE), + + /* Internal values */ + REQ(NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS, "ip6_address", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_PREFIXLEN, "ip6_prefixlen", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_PREFERRED_LIFE, "preferred_life", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_MAX_LIFE, "max_life", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_STARTS, "starts", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_LIFE_STARTS, "life_starts", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_RENEW, "renew", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_REBIND, "rebind", FALSE), + REQ(NM_DHCP_OPTION_DHCP6_NM_IAID, "iaid", FALSE), +}; + +#undef REQ + +static const NMDhcpOption *const _sorted_options_6[G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options)] = { +#define A(idx) (&_nm_dhcp_option_dhcp6_options[(idx)]) + A(0), + A(1), + A(2), + A(3), + A(4), + A(5), + A(6), + A(7), + A(8), + A(9), + A(10), + A(11), + A(12), + A(13), + A(14), + A(15), +#undef A +}; + +/*****************************************************************************/ + +static int +_sorted_options_generate_sort(gconstpointer pa, gconstpointer pb, gpointer user_data) +{ + const NMDhcpOption *const *a = pa; + const NMDhcpOption *const *b = pb; + + NM_CMP_DIRECT((*a)->option_num, (*b)->option_num); + return nm_assert_unreachable_val(0); +} + +static char * +_sorted_options_generate(const NMDhcpOption *base, const NMDhcpOption *const *sorted, guint n) +{ + gs_free const NMDhcpOption **sort2 = NULL; + NMStrBuf sbuf = NM_STR_BUF_INIT(0, FALSE); + guint i; + + sort2 = nm_memdup(sorted, n * sizeof(sorted[0])); + + g_qsort_with_data(sort2, n, sizeof(sort2[0]), _sorted_options_generate_sort, NULL); + + for (i = 0; i < n; i++) { + if (i > 0) + nm_str_buf_append(&sbuf, ", "); + nm_str_buf_append_printf(&sbuf, "A(%d)", (int) (sort2[i] - base)); + } + + return nm_str_buf_finalize(&sbuf, NULL); +} + +_nm_unused static void +_ASSERT_sorted(int IS_IPv4, const NMDhcpOption *const *const sorted, int n) + +{ + const NMDhcpOption *const options = + IS_IPv4 ? _nm_dhcp_option_dhcp4_options : _nm_dhcp_option_dhcp6_options; + int i; + int j; + gs_free char *sorted_msg = NULL; + + for (i = 0; i < n; i++) { + const NMDhcpOption *opt = sorted[i]; + + g_assert(opt); + g_assert(opt >= options); + g_assert(opt < &options[n]); + + for (j = 0; j < i; j++) { + const NMDhcpOption *opt2 = sorted[j]; + + if (opt == opt2) { + g_error("%s:%d: the _sorted_options_%c at [%d] (opt=%u, %s) is duplicated at " + "[%d] (SORT: %s)", + __FILE__, + __LINE__, + IS_IPv4 ? '4' : '6', + i, + opt->option_num, + opt->name, + j, + (sorted_msg = _sorted_options_generate(options, sorted, n))); + } + } + + if (i > 0) { + const NMDhcpOption *opt2 = sorted[i - 1]; + + if (opt2->option_num >= opt->option_num) { + g_error("%s:%d: the _sorted_options_%c at [%d] (opt=%u, %s) should come before " + "[%d] (opt=%u, %s) (SORT: %s)", + __FILE__, + __LINE__, + IS_IPv4 ? '4' : '6', + i, + opt->option_num, + opt->name, + i - 1, + opt2->option_num, + opt2->name, + (sorted_msg = _sorted_options_generate(options, sorted, n))); + } + } + } +} + +/*****************************************************************************/ + +const NMDhcpOption * +nm_dhcp_option_find(int addr_family, guint option) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + const NMDhcpOption *const *const sorted = IS_IPv4 ? _sorted_options_4 : _sorted_options_6; + const int n = IS_IPv4 ? G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options) + : G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options); + int imin = 0; + int imax = n - 1; + int imid = (n - 1) / 2; + +#if NM_MORE_ASSERTS > 10 + nm_assert(n < G_MAXINT / 2); + if (IS_IPv4 && !NM_MORE_ASSERT_ONCE(10)) { + /* already checked */ + } else if (!IS_IPv4 && !NM_MORE_ASSERT_ONCE(10)) { + /* already checked */ + } else + _ASSERT_sorted(IS_IPv4, sorted, n); +#endif + + for (;;) { + const guint o = sorted[imid]->option_num; + + if (G_UNLIKELY(o == option)) + return sorted[imid]; + + if (o < option) + imin = imid + 1; + else + imax = imid - 1; + + if (G_UNLIKELY(imin > imax)) + break; + + imid = (imin + imax) / 2; + } + + /* Option should always be found */ + return nm_assert_unreachable_val(NULL); +} + +/*****************************************************************************/ + +void +nm_dhcp_option_take_option(GHashTable *options, int addr_family, guint option, char *value) +{ + nm_assert_addr_family(addr_family); + nm_assert(value); + nm_assert(g_utf8_validate(value, -1, NULL)); + + if (!options) { + nm_assert_not_reached(); + g_free(value); + return; + } + + g_hash_table_insert(options, + (gpointer) nm_dhcp_option_request_string(addr_family, option), + value); +} + +void +nm_dhcp_option_add_option(GHashTable *options, int addr_family, guint option, const char *value) +{ + nm_dhcp_option_take_option(options, addr_family, option, g_strdup(value)); +} + +void +nm_dhcp_option_add_option_utf8safe_escape(GHashTable * options, + int addr_family, + guint option, + const guint8 *data, + gsize n_data) +{ + gs_free char *to_free = NULL; + const char * escaped; + + escaped = nm_utils_buf_utf8safe_escape((char *) data, n_data, 0, &to_free); + nm_dhcp_option_add_option(options, addr_family, option, escaped ?: ""); +} + +void +nm_dhcp_option_add_option_u64(GHashTable *options, int addr_family, guint option, guint64 value) +{ + nm_dhcp_option_take_option(options, + addr_family, + option, + g_strdup_printf("%" G_GUINT64_FORMAT, value)); +} + +void +nm_dhcp_option_add_option_in_addr(GHashTable *options, + int addr_family, + guint option, + in_addr_t value) +{ + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + nm_dhcp_option_add_option(options, addr_family, option, _nm_utils_inet4_ntop(value, sbuf)); +} + +void +nm_dhcp_option_add_requests_to_options(GHashTable *options, int addr_family) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + const NMDhcpOption *const all_options = + IS_IPv4 ? _nm_dhcp_option_dhcp4_options : _nm_dhcp_option_dhcp6_options; + int n_options = IS_IPv4 ? G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options) + : G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options); + int i; + + for (i = 0; i < n_options; i++) { + if (all_options[i].include) + g_hash_table_insert(options, (gpointer) all_options[i].name, g_strdup("1")); + } +} + +GHashTable * +nm_dhcp_option_create_options_dict(void) +{ + return g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free); +} diff --git a/src/core/dhcp/nm-dhcp-options.h b/src/core/dhcp/nm-dhcp-options.h new file mode 100644 index 00000000..585f1187 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-options.h @@ -0,0 +1,226 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2019 Red Hat, Inc. + */ + +#ifndef __NM_DHCP_OPTIONS_H__ +#define __NM_DHCP_OPTIONS_H__ + +typedef enum { + NM_DHCP_OPTION_DHCP4_PAD = 0, + NM_DHCP_OPTION_DHCP4_SUBNET_MASK = 1, + NM_DHCP_OPTION_DHCP4_TIME_OFFSET = 2, + NM_DHCP_OPTION_DHCP4_ROUTER = 3, + NM_DHCP_OPTION_DHCP4_TIME_SERVERS = 4, + NM_DHCP_OPTION_DHCP4_IEN116_NAME_SERVERS = 5, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER = 6, + NM_DHCP_OPTION_DHCP4_LOG_SERVERS = 7, + NM_DHCP_OPTION_DHCP4_COOKIE_SERVERS = 8, + NM_DHCP_OPTION_DHCP4_LPR_SERVERS = 9, + NM_DHCP_OPTION_DHCP4_IMPRESS_SERVERS = 10, + NM_DHCP_OPTION_DHCP4_RESOURCE_LOCATION_SERVERS = 11, + NM_DHCP_OPTION_DHCP4_HOST_NAME = 12, + NM_DHCP_OPTION_DHCP4_BOOT_FILE_SIZE = 13, + NM_DHCP_OPTION_DHCP4_MERIT_DUMP = 14, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME = 15, + NM_DHCP_OPTION_DHCP4_SWAP_SERVER = 16, + NM_DHCP_OPTION_DHCP4_ROOT_PATH = 17, + NM_DHCP_OPTION_DHCP4_EXTENSIONS_PATH = 18, + NM_DHCP_OPTION_DHCP4_ENABLE_IP_FORWARDING = 19, + NM_DHCP_OPTION_DHCP4_ENABLE_SRC_ROUTING = 20, + NM_DHCP_OPTION_DHCP4_POLICY_FILTER = 21, + NM_DHCP_OPTION_DHCP4_INTERFACE_MDR = 22, + NM_DHCP_OPTION_DHCP4_INTERFACE_TTL = 23, + NM_DHCP_OPTION_DHCP4_INTERFACE_MTU_AGING_TIMEOUT = 24, + NM_DHCP_OPTION_DHCP4_PATH_MTU_PLATEAU_TABLE = 25, + NM_DHCP_OPTION_DHCP4_INTERFACE_MTU = 26, + NM_DHCP_OPTION_DHCP4_ALL_SUBNETS_LOCAL = 27, + NM_DHCP_OPTION_DHCP4_BROADCAST = 28, + NM_DHCP_OPTION_DHCP4_PERFORM_MASK_DISCOVERY = 29, + NM_DHCP_OPTION_DHCP4_MASK_SUPPLIER = 30, + NM_DHCP_OPTION_DHCP4_ROUTER_DISCOVERY = 31, + NM_DHCP_OPTION_DHCP4_ROUTER_SOLICITATION_ADDR = 32, + NM_DHCP_OPTION_DHCP4_STATIC_ROUTE = 33, + NM_DHCP_OPTION_DHCP4_TRAILER_ENCAPSULATION = 34, + NM_DHCP_OPTION_DHCP4_ARP_CACHE_TIMEOUT = 35, + NM_DHCP_OPTION_DHCP4_IEEE802_3_ENCAPSULATION = 36, + NM_DHCP_OPTION_DHCP4_DEFAULT_TCP_TTL = 37, + NM_DHCP_OPTION_DHCP4_TCP_KEEPALIVE_INTERVAL = 38, + NM_DHCP_OPTION_DHCP4_TCP_KEEPALIVE_GARBAGE = 39, + NM_DHCP_OPTION_DHCP4_NIS_DOMAIN = 40, + NM_DHCP_OPTION_DHCP4_NIS_SERVERS = 41, + NM_DHCP_OPTION_DHCP4_NTP_SERVER = 42, + NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC = 43, + NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER = 44, + NM_DHCP_OPTION_DHCP4_NETBIOS_DD_SERVER = 45, + NM_DHCP_OPTION_DHCP4_FONT_SERVERS = 48, + NM_DHCP_OPTION_DHCP4_X_DISPLAY_MANAGER = 49, + NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME = 51, + NM_DHCP_OPTION_DHCP4_SERVER_ID = 54, + NM_DHCP_OPTION_DHCP4_RENEWAL_T1_TIME = 58, + NM_DHCP_OPTION_DHCP4_REBINDING_T2_TIME = 59, + NM_DHCP_OPTION_DHCP4_VENDOR_CLASS_IDENTIFIER = 60, + NM_DHCP_OPTION_DHCP4_CLIENT_ID = 61, + NM_DHCP_OPTION_DHCP4_NWIP_DOMAIN = 62, + NM_DHCP_OPTION_DHCP4_NWIP_SUBOPTIONS = 63, + NM_DHCP_OPTION_DHCP4_NISPLUS_DOMAIN = 64, + NM_DHCP_OPTION_DHCP4_NISPLUS_SERVERS = 65, + NM_DHCP_OPTION_DHCP4_TFTP_SERVER_NAME = 66, + NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME = 67, + NM_DHCP_OPTION_DHCP4_MOBILE_IP_HOME_AGENT = 68, + NM_DHCP_OPTION_DHCP4_SMTP_SERVER = 69, + NM_DHCP_OPTION_DHCP4_POP_SERVER = 70, + NM_DHCP_OPTION_DHCP4_NNTP_SERVER = 71, + NM_DHCP_OPTION_DHCP4_WWW_SERVER = 72, + NM_DHCP_OPTION_DHCP4_FINGER_SERVER = 73, + NM_DHCP_OPTION_DHCP4_IRC_SERVER = 74, + NM_DHCP_OPTION_DHCP4_STREETTALK_SERVER = 75, + NM_DHCP_OPTION_DHCP4_STREETTALK_DIR_ASSIST_SERVER = 76, + NM_DHCP_OPTION_DHCP4_SLP_DIRECTORY_AGENT = 78, + NM_DHCP_OPTION_DHCP4_SLP_SERVICE_SCOPE = 79, + NM_DHCP_OPTION_DHCP4_CLIENT_FQDN = 81, + NM_DHCP_OPTION_DHCP4_RELAY_AGENT_INFORMATION = 82, + NM_DHCP_OPTION_DHCP4_NDS_SERVERS = 85, + NM_DHCP_OPTION_DHCP4_NDS_TREE_NAME = 86, + NM_DHCP_OPTION_DHCP4_NDS_CONTEXT = 87, + NM_DHCP_OPTION_DHCP4_BCMS_CONTROLLER_NAMES = 88, + NM_DHCP_OPTION_DHCP4_BCMS_CONTROLLER_ADDRESS = 89, + NM_DHCP_OPTION_DHCP4_CLIENT_LAST_TRANSACTION = 91, + NM_DHCP_OPTION_DHCP4_ASSOCIATED_IP = 92, + NM_DHCP_OPTION_DHCP4_PXE_SYSTEM_TYPE = 93, + NM_DHCP_OPTION_DHCP4_PXE_INTERFACE_ID = 94, + NM_DHCP_OPTION_DHCP4_PXE_CLIENT_ID = 97, + NM_DHCP_OPTION_DHCP4_UAP_SERVERS = 98, + NM_DHCP_OPTION_DHCP4_GEOCONF_CIVIC = 99, + NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE = 101, + NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_ADDRESS = 112, + NM_DHCP_OPTION_DHCP4_NETINFO_SERVER_TAG = 113, + NM_DHCP_OPTION_DHCP4_DEFAULT_URL = 114, + NM_DHCP_OPTION_DHCP4_AUTO_CONFIG = 116, + NM_DHCP_OPTION_DHCP4_NAME_SERVICE_SEARCH = 117, + NM_DHCP_OPTION_DHCP4_SUBNET_SELECTION = 118, + NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST = 119, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE = 121, + NM_DHCP_OPTION_DHCP4_VIVCO = 124, + NM_DHCP_OPTION_DHCP4_VIVSO = 125, + NM_DHCP_OPTION_DHCP4_PANA_AGENT = 136, + NM_DHCP_OPTION_DHCP4_V4_LOST = 137, + NM_DHCP_OPTION_DHCP4_SIP_UA_CS_DOMAINS = 141, + NM_DHCP_OPTION_DHCP4_IPV4_ADDRESS_ANDSF = 142, + NM_DHCP_OPTION_DHCP4_RDNSS_SELECTION = 146, + NM_DHCP_OPTION_DHCP4_TFTP_SERVER_ADDRESS = 150, + NM_DHCP_OPTION_DHCP4_V4_PORTPARAMS = 159, + NM_DHCP_OPTION_DHCP4_V4_CAPTIVE_PORTAL = 160, + NM_DHCP_OPTION_DHCP4_MUD_URL = 161, + NM_DHCP_OPTION_DHCP4_LOADER_CONFIGFILE = 209, + NM_DHCP_OPTION_DHCP4_LOADER_PATHPREFIX = 210, + NM_DHCP_OPTION_DHCP4_LOADER_REBOOTTIME = 211, + NM_DHCP_OPTION_DHCP4_OPTION_6RD = 212, + NM_DHCP_OPTION_DHCP4_V4_ACCESS_DOMAIN = 213, + NM_DHCP_OPTION_DHCP4_PRIVATE_224 = 224, + NM_DHCP_OPTION_DHCP4_PRIVATE_225 = 225, + NM_DHCP_OPTION_DHCP4_PRIVATE_226 = 226, + NM_DHCP_OPTION_DHCP4_PRIVATE_227 = 227, + NM_DHCP_OPTION_DHCP4_PRIVATE_228 = 228, + NM_DHCP_OPTION_DHCP4_PRIVATE_229 = 229, + NM_DHCP_OPTION_DHCP4_PRIVATE_230 = 230, + NM_DHCP_OPTION_DHCP4_PRIVATE_231 = 231, + NM_DHCP_OPTION_DHCP4_PRIVATE_232 = 232, + NM_DHCP_OPTION_DHCP4_PRIVATE_233 = 233, + NM_DHCP_OPTION_DHCP4_PRIVATE_234 = 234, + NM_DHCP_OPTION_DHCP4_PRIVATE_235 = 235, + NM_DHCP_OPTION_DHCP4_PRIVATE_236 = 236, + NM_DHCP_OPTION_DHCP4_PRIVATE_237 = 237, + NM_DHCP_OPTION_DHCP4_PRIVATE_238 = 238, + NM_DHCP_OPTION_DHCP4_PRIVATE_239 = 239, + NM_DHCP_OPTION_DHCP4_PRIVATE_240 = 240, + NM_DHCP_OPTION_DHCP4_PRIVATE_241 = 241, + NM_DHCP_OPTION_DHCP4_PRIVATE_242 = 242, + NM_DHCP_OPTION_DHCP4_PRIVATE_243 = 243, + NM_DHCP_OPTION_DHCP4_PRIVATE_244 = 244, + NM_DHCP_OPTION_DHCP4_PRIVATE_245 = 245, + NM_DHCP_OPTION_DHCP4_PRIVATE_246 = 246, + NM_DHCP_OPTION_DHCP4_PRIVATE_247 = 247, + NM_DHCP_OPTION_DHCP4_PRIVATE_248 = 248, + NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE = 249, + NM_DHCP_OPTION_DHCP4_PRIVATE_250 = 250, + NM_DHCP_OPTION_DHCP4_PRIVATE_251 = 251, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY = 252, + NM_DHCP_OPTION_DHCP4_PRIVATE_253 = 253, + NM_DHCP_OPTION_DHCP4_PRIVATE_254 = 254, + NM_DHCP_OPTION_DHCP4_END = 255, + /* Internal values */ + NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS = 1024, + NM_DHCP_OPTION_DHCP4_NM_EXPIRY = 1025, + NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER = 1026, +} NMDhcpOptionDhcp4Options; + +typedef enum { + NM_DHCP_OPTION_DHCP6_CLIENTID = 1, + NM_DHCP_OPTION_DHCP6_SERVERID = 2, + NM_DHCP_OPTION_DHCP6_DNS_SERVERS = 23, + NM_DHCP_OPTION_DHCP6_DOMAIN_LIST = 24, + NM_DHCP_OPTION_DHCP6_SNTP_SERVERS = 31, + NM_DHCP_OPTION_DHCP6_FQDN = 39, + NM_DHCP_OPTION_DHCP6_MUD_URL = 112, + + /* Internal values */ + NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS = 1026, + NM_DHCP_OPTION_DHCP6_NM_PREFIXLEN = 1027, + NM_DHCP_OPTION_DHCP6_NM_PREFERRED_LIFE = 1028, + NM_DHCP_OPTION_DHCP6_NM_MAX_LIFE = 1029, + NM_DHCP_OPTION_DHCP6_NM_STARTS = 1030, + NM_DHCP_OPTION_DHCP6_NM_LIFE_STARTS = 1031, + NM_DHCP_OPTION_DHCP6_NM_RENEW = 1032, + NM_DHCP_OPTION_DHCP6_NM_REBIND = 1033, + NM_DHCP_OPTION_DHCP6_NM_IAID = 1034, + +} NMDhcpOptionDhcp6Options; + +#define NM_DHCP_OPTION_REQPREFIX "requested_" + +typedef struct { + const char *name; + uint16_t option_num; + bool include; +} NMDhcpOption; + +extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[142]; +extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[16]; + +static inline const char * +nm_dhcp_option_get_name(const NMDhcpOption *option) +{ + nm_assert(option); + nm_assert(option->name); + nm_assert(NM_STR_HAS_PREFIX(option->name, NM_DHCP_OPTION_REQPREFIX)); + + return &option->name[NM_STRLEN(NM_DHCP_OPTION_REQPREFIX)]; +} + +const NMDhcpOption *nm_dhcp_option_find(int addr_family, guint option); + +static inline const char * +nm_dhcp_option_request_string(int addr_family, guint option) +{ + return nm_dhcp_option_get_name(nm_dhcp_option_find(addr_family, option)); +} + +void nm_dhcp_option_take_option(GHashTable *options, int addr_family, guint option, char *value); +void +nm_dhcp_option_add_option(GHashTable *options, int addr_family, guint option, const char *value); +void nm_dhcp_option_add_option_utf8safe_escape(GHashTable * options, + int addr_family, + guint option, + const guint8 *data, + gsize n_data); +void nm_dhcp_option_add_option_in_addr(GHashTable *options, + int addr_family, + guint option, + in_addr_t value); +void +nm_dhcp_option_add_option_u64(GHashTable *options, int addr_family, guint option, guint64 value); +void nm_dhcp_option_add_requests_to_options(GHashTable *options, int addr_family); +GHashTable *nm_dhcp_option_create_options_dict(void); + +#endif /* __NM_DHCP_OPTIONS_H__ */ diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c new file mode 100644 index 00000000..b92a9073 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-systemd.c @@ -0,0 +1,1124 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <stdlib.h> +#include <unistd.h> +#include <stdio.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <ctype.h> +#include <net/if_arp.h> + +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-std-aux/unaligned.h" + +#include "nm-utils.h" +#include "nm-dhcp-utils.h" +#include "nm-dhcp-options.h" +#include "nm-core-utils.h" +#include "NetworkManagerUtils.h" +#include "platform/nm-platform.h" +#include "nm-dhcp-client-logging.h" +#include "systemd/nm-sd.h" +#include "systemd/nm-sd-utils-dhcp.h" + +/*****************************************************************************/ + +#define NM_TYPE_DHCP_SYSTEMD (nm_dhcp_systemd_get_type()) +#define NM_DHCP_SYSTEMD(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemd)) +#define NM_DHCP_SYSTEMD_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemdClass)) +#define NM_IS_DHCP_SYSTEMD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DHCP_SYSTEMD)) +#define NM_IS_DHCP_SYSTEMD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DHCP_SYSTEMD)) +#define NM_DHCP_SYSTEMD_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_SYSTEMD, NMDhcpSystemdClass)) + +typedef struct _NMDhcpSystemd NMDhcpSystemd; +typedef struct _NMDhcpSystemdClass NMDhcpSystemdClass; + +static GType nm_dhcp_systemd_get_type(void); + +/*****************************************************************************/ + +typedef struct { + sd_dhcp_client * client4; + sd_dhcp6_client *client6; + char * lease_file; + + guint request_count; + + bool privacy : 1; +} NMDhcpSystemdPrivate; + +struct _NMDhcpSystemd { + NMDhcpClient parent; + NMDhcpSystemdPrivate _priv; +}; + +struct _NMDhcpSystemdClass { + NMDhcpClientClass parent; +}; + +G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT) + +#define NM_DHCP_SYSTEMD_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDhcpSystemd, NM_IS_DHCP_SYSTEMD) + +/*****************************************************************************/ + +static NMIP4Config * +lease_to_ip4_config(NMDedupMultiIndex *multi_idx, + const char * iface, + int ifindex, + sd_dhcp_lease * lease, + guint32 route_table, + guint32 route_metric, + GHashTable ** out_options, + GError ** error) +{ + gs_unref_object NMIP4Config *ip4_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + const struct in_addr * addr_list; + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + const char * s; + nm_auto_free_gstring GString *str = NULL; + gs_free sd_dhcp_route **routes = NULL; + const char *const * search_domains = NULL; + guint16 mtu; + int i, num; + const void * data; + gsize data_len; + gboolean metered = FALSE; + gboolean has_router_from_classless = FALSE; + gboolean has_classless_route = FALSE; + gboolean has_static_route = FALSE; + const gint32 ts = nm_utils_get_monotonic_timestamp_sec(); + gint64 ts_time = time(NULL); + struct in_addr a_address; + struct in_addr a_netmask; + struct in_addr a_next_server; + struct in_addr server_id; + struct in_addr broadcast; + const struct in_addr * a_router; + guint32 a_plen; + guint32 a_lifetime; + guint32 renewal; + guint32 rebinding; + gs_free nm_sd_dhcp_option *private_options = NULL; + + nm_assert(lease != NULL); + + if (sd_dhcp_lease_get_address(lease, &a_address) < 0) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get address from lease"); + return NULL; + } + + if (sd_dhcp_lease_get_netmask(lease, &a_netmask) < 0) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get netmask from lease"); + return NULL; + } + + if (sd_dhcp_lease_get_lifetime(lease, &a_lifetime) < 0) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get lifetime from lease"); + return NULL; + } + + ip4_config = nm_ip4_config_new(multi_idx, ifindex); + + options = out_options ? nm_dhcp_option_create_options_dict() : NULL; + + _nm_utils_inet4_ntop(a_address.s_addr, addr_str); + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, addr_str); + + a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask.s_addr); + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_SUBNET_MASK, + _nm_utils_inet4_ntop(a_netmask.s_addr, addr_str)); + + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME, + a_lifetime); + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_NM_EXPIRY, + (guint64)(ts_time + a_lifetime)); + + if (sd_dhcp_lease_get_next_server(lease, &a_next_server) == 0) { + _nm_utils_inet4_ntop(a_next_server.s_addr, addr_str); + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, addr_str); + } + + nm_ip4_config_add_address(ip4_config, + &((const NMPlatformIP4Address){ + .address = a_address.s_addr, + .peer_address = a_address.s_addr, + .plen = a_plen, + .addr_source = NM_IP_CONFIG_SOURCE_DHCP, + .timestamp = ts, + .lifetime = a_lifetime, + .preferred = a_lifetime, + })); + + if (sd_dhcp_lease_get_server_identifier(lease, &server_id) >= 0) { + _nm_utils_inet4_ntop(server_id.s_addr, addr_str); + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_SERVER_ID, addr_str); + } + + if (sd_dhcp_lease_get_broadcast(lease, &broadcast) >= 0) { + _nm_utils_inet4_ntop(broadcast.s_addr, addr_str); + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_BROADCAST, addr_str); + } + + num = sd_dhcp_lease_get_dns(lease, &addr_list); + if (num > 0) { + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str); + g_string_append(nm_gstring_add_space_delimiter(str), addr_str); + + if (addr_list[i].s_addr == 0 || nm_ip4_addr_is_localhost(addr_list[i].s_addr)) { + /* Skip localhost addresses, like also networkd does. + * See https://github.com/systemd/systemd/issues/4524. */ + continue; + } + nm_ip4_config_add_nameserver(ip4_config, addr_list[i].s_addr); + } + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, + str->str); + } + + num = sd_dhcp_lease_get_search_domains(lease, (char ***) &search_domains); + if (num > 0) { + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + g_string_append(nm_gstring_add_space_delimiter(str), search_domains[i]); + nm_ip4_config_add_search(ip4_config, search_domains[i]); + } + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, + str->str); + } + + if (sd_dhcp_lease_get_domainname(lease, &s) >= 0) { + gs_strfreev char **domains = NULL; + char ** d; + + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, s); + + /* Multiple domains sometimes stuffed into option 15 "Domain Name". + * As systemd escapes such characters, split them at \\032. */ + domains = g_strsplit(s, "\\032", 0); + for (d = domains; *d; d++) + nm_ip4_config_add_domain(ip4_config, *d); + } + + if (sd_dhcp_lease_get_hostname(lease, &s) >= 0) { + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s); + } + + num = sd_dhcp_lease_get_routes(lease, &routes); + if (num > 0) { + nm_auto_free_gstring GString *str_classless = NULL; + nm_auto_free_gstring GString *str_static = NULL; + guint32 default_route_metric = route_metric; + + for (i = 0; i < num; i++) { + switch (sd_dhcp_route_get_option(routes[i])) { + case NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE: + has_classless_route = TRUE; + break; + case NM_DHCP_OPTION_DHCP4_STATIC_ROUTE: + has_static_route = TRUE; + break; + } + } + + if (has_classless_route) + str_classless = g_string_sized_new(30); + if (has_static_route) + str_static = g_string_sized_new(30); + + for (i = 0; i < num; i++) { + char network_net_str[NM_UTILS_INET_ADDRSTRLEN]; + char gateway_str[NM_UTILS_INET_ADDRSTRLEN]; + guint8 r_plen; + struct in_addr r_network; + struct in_addr r_gateway; + in_addr_t network_net; + int option; + guint32 m; + + option = sd_dhcp_route_get_option(routes[i]); + if (!NM_IN_SET(option, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + NM_DHCP_OPTION_DHCP4_STATIC_ROUTE)) + continue; + + if (sd_dhcp_route_get_destination(routes[i], &r_network) < 0) + continue; + if (sd_dhcp_route_get_destination_prefix_length(routes[i], &r_plen) < 0 || r_plen > 32) + continue; + if (sd_dhcp_route_get_gateway(routes[i], &r_gateway) < 0) + continue; + + network_net = nm_utils_ip4_address_clear_host_address(r_network.s_addr, r_plen); + _nm_utils_inet4_ntop(network_net, network_net_str); + _nm_utils_inet4_ntop(r_gateway.s_addr, gateway_str); + + g_string_append_printf( + nm_gstring_add_space_delimiter(option == NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE + ? str_classless + : str_static), + "%s/%d %s", + network_net_str, + (int) r_plen, + gateway_str); + + if (option == NM_DHCP_OPTION_DHCP4_STATIC_ROUTE && has_classless_route) { + /* RFC 3443: if the DHCP server returns both a Classless Static Routes + * option and a Static Routes option, the DHCP client MUST ignore the + * Static Routes option. */ + continue; + } + + if (r_plen == 0 && option == NM_DHCP_OPTION_DHCP4_STATIC_ROUTE) { + /* for option 33 (static route), RFC 2132 says: + * + * The default route (0.0.0.0) is an illegal destination for a static + * route. */ + continue; + } + + if (r_plen == 0) { + /* if there are multiple default routes, we add them with differing + * metrics. */ + m = default_route_metric; + if (default_route_metric < G_MAXUINT32) + default_route_metric++; + + has_router_from_classless = TRUE; + } else + m = route_metric; + + nm_ip4_config_add_route( + ip4_config, + &((const NMPlatformIP4Route){ + .network = network_net, + .plen = r_plen, + .gateway = r_gateway.s_addr, + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .metric = m, + .table_coerced = nm_platform_route_table_coerce(route_table), + }), + NULL); + } + + if (str_classless && str_classless->len > 0) + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + str_classless->str); + if (str_static && str_static->len > 0) + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, + str_static->str); + } + + num = sd_dhcp_lease_get_router(lease, &a_router); + if (num > 0) { + guint32 default_route_metric = route_metric; + + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + guint32 m; + + s = _nm_utils_inet4_ntop(a_router[i].s_addr, addr_str); + g_string_append(nm_gstring_add_space_delimiter(str), s); + + if (a_router[i].s_addr == 0) { + /* silently skip 0.0.0.0 */ + continue; + } + + if (has_router_from_classless) { + /* If the DHCP server returns both a Classless Static Routes option and a + * Router option, the DHCP client MUST ignore the Router option [RFC 3442]. + * + * Be more lenient and ignore the Router option only if Classless Static + * Routes contain a default gateway (as other DHCP backends do). + */ + continue; + } + + /* if there are multiple default routes, we add them with differing + * metrics. */ + m = default_route_metric; + if (default_route_metric < G_MAXUINT32) + default_route_metric++; + + nm_ip4_config_add_route( + ip4_config, + &((const NMPlatformIP4Route){ + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .gateway = a_router[i].s_addr, + .table_coerced = nm_platform_route_table_coerce(route_table), + .metric = m, + }), + NULL); + } + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROUTER, str->str); + } + + if (sd_dhcp_lease_get_mtu(lease, &mtu) >= 0 && mtu) { + nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, mtu); + nm_ip4_config_set_mtu(ip4_config, mtu, NM_IP_CONFIG_SOURCE_DHCP); + } + + num = sd_dhcp_lease_get_ntp(lease, &addr_list); + if (num > 0) { + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str); + g_string_append(nm_gstring_add_space_delimiter(str), addr_str); + } + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NTP_SERVER, str->str); + } + + if (sd_dhcp_lease_get_root_path(lease, &s) >= 0) { + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROOT_PATH, s); + } + + if (sd_dhcp_lease_get_t1(lease, &renewal) >= 0) { + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_RENEWAL_T1_TIME, + renewal); + } + + if (sd_dhcp_lease_get_t2(lease, &rebinding) >= 0) { + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_REBINDING_T2_TIME, + rebinding); + } + + if (sd_dhcp_lease_get_timezone(lease, &s) >= 0) { + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE, s); + } + + if (sd_dhcp_lease_get_vendor_specific(lease, &data, &data_len) >= 0) + metered = !!memmem(data, data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")); + nm_ip4_config_set_metered(ip4_config, metered); + + num = nm_sd_dhcp_lease_get_private_options(lease, &private_options); + if (num > 0) { + for (i = 0; i < num; i++) { + char *option_string; + + option_string = nm_utils_bin2hexstr_full(private_options[i].data, + private_options[i].data_len, + ':', + FALSE, + NULL); + if (!options) { + g_free(option_string); + continue; + } + nm_dhcp_option_take_option(options, AF_INET, private_options[i].code, option_string); + } + } + NM_SET_OUT(out_options, g_steal_pointer(&options)); + return g_steal_pointer(&ip4_config); +} + +/*****************************************************************************/ + +static void +bound4_handle(NMDhcpSystemd *self, gboolean extended) +{ + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + const char * iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self)); + gs_unref_object NMIP4Config *ip4_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + sd_dhcp_lease * lease = NULL; + GError * error = NULL; + + if (sd_dhcp_client_get_lease(priv->client4, &lease) < 0 || !lease) { + _LOGW("no lease!"); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + _LOGD("lease available"); + + ip4_config = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)), + iface, + nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)), + lease, + nm_dhcp_client_get_route_table(NM_DHCP_CLIENT(self)), + nm_dhcp_client_get_route_metric(NM_DHCP_CLIENT(self)), + &options, + &error); + if (!ip4_config) { + _LOGW("%s", error->message); + g_clear_error(&error); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + nm_dhcp_option_add_requests_to_options(options, AF_INET); + dhcp_lease_save(lease, priv->lease_file); + + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), + extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND, + NM_IP_CONFIG_CAST(ip4_config), + options); +} + +static int +dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data) +{ + NMDhcpSystemd * self = NM_DHCP_SYSTEMD(user_data); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + char addr_str[INET_ADDRSTRLEN]; + sd_dhcp_lease * lease = NULL; + struct in_addr addr; + int r; + + nm_assert(priv->client4 == client); + + _LOGD("client event %d", event); + + switch (event) { + case SD_DHCP_CLIENT_EVENT_EXPIRED: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_EXPIRE, NULL, NULL); + break; + case SD_DHCP_CLIENT_EVENT_STOP: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL, NULL); + break; + case SD_DHCP_CLIENT_EVENT_RENEW: + case SD_DHCP_CLIENT_EVENT_IP_CHANGE: + bound4_handle(self, TRUE); + break; + case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE: + bound4_handle(self, FALSE); + break; + case SD_DHCP_CLIENT_EVENT_SELECTING: + r = sd_dhcp_client_get_lease(priv->client4, &lease); + if (r < 0) + return r; + r = sd_dhcp_lease_get_server_identifier(lease, &addr); + if (r < 0) + return r; + if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(user_data), &addr)) { + _LOGD("server-id %s is in the reject-list, ignoring", + nm_utils_inet_ntop(AF_INET, &addr, addr_str)); + return -ENOMSG; + } + break; + case SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE: + break; + default: + _LOGW("unhandled DHCP event %d", event); + break; + } + + return 0; +} + +static gboolean +ip4_start(NMDhcpClient *client, + const char * dhcp_anycast_addr, + const char * last_ip4_address, + GError ** error) +{ + nm_auto(sd_dhcp_client_unrefp) sd_dhcp_client *sd_client = NULL; + NMDhcpSystemd * self = NM_DHCP_SYSTEMD(client); + NMDhcpSystemdPrivate * priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + gs_free char * lease_file = NULL; + GBytes * hwaddr; + const uint8_t * hwaddr_arr; + gsize hwaddr_len; + int arp_type; + GBytes * client_id; + gs_unref_bytes GBytes *client_id_new = NULL; + GBytes * vendor_class_identifier; + const uint8_t * client_id_arr; + size_t client_id_len; + struct in_addr last_addr = {0}; + const char * hostname; + const char * mud_url; + int r, i; + GBytes * bcast_hwaddr; + const uint8_t * bcast_hwaddr_arr; + gsize bcast_hwaddr_len; + + g_return_val_if_fail(!priv->client4, FALSE); + g_return_val_if_fail(!priv->client6, FALSE); + + r = sd_dhcp_client_new(&sd_client, FALSE); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to create dhcp-client: %s"); + return FALSE; + } + + _LOGT("dhcp-client4: set %p", sd_client); + + r = sd_dhcp_client_attach_event(sd_client, NULL, 0); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to attach event: %s"); + return FALSE; + } + + hwaddr = nm_dhcp_client_get_hw_addr(client); + if (!hwaddr || !(hwaddr_arr = g_bytes_get_data(hwaddr, &hwaddr_len)) + || (arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len)) < 0) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address"); + return FALSE; + } + + bcast_hwaddr_arr = NULL; + if ((bcast_hwaddr = nm_dhcp_client_get_broadcast_hw_addr(NM_DHCP_CLIENT(self)))) { + bcast_hwaddr_arr = g_bytes_get_data(bcast_hwaddr, &bcast_hwaddr_len); + if (bcast_hwaddr_len != hwaddr_len) + bcast_hwaddr_arr = NULL; + } + + r = sd_dhcp_client_set_mac(sd_client, + hwaddr_arr, + bcast_hwaddr_arr, + hwaddr_len, + (guint16) arp_type); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set MAC address: %s"); + return FALSE; + } + + r = sd_dhcp_client_set_ifindex(sd_client, nm_dhcp_client_get_ifindex(client)); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set ifindex: %s"); + return FALSE; + } + + nm_dhcp_utils_get_leasefile_path(AF_INET, + "internal", + nm_dhcp_client_get_iface(client), + nm_dhcp_client_get_uuid(client), + &lease_file); + + if (last_ip4_address) + inet_pton(AF_INET, last_ip4_address, &last_addr); + else { + nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL; + + dhcp_lease_load(&lease, lease_file); + if (lease) + sd_dhcp_lease_get_address(lease, &last_addr); + } + + if (last_addr.s_addr) { + r = sd_dhcp_client_set_request_address(sd_client, &last_addr); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set last IPv4 address: %s"); + return FALSE; + } + } + + client_id = nm_dhcp_client_get_client_id(client); + if (!client_id) { + client_id_new = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_arr, hwaddr_len); + client_id = client_id_new; + } + + if (!(client_id_arr = g_bytes_get_data(client_id, &client_id_len)) || client_id_len < 2) { + /* invalid client-ids are not expected. */ + nm_assert_not_reached(); + + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id"); + return FALSE; + } + + /* Note that we always set a client-id. In particular for infiniband that is necessary, + * see https://tools.ietf.org/html/rfc4390#section-2.1 . */ + r = sd_dhcp_client_set_client_id(sd_client, + client_id_arr[0], + client_id_arr + 1, + NM_MIN(client_id_len - 1, _NM_SD_MAX_CLIENT_ID_LEN)); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set IPv4 client-id: %s"); + return FALSE; + } + + /* Add requested options */ + for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options); i++) { + if (_nm_dhcp_option_dhcp4_options[i].include) { + nm_assert(_nm_dhcp_option_dhcp4_options[i].option_num <= 255); + r = sd_dhcp_client_set_request_option(sd_client, + _nm_dhcp_option_dhcp4_options[i].option_num); + nm_assert(r >= 0 || r == -EEXIST); + } + } + + hostname = nm_dhcp_client_get_hostname(client); + if (hostname) { + /* FIXME: sd-dhcp decides which hostname/FQDN option to send (12 or 81) + * only based on whether the hostname has a domain part or not. At the + * moment there is no way to force one or another. + */ + r = sd_dhcp_client_set_hostname(sd_client, hostname); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s"); + return FALSE; + } + } + + mud_url = nm_dhcp_client_get_mud_url(client); + if (mud_url) { + r = sd_dhcp_client_set_mud_url(sd_client, mud_url); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set DHCP MUDURL: %s"); + return FALSE; + } + } + + vendor_class_identifier = nm_dhcp_client_get_vendor_class_identifier(client); + if (vendor_class_identifier) { + const char *option_data; + gsize len; + + option_data = g_bytes_get_data(vendor_class_identifier, &len); + nm_assert(option_data); + nm_assert(len <= 255); + + option_data = nm_strndup_a(300, option_data, len, NULL); + + r = sd_dhcp_client_set_vendor_class_identifier(sd_client, option_data); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set DHCP vendor class identifier: %s"); + return FALSE; + } + } + + r = sd_dhcp_client_set_callback(sd_client, dhcp_event_cb, client); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set callback: %s"); + return FALSE; + } + + priv->client4 = g_steal_pointer(&sd_client); + + g_free(priv->lease_file); + priv->lease_file = g_steal_pointer(&lease_file); + + nm_dhcp_client_set_client_id(client, client_id); + + r = sd_dhcp_client_start(priv->client4); + if (r < 0) { + sd_dhcp_client_set_callback(priv->client4, NULL, NULL); + nm_clear_pointer(&priv->client4, sd_dhcp_client_unref); + nm_utils_error_set_errno(error, r, "failed to start DHCP client: %s"); + return FALSE; + } + + nm_dhcp_client_start_timeout(client); + return TRUE; +} + +static NMIP6Config * +lease_to_ip6_config(NMDedupMultiIndex *multi_idx, + const char * iface, + int ifindex, + sd_dhcp6_lease * lease, + gboolean info_only, + GHashTable ** out_options, + gint32 ts, + GError ** error) +{ + gs_unref_object NMIP6Config *ip6_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + struct in6_addr tmp_addr; + const struct in6_addr * dns; + uint32_t lft_pref, lft_valid; + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + char ** domains; + const char * s; + nm_auto_free_gstring GString *str = NULL; + int num, i; + + nm_assert(lease); + + ip6_config = nm_ip6_config_new(multi_idx, ifindex); + + options = out_options ? nm_dhcp_option_create_options_dict() : NULL; + + sd_dhcp6_lease_reset_address_iter(lease); + nm_gstring_prepare(&str); + while (sd_dhcp6_lease_get_address(lease, &tmp_addr, &lft_pref, &lft_valid) >= 0) { + const NMPlatformIP6Address address = { + .plen = 128, + .address = tmp_addr, + .timestamp = ts, + .lifetime = lft_valid, + .preferred = lft_pref, + .addr_source = NM_IP_CONFIG_SOURCE_DHCP, + }; + + nm_ip6_config_add_address(ip6_config, &address); + + _nm_utils_inet6_ntop(&tmp_addr, addr_str); + g_string_append(nm_gstring_add_space_delimiter(str), addr_str); + }; + if (str->len) + nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS, str->str); + + if (!info_only && nm_ip6_config_get_num_addresses(ip6_config) == 0) { + g_set_error_literal(error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "no address received in managed mode"); + return NULL; + } + + num = sd_dhcp6_lease_get_dns(lease, &dns); + if (num > 0) { + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + _nm_utils_inet6_ntop(&dns[i], addr_str); + g_string_append(nm_gstring_add_space_delimiter(str), addr_str); + nm_ip6_config_add_nameserver(ip6_config, &dns[i]); + } + nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_DNS_SERVERS, str->str); + } + + num = sd_dhcp6_lease_get_domains(lease, &domains); + if (num > 0) { + nm_gstring_prepare(&str); + for (i = 0; i < num; i++) { + g_string_append(nm_gstring_add_space_delimiter(str), domains[i]); + nm_ip6_config_add_search(ip6_config, domains[i]); + } + nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, str->str); + } + + if (sd_dhcp6_lease_get_fqdn(lease, &s) >= 0) { + nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_FQDN, s); + } + + NM_SET_OUT(out_options, g_steal_pointer(&options)); + return g_steal_pointer(&ip6_config); +} + +static void +bound6_handle(NMDhcpSystemd *self) +{ + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + const gint32 ts = nm_utils_get_monotonic_timestamp_sec(); + const char * iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self)); + gs_unref_object NMIP6Config *ip6_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + gs_free_error GError *error = NULL; + NMPlatformIP6Address prefix = {0}; + sd_dhcp6_lease * lease = NULL; + + if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) { + _LOGW(" no lease!"); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + _LOGD("lease available"); + + ip6_config = lease_to_ip6_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)), + iface, + nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)), + lease, + nm_dhcp_client_get_info_only(NM_DHCP_CLIENT(self)), + &options, + ts, + &error); + + if (!ip6_config) { + _LOGW("%s", error->message); + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), + NM_DHCP_STATE_BOUND, + NM_IP_CONFIG_CAST(ip6_config), + options); + + sd_dhcp6_lease_reset_pd_prefix_iter(lease); + while (!sd_dhcp6_lease_get_pd(lease, + &prefix.address, + &prefix.plen, + &prefix.preferred, + &prefix.lifetime)) { + prefix.timestamp = ts; + nm_dhcp_client_emit_ipv6_prefix_delegated(NM_DHCP_CLIENT(self), &prefix); + } +} + +static void +dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data) +{ + NMDhcpSystemd * self = NM_DHCP_SYSTEMD(user_data); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + + nm_assert(priv->client6 == client); + + _LOGD("client event %d", event); + + switch (event) { + case SD_DHCP6_CLIENT_EVENT_RETRANS_MAX: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_TIMEOUT, NULL, NULL); + break; + case SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE: + case SD_DHCP6_CLIENT_EVENT_STOP: + nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL, NULL); + break; + case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE: + case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST: + bound6_handle(self); + break; + default: + _LOGW("unhandled event %d", event); + break; + } +} + +static gboolean +ip6_start(NMDhcpClient * client, + const char * dhcp_anycast_addr, + const struct in6_addr * ll_addr, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes, + GError ** error) +{ + NMDhcpSystemd * self = NM_DHCP_SYSTEMD(client); + NMDhcpSystemdPrivate * priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + nm_auto(sd_dhcp6_client_unrefp) sd_dhcp6_client *sd_client = NULL; + const char * hostname; + const char * mud_url; + int r, i; + const guint8 * duid_arr; + gsize duid_len; + GBytes * duid; + + g_return_val_if_fail(!priv->client4, FALSE); + g_return_val_if_fail(!priv->client6, FALSE); + + if (!(duid = nm_dhcp_client_get_client_id(client)) + || !(duid_arr = g_bytes_get_data(duid, &duid_len)) || duid_len < 2) { + nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing DUID"); + g_return_val_if_reached(FALSE); + } + + r = sd_dhcp6_client_new(&sd_client); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to create dhcp-client: %s"); + return FALSE; + } + + _LOGT("dhcp-client6: set %p", sd_client); + + if (nm_dhcp_client_get_info_only(client)) { + sd_dhcp6_client_set_address_request(sd_client, 0); + if (needed_prefixes == 0) + sd_dhcp6_client_set_information_request(sd_client, 1); + } + + r = sd_dhcp6_client_set_iaid(sd_client, nm_dhcp_client_get_iaid(client)); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set IAID: %s"); + return FALSE; + } + + r = sd_dhcp6_client_set_duid(sd_client, + unaligned_read_be16(&duid_arr[0]), + &duid_arr[2], + duid_len - 2); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set DUID: %s"); + return FALSE; + } + + r = sd_dhcp6_client_attach_event(sd_client, NULL, 0); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to attach event: %s"); + return FALSE; + } + + r = sd_dhcp6_client_set_ifindex(sd_client, nm_dhcp_client_get_ifindex(client)); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set ifindex: %s"); + return FALSE; + } + + /* Add requested options */ + for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options); i++) { + if (_nm_dhcp_option_dhcp6_options[i].include) { + r = sd_dhcp6_client_set_request_option(sd_client, + _nm_dhcp_option_dhcp6_options[i].option_num); + nm_assert(r >= 0 || r == -EEXIST); + } + } + + mud_url = nm_dhcp_client_get_mud_url(client); + if (mud_url) { + r = sd_dhcp6_client_set_request_mud_url(sd_client, mud_url); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set mud-url: %s"); + return FALSE; + } + } + + if (needed_prefixes > 0) { + if (needed_prefixes > 1) + _LOGW("dhcp-client6: only one prefix request is supported"); + /* FIXME: systemd-networkd API only allows to request a + * single prefix */ + r = sd_dhcp6_client_set_prefix_delegation(sd_client, TRUE); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to enable prefix delegation: %s"); + return FALSE; + } + } + + r = sd_dhcp6_client_set_local_address(sd_client, ll_addr); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set local address: %s"); + return FALSE; + } + + hostname = nm_dhcp_client_get_hostname(client); + r = sd_dhcp6_client_set_fqdn(sd_client, hostname); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s"); + return FALSE; + } + + r = sd_dhcp6_client_set_callback(sd_client, dhcp6_event_cb, client); + if (r < 0) { + nm_utils_error_set_errno(error, r, "failed to set callback: %s"); + return FALSE; + } + + priv->client6 = g_steal_pointer(&sd_client); + + r = sd_dhcp6_client_start(priv->client6); + if (r < 0) { + sd_dhcp6_client_set_callback(priv->client6, NULL, NULL); + nm_clear_pointer(&priv->client6, sd_dhcp6_client_unref); + nm_utils_error_set_errno(error, r, "failed to start client: %s"); + return FALSE; + } + + nm_dhcp_client_start_timeout(client); + return TRUE; +} + +static void +stop(NMDhcpClient *client, gboolean release) +{ + NMDhcpSystemd * self = NM_DHCP_SYSTEMD(client); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self); + int r = 0; + + NM_DHCP_CLIENT_CLASS(nm_dhcp_systemd_parent_class)->stop(client, release); + + _LOGT("dhcp-client%d: stop %p", + priv->client4 ? '4' : '6', + priv->client4 ? (gpointer) priv->client4 : (gpointer) priv->client6); + + if (priv->client4) { + sd_dhcp_client_set_callback(priv->client4, NULL, NULL); + r = sd_dhcp_client_stop(priv->client4); + } else if (priv->client6) { + sd_dhcp6_client_set_callback(priv->client6, NULL, NULL); + r = sd_dhcp6_client_stop(priv->client6); + } + + if (r) + _LOGW("failed to stop client (%d)", r); +} + +/*****************************************************************************/ + +static void +nm_dhcp_systemd_init(NMDhcpSystemd *self) +{} + +static void +dispose(GObject *object) +{ + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(object); + + nm_clear_g_free(&priv->lease_file); + + if (priv->client4) { + sd_dhcp_client_stop(priv->client4); + sd_dhcp_client_unref(priv->client4); + priv->client4 = NULL; + } + + if (priv->client6) { + sd_dhcp6_client_stop(priv->client6); + sd_dhcp6_client_unref(priv->client6); + priv->client6 = NULL; + } + + G_OBJECT_CLASS(nm_dhcp_systemd_parent_class)->dispose(object); +} + +static void +nm_dhcp_systemd_class_init(NMDhcpSystemdClass *sdhcp_class) +{ + NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(sdhcp_class); + GObjectClass * object_class = G_OBJECT_CLASS(sdhcp_class); + + object_class->dispose = dispose; + + client_class->ip4_start = ip4_start; + client_class->ip6_start = ip6_start; + client_class->stop = stop; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_systemd = { + .name = "systemd", + .get_type = nm_dhcp_systemd_get_type, + .experimental = TRUE, +}; + +/*****************************************************************************/ + +static GType +_get_type_per_addr_family(int addr_family) +{ + nm_assert_addr_family(addr_family); + + if (addr_family == AF_INET) + return nm_dhcp_nettools_get_type(); + return nm_dhcp_systemd_get_type(); +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_internal = { + .name = "internal", + .get_type_per_addr_family = _get_type_per_addr_family, +}; diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c new file mode 100644 index 00000000..646411e2 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-utils.c @@ -0,0 +1,1121 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2005 - 2010 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <unistd.h> +#include <arpa/inet.h> + +#include "nm-std-aux/unaligned.h" +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-glib-aux/nm-str-buf.h" +#include "systemd/nm-sd-utils-shared.h" + +#include "nm-dhcp-utils.h" +#include "nm-utils.h" +#include "nm-config.h" +#include "NetworkManagerUtils.h" +#include "platform/nm-platform.h" +#include "nm-dhcp-client-logging.h" +#include "nm-core-internal.h" + +/*****************************************************************************/ + +static gboolean +ip4_process_dhcpcd_rfc3442_routes(const char * iface, + const char * str, + guint32 route_table, + guint32 route_metric, + NMIP4Config *ip4_config, + guint32 * gwaddr) +{ + gs_free const char **routes = NULL; + const char ** r; + gboolean have_routes = FALSE; + + routes = nm_utils_strsplit_set(str, " "); + if (!routes) + return FALSE; + + if ((NM_PTRARRAY_LEN(routes) % 2) != 0) { + _LOG2W(LOGD_DHCP4, iface, " classless static routes provided, but invalid"); + return FALSE; + } + + for (r = routes; *r; r += 2) { + char * slash; + NMPlatformIP4Route route; + int rt_cidr = 32; + guint32 rt_addr, rt_route; + + slash = strchr(*r, '/'); + if (slash) { + *slash = '\0'; + errno = 0; + rt_cidr = strtol(slash + 1, NULL, 10); + if (errno || rt_cidr > 32) { + _LOG2W(LOGD_DHCP4, + iface, + "DHCP provided invalid classless static route cidr: '%s'", + slash + 1); + continue; + } + } + if (inet_pton(AF_INET, *r, &rt_addr) <= 0) { + _LOG2W(LOGD_DHCP4, + iface, + "DHCP provided invalid classless static route address: '%s'", + *r); + continue; + } + if (inet_pton(AF_INET, *(r + 1), &rt_route) <= 0) { + _LOG2W(LOGD_DHCP4, + iface, + "DHCP provided invalid classless static route gateway: '%s'", + *(r + 1)); + continue; + } + + have_routes = TRUE; + if (rt_cidr == 0 && rt_addr == 0) { + /* FIXME: how to handle multiple routers? */ + *gwaddr = rt_route; + } else { + _LOG2I(LOGD_DHCP4, + iface, + " classless static route %s/%d gw %s", + *r, + rt_cidr, + *(r + 1)); + memset(&route, 0, sizeof(route)); + route.network = nm_utils_ip4_address_clear_host_address(rt_addr, rt_cidr); + route.plen = rt_cidr; + route.gateway = rt_route; + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = route_metric; + route.table_coerced = nm_platform_route_table_coerce(route_table); + nm_ip4_config_add_route(ip4_config, &route, NULL); + } + } + + return have_routes; +} + +static gboolean +process_dhclient_rfc3442_route(const char *const **p_octets, NMPlatformIP4Route *route) +{ + const char *const *o = *p_octets; + gs_free char * next_hop = NULL; + int addr_len; + int v_plen; + in_addr_t tmp_addr; + in_addr_t v_network = 0; + + v_plen = _nm_utils_ascii_str_to_int64(*o, 10, 0, 32, -1); + if (v_plen == -1) + return FALSE; + o++; + + addr_len = v_plen > 0 ? ((v_plen - 1) / 8) + 1 : 0; + + /* ensure there's at least the address + next hop left */ + if (NM_PTRARRAY_LEN(o) < addr_len + 4) + return FALSE; + + if (v_plen > 0) { + const char * addr[4] = {"0", "0", "0", "0"}; + gs_free char *str_addr = NULL; + int i; + + for (i = 0; i < addr_len; i++) + addr[i] = *o++; + + str_addr = g_strjoin(".", addr[0], addr[1], addr[2], addr[3], NULL); + if (inet_pton(AF_INET, str_addr, &tmp_addr) <= 0) + return FALSE; + v_network = nm_utils_ip4_address_clear_host_address(tmp_addr, v_plen); + } + + next_hop = g_strjoin(".", o[0], o[1], o[2], o[3], NULL); + o += 4; + if (inet_pton(AF_INET, next_hop, &tmp_addr) <= 0) + return FALSE; + + *route = (NMPlatformIP4Route){ + .network = v_network, + .plen = v_plen, + .gateway = tmp_addr, + }; + *p_octets = o; + return TRUE; +} + +static gboolean +ip4_process_dhclient_rfc3442_routes(const char * iface, + const char * str, + guint32 route_table, + guint32 route_metric, + NMIP4Config *ip4_config, + guint32 * gwaddr) +{ + gs_free const char **octets = NULL; + const char *const * o; + gboolean have_routes = FALSE; + + octets = nm_utils_strsplit_set_with_empty(str, " ."); + if (NM_PTRARRAY_LEN(octets) < 5) { + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid classless static routes '%s'", str); + return FALSE; + } + + o = octets; + while (*o) { + NMPlatformIP4Route route; + + if (!process_dhclient_rfc3442_route(&o, &route)) { + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid classless static routes"); + return have_routes; + } + + have_routes = TRUE; + if (!route.plen) { + /* gateway passed as classless static route */ + *gwaddr = route.gateway; + } else { + char b1[INET_ADDRSTRLEN]; + char b2[INET_ADDRSTRLEN]; + + /* normal route */ + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = route_metric; + route.table_coerced = nm_platform_route_table_coerce(route_table); + nm_ip4_config_add_route(ip4_config, &route, NULL); + + _LOG2I(LOGD_DHCP4, + iface, + " classless static route %s/%d gw %s", + _nm_utils_inet4_ntop(route.network, b1), + route.plen, + _nm_utils_inet4_ntop(route.gateway, b2)); + } + } + + return have_routes; +} + +static gboolean +ip4_process_classless_routes(const char * iface, + GHashTable * options, + guint32 route_table, + guint32 route_metric, + NMIP4Config *ip4_config, + guint32 * gwaddr) +{ + const char *str, *p; + + g_return_val_if_fail(options != NULL, FALSE); + g_return_val_if_fail(ip4_config != NULL, FALSE); + + *gwaddr = 0; + + /* dhcpd/dhclient in Fedora has support for rfc3442 implemented using a + * slightly different format: + * + * option classless-static-routes = array of (destination-descriptor ip-address); + * + * which results in: + * + * 0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6 + * + * dhcpcd supports classless static routes natively and uses this same + * option identifier with the following format: + * + * 192.168.10.0/24 192.168.1.1 10.0.0.0/8 10.17.66.41 + */ + str = g_hash_table_lookup(options, "classless_static_routes"); + + /* dhclient doesn't have actual support for rfc3442 classless static routes + * upstream. Thus, people resort to defining the option in dhclient.conf + * and using arbitrary formats like so: + * + * option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; + * + * See https://lists.isc.org/pipermail/dhcp-users/2008-December/007629.html + */ + if (!str) + str = g_hash_table_lookup(options, "rfc3442_classless_static_routes"); + + /* Microsoft version; same as rfc3442 but with a different option # (249) */ + if (!str) + str = g_hash_table_lookup(options, "ms_classless_static_routes"); + + if (!str || !strlen(str)) + return FALSE; + + p = str; + while (*p) { + if (!g_ascii_isdigit(*p) && (*p != ' ') && (*p != '.') && (*p != '/')) { + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid classless static routes '%s'", str); + return FALSE; + } + p++; + }; + + if (strchr(str, '/')) { + /* dhcpcd format */ + return ip4_process_dhcpcd_rfc3442_routes(iface, + str, + route_table, + route_metric, + ip4_config, + gwaddr); + } + + return ip4_process_dhclient_rfc3442_routes(iface, + str, + route_table, + route_metric, + ip4_config, + gwaddr); +} + +static void +process_classful_routes(const char * iface, + GHashTable * options, + guint32 route_table, + guint32 route_metric, + NMIP4Config *ip4_config) +{ + gs_free const char **searches = NULL; + const char ** s; + const char * str; + + str = g_hash_table_lookup(options, "static_routes"); + if (!str) + return; + + searches = nm_utils_strsplit_set(str, " "); + if (!searches) + return; + + if ((NM_PTRARRAY_LEN(searches) % 2) != 0) { + _LOG2I(LOGD_DHCP, iface, " static routes provided, but invalid"); + return; + } + + for (s = searches; *s; s += 2) { + NMPlatformIP4Route route; + guint32 rt_addr, rt_route; + + if (inet_pton(AF_INET, *s, &rt_addr) <= 0) { + _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route address: '%s'", *s); + continue; + } + if (inet_pton(AF_INET, *(s + 1), &rt_route) <= 0) { + _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route gateway: '%s'", *(s + 1)); + continue; + } + + // FIXME: ensure the IP address and route are sane + + memset(&route, 0, sizeof(route)); + route.network = rt_addr; + /* RFC 2132, updated by RFC 3442: + * The Static Routes option (option 33) does not provide a subnet mask + * for each route - it is assumed that the subnet mask is implicit in + * whatever network number is specified in each route entry */ + route.plen = _nm_utils_ip4_get_default_prefix(rt_addr); + if (rt_addr & ~_nm_utils_ip4_prefix_to_netmask(route.plen)) { + /* RFC 943: target not "this network"; using host routing */ + route.plen = 32; + } + route.gateway = rt_route; + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = route_metric; + route.table_coerced = nm_platform_route_table_coerce(route_table); + + route.network = nm_utils_ip4_address_clear_host_address(route.network, route.plen); + + nm_ip4_config_add_route(ip4_config, &route, NULL); + _LOG2I(LOGD_DHCP, + iface, + " static route %s", + nm_platform_ip4_route_to_string(&route, NULL, 0)); + } +} + +static void +process_domain_search(const char *iface, const char *str, GFunc add_func, gpointer user_data) +{ + gs_free const char **searches = NULL; + gs_free char * unescaped = NULL; + const char ** s; + char * p; + int i; + + g_return_if_fail(str != NULL); + g_return_if_fail(add_func != NULL); + + unescaped = g_strdup(str); + + p = unescaped; + do { + p = strstr(p, "\\032"); + if (!p) + break; + + /* Clear the escaped space with real spaces */ + for (i = 0; i < 4; i++) + *p++ = ' '; + } while (*p++); + + if (strchr(unescaped, '\\')) { + _LOG2W(LOGD_DHCP, iface, " invalid domain search: '%s'", unescaped); + return; + } + + searches = nm_utils_strsplit_set(unescaped, " "); + for (s = searches; searches && *s; s++) { + _LOG2I(LOGD_DHCP, iface, " domain search '%s'", *s); + add_func((gpointer) *s, user_data); + } +} + +static void +ip4_add_domain_search(gpointer data, gpointer user_data) +{ + nm_ip4_config_add_search(NM_IP4_CONFIG(user_data), (const char *) data); +} + +NMIP4Config * +nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx, + int ifindex, + const char * iface, + GHashTable * options, + guint32 route_table, + guint32 route_metric) +{ + gs_unref_object NMIP4Config *ip4_config = NULL; + guint32 tmp_addr; + in_addr_t addr; + NMPlatformIP4Address address; + char * str = NULL; + gboolean gateway_has = FALSE; + guint32 gateway = 0; + guint8 plen = 0; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + g_return_val_if_fail(options != NULL, NULL); + + ip4_config = nm_ip4_config_new(multi_idx, ifindex); + memset(&address, 0, sizeof(address)); + address.timestamp = nm_utils_get_monotonic_timestamp_sec(); + + str = g_hash_table_lookup(options, "ip_address"); + if (str && (inet_pton(AF_INET, str, &addr) > 0)) + _LOG2I(LOGD_DHCP4, iface, " address %s", str); + else + return NULL; + + str = g_hash_table_lookup(options, "subnet_mask"); + if (str && (inet_pton(AF_INET, str, &tmp_addr) > 0)) { + plen = nm_utils_ip4_netmask_to_prefix(tmp_addr); + _LOG2I(LOGD_DHCP4, iface, " plen %d (%s)", plen, str); + } else { + /* Get default netmask for the IP according to appropriate class. */ + plen = _nm_utils_ip4_get_default_prefix(addr); + _LOG2I(LOGD_DHCP4, iface, " plen %d (default)", plen); + } + nm_platform_ip4_address_set_addr(&address, addr, plen); + + /* Routes: if the server returns classless static routes, we MUST ignore + * the 'static_routes' option. + */ + if (!ip4_process_classless_routes(iface, + options, + route_table, + route_metric, + ip4_config, + &gateway)) + process_classful_routes(iface, options, route_table, route_metric, ip4_config); + + if (gateway) { + _LOG2I(LOGD_DHCP4, iface, " gateway %s", _nm_utils_inet4_ntop(gateway, sbuf)); + gateway_has = TRUE; + } else { + /* If the gateway wasn't provided as a classless static route with a + * subnet length of 0, try to find it using the old-style 'routers' option. + */ + str = g_hash_table_lookup(options, "routers"); + if (str) { + gs_free const char **routers = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = routers; routers && *s; s++) { + /* FIXME: how to handle multiple routers? */ + if (inet_pton(AF_INET, *s, &gateway) > 0) { + _LOG2I(LOGD_DHCP4, iface, " gateway %s", *s); + gateway_has = TRUE; + break; + } else + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid gateway '%s'", *s); + } + } + } + + if (gateway_has) { + const NMPlatformIP4Route r = { + .rt_source = NM_IP_CONFIG_SOURCE_DHCP, + .gateway = gateway, + .table_coerced = nm_platform_route_table_coerce(route_table), + .metric = route_metric, + }; + + nm_ip4_config_add_route(ip4_config, &r, NULL); + } + + str = g_hash_table_lookup(options, "dhcp_lease_time"); + if (str) { + address.lifetime = address.preferred = strtoul(str, NULL, 10); + _LOG2I(LOGD_DHCP4, iface, " lease time %u", address.lifetime); + } + + address.addr_source = NM_IP_CONFIG_SOURCE_DHCP; + nm_ip4_config_add_address(ip4_config, &address); + + str = g_hash_table_lookup(options, "host_name"); + if (str) + _LOG2I(LOGD_DHCP4, iface, " hostname '%s'", str); + + str = g_hash_table_lookup(options, "domain_name_servers"); + if (str) { + gs_free const char **dns = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = dns; dns && *s; s++) { + if (inet_pton(AF_INET, *s, &tmp_addr) > 0) { + if (tmp_addr) { + nm_ip4_config_add_nameserver(ip4_config, tmp_addr); + _LOG2I(LOGD_DHCP4, iface, " nameserver '%s'", *s); + } + } else + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid nameserver '%s'", *s); + } + } + + str = g_hash_table_lookup(options, "domain_name"); + if (str) { + gs_free const char **domains = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = domains; domains && *s; s++) { + _LOG2I(LOGD_DHCP4, iface, " domain name '%s'", *s); + nm_ip4_config_add_domain(ip4_config, *s); + } + } + + str = g_hash_table_lookup(options, "domain_search"); + if (str) + process_domain_search(iface, str, ip4_add_domain_search, ip4_config); + + str = g_hash_table_lookup(options, "netbios_name_servers"); + if (str) { + gs_free const char **nbns = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = nbns; nbns && *s; s++) { + if (inet_pton(AF_INET, *s, &tmp_addr) > 0) { + if (tmp_addr) { + nm_ip4_config_add_wins(ip4_config, tmp_addr); + _LOG2I(LOGD_DHCP4, iface, " wins '%s'", *s); + } + } else + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid WINS server '%s'", *s); + } + } + + str = g_hash_table_lookup(options, "interface_mtu"); + if (str) { + int int_mtu; + + errno = 0; + int_mtu = strtol(str, NULL, 10); + if (NM_IN_SET(errno, EINVAL, ERANGE)) + return NULL; + + if (int_mtu > 576) + nm_ip4_config_set_mtu(ip4_config, int_mtu, NM_IP_CONFIG_SOURCE_DHCP); + } + + str = g_hash_table_lookup(options, "nis_domain"); + if (str) { + _LOG2I(LOGD_DHCP4, iface, " NIS domain '%s'", str); + nm_ip4_config_set_nis_domain(ip4_config, str); + } + + str = g_hash_table_lookup(options, "nis_servers"); + if (str) { + gs_free const char **nis = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = nis; nis && *s; s++) { + if (inet_pton(AF_INET, *s, &tmp_addr) > 0) { + if (tmp_addr) { + nm_ip4_config_add_nis_server(ip4_config, tmp_addr); + _LOG2I(LOGD_DHCP4, iface, " nis '%s'", *s); + } + } else + _LOG2W(LOGD_DHCP4, iface, "ignoring invalid NIS server '%s'", *s); + } + } + + str = g_hash_table_lookup(options, "vendor_encapsulated_options"); + nm_ip4_config_set_metered(ip4_config, str && strstr(str, "ANDROID_METERED")); + + return g_steal_pointer(&ip4_config); +} + +/*****************************************************************************/ + +static void +ip6_add_domain_search(gpointer data, gpointer user_data) +{ + nm_ip6_config_add_search(NM_IP6_CONFIG(user_data), (const char *) data); +} + +NMPlatformIP6Address +nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options) +{ + gs_strfreev char ** split_addr = NULL; + NMPlatformIP6Address address = { + 0, + }; + struct in6_addr tmp_addr; + char * str = NULL; + int prefix; + + g_return_val_if_fail(options != NULL, address); + + str = g_hash_table_lookup(options, "ip6_prefix"); + if (!str) + return address; + + split_addr = g_strsplit(str, "/", 2); + if (split_addr[0] == NULL && split_addr[1] == NULL) { + nm_log_warn(LOGD_DHCP6, "DHCP returned prefix without length '%s'", str); + return address; + } + + if (!inet_pton(AF_INET6, split_addr[0], &tmp_addr)) { + nm_log_warn(LOGD_DHCP6, "DHCP returned invalid prefix '%s'", str); + return address; + } + + prefix = _nm_utils_ascii_str_to_int64(split_addr[1], 10, 0, 128, -1); + if (prefix < 0) { + nm_log_warn(LOGD_DHCP6, "DHCP returned prefix with invalid length '%s'", str); + return address; + } + + address.address = tmp_addr; + address.addr_source = NM_IP_CONFIG_SOURCE_DHCP; + address.plen = prefix; + address.timestamp = nm_utils_get_monotonic_timestamp_sec(); + + str = g_hash_table_lookup(options, "max_life"); + if (str) + address.lifetime = strtoul(str, NULL, 10); + + str = g_hash_table_lookup(options, "preferred_life"); + if (str) + address.preferred = strtoul(str, NULL, 10); + + return address; +} + +NMIP6Config * +nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx, + int ifindex, + const char * iface, + GHashTable * options, + gboolean info_only) +{ + gs_unref_object NMIP6Config *ip6_config = NULL; + struct in6_addr tmp_addr; + NMPlatformIP6Address address; + char * str = NULL; + + g_return_val_if_fail(options != NULL, NULL); + + memset(&address, 0, sizeof(address)); + address.plen = 128; + address.timestamp = nm_utils_get_monotonic_timestamp_sec(); + + ip6_config = nm_ip6_config_new(multi_idx, ifindex); + + str = g_hash_table_lookup(options, "max_life"); + if (str) { + address.lifetime = strtoul(str, NULL, 10); + _LOG2I(LOGD_DHCP6, iface, " valid_lft %u", address.lifetime); + } + + str = g_hash_table_lookup(options, "preferred_life"); + if (str) { + address.preferred = strtoul(str, NULL, 10); + _LOG2I(LOGD_DHCP6, iface, " preferred_lft %u", address.preferred); + } + + str = g_hash_table_lookup(options, "ip6_address"); + if (str) { + if (!inet_pton(AF_INET6, str, &tmp_addr)) { + _LOG2W(LOGD_DHCP6, iface, "(%s): DHCP returned invalid address '%s'", iface, str); + return NULL; + } + + address.address = tmp_addr; + address.addr_source = NM_IP_CONFIG_SOURCE_DHCP; + nm_ip6_config_add_address(ip6_config, &address); + _LOG2I(LOGD_DHCP6, iface, " address %s", str); + } else if (info_only == FALSE) { + /* No address in Managed mode is a hard error */ + return NULL; + } + + str = g_hash_table_lookup(options, "host_name"); + if (str) + _LOG2I(LOGD_DHCP6, iface, " hostname '%s'", str); + + str = g_hash_table_lookup(options, "dhcp6_name_servers"); + if (str) { + gs_free const char **dns = nm_utils_strsplit_set(str, " "); + const char ** s; + + for (s = dns; dns && *s; s++) { + if (inet_pton(AF_INET6, *s, &tmp_addr) > 0) { + if (!IN6_IS_ADDR_UNSPECIFIED(&tmp_addr)) { + nm_ip6_config_add_nameserver(ip6_config, &tmp_addr); + _LOG2I(LOGD_DHCP6, iface, " nameserver '%s'", *s); + } + } else + _LOG2W(LOGD_DHCP6, iface, "ignoring invalid nameserver '%s'", *s); + } + } + + str = g_hash_table_lookup(options, "dhcp6_domain_search"); + if (str) + process_domain_search(iface, str, ip6_add_domain_search, ip6_config); + + return g_steal_pointer(&ip6_config); +} + +char * +nm_dhcp_utils_duid_to_string(GBytes *duid) +{ + gconstpointer data; + gsize len; + + g_return_val_if_fail(duid, NULL); + + data = g_bytes_get_data(duid, &len); + return nm_utils_bin2hexstr_full(data, len, ':', FALSE, NULL); +} + +/** + * nm_dhcp_utils_client_id_string_to_bytes: + * @client_id: the client ID string + * + * Accepts either a hex string ("aa:bb:cc") representing a binary client ID + * (the first byte is assumed to be the 'type' field per RFC 2132 section 9.14), + * or a string representing a non-hardware-address client ID, in which case + * the 'type' field is set to 0. + * + * Returns: the binary client ID suitable for sending over the wire + * to the DHCP server. + */ +GBytes * +nm_dhcp_utils_client_id_string_to_bytes(const char *client_id) +{ + GBytes *bytes = NULL; + guint len; + char * c; + + g_return_val_if_fail(client_id && client_id[0], NULL); + + /* Try as hex encoded */ + if (strchr(client_id, ':')) { + bytes = nm_utils_hexstr2bin(client_id); + + /* the result must be at least two bytes long, + * because @client_id contains a delimiter + * but nm_utils_hexstr2bin() does not allow + * leading nor trailing delimiters. */ + nm_assert(!bytes || g_bytes_get_size(bytes) >= 2); + } + if (!bytes) { + /* Fall back to string */ + len = strlen(client_id); + c = g_malloc(len + 1); + c[0] = 0; /* type: non-hardware address per RFC 2132 section 9.14 */ + memcpy(c + 1, client_id, len); + bytes = g_bytes_new_take(c, len + 1); + } + + return bytes; +} + +/** + * nm_dhcp_utils_get_leasefile_path: + * @addr_family: the IP address family + * @plugin_name: the name of the plugin part of the lease file name + * @iface: the interface name to which the lease relates to + * @uuid: uuid of the connection to which the lease relates to + * @out_leasefile_path: will store the computed lease file path + * + * Constructs the lease file name on the basis of the calling plugin, + * interface name and connection uuid. Then returns in @out_leasefile_path + * the full path of the lease filename. + * + * Returns: TRUE if the lease file already exists, FALSE otherwise. + */ +gboolean +nm_dhcp_utils_get_leasefile_path(int addr_family, + const char *plugin_name, + const char *iface, + const char *uuid, + char ** out_leasefile_path) +{ + gs_free char *rundir_path = NULL; + gs_free char *statedir_path = NULL; + + rundir_path = g_strdup_printf(NMRUNDIR "/%s%s-%s-%s.lease", + plugin_name, + addr_family == AF_INET6 ? "6" : "", + uuid, + iface); + + if (g_file_test(rundir_path, G_FILE_TEST_EXISTS)) { + *out_leasefile_path = g_steal_pointer(&rundir_path); + return TRUE; + } + + statedir_path = g_strdup_printf(NMSTATEDIR "/%s%s-%s-%s.lease", + plugin_name, + addr_family == AF_INET6 ? "6" : "", + uuid, + iface); + + if (g_file_test(statedir_path, G_FILE_TEST_EXISTS)) { + *out_leasefile_path = g_steal_pointer(&statedir_path); + return TRUE; + } + + if (nm_config_get_configure_and_quit(nm_config_get()) == NM_CONFIG_CONFIGURE_AND_QUIT_INITRD) + *out_leasefile_path = g_steal_pointer(&rundir_path); + else + *out_leasefile_path = g_steal_pointer(&statedir_path); + return FALSE; +} + +char * +nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease) +{ + const char *start; + const char *iaid; + + if (!lease) + return NULL; + + iaid = g_hash_table_lookup(lease, "iaid"); + if (!iaid) + return NULL; + + start = g_hash_table_lookup(lease, "life_starts"); + if (!start) + return NULL; + + return g_strdup_printf("%s|%s", iaid, start); +} + +/*****************************************************************************/ + +gboolean +nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, uint16_t *out_val) +{ + if (n_data != 2) + return FALSE; + + *out_val = unaligned_read_be16(data); + return TRUE; +} + +gboolean +nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, uint16_t *out_val) +{ + uint16_t mtu; + + if (!nm_dhcp_lease_data_parse_u16(data, n_data, &mtu)) + return FALSE; + + if (mtu < 68) { + /* https://tools.ietf.org/html/rfc2132#section-5.1: + * + * The minimum legal value for the MTU is 68. */ + return FALSE; + } + + *out_val = mtu; + return TRUE; +} + +gboolean +nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len) +{ + /* WARNING: this function only validates that the string does not contain + * NUL characters (and ignores trailing NULs). It does not check character + * encoding! */ + + while (n_data > 0 && data[n_data - 1] == '\0') + n_data--; + + if (n_data > 0) { + if (memchr(data, n_data, '\0')) { + /* we accept trailing NUL, but none in between. + * + * https://tools.ietf.org/html/rfc2132#section-2 + * https://github.com/systemd/systemd/issues/1337 */ + return FALSE; + } + } + + NM_SET_OUT(out_new_len, n_data); + return TRUE; +} + +char * +nm_dhcp_lease_data_parse_domain_validate(const char *str) +{ + gs_free char *s = NULL; + + s = nm_sd_dns_name_normalize(str); + if (!s) + return NULL; + + if (nm_str_is_empty(s) || (s[0] == '.' && s[1] == '\0')) { + /* root domains are not allowed. */ + return NULL; + } + + if (nm_utils_is_localhost(s)) + return NULL; + + if (!g_utf8_validate(s, -1, NULL)) { + /* the result must be valid UTF-8. */ + return NULL; + } + + return g_steal_pointer(&s); +} + +gboolean +nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val) +{ + gs_free char *str1_free = NULL; + const char * str1; + gs_free char *s = NULL; + + /* this is mostly the same as systemd's lease_parse_domain(). */ + + if (!nm_dhcp_lease_data_parse_cstr(data, n_data, &n_data)) + return FALSE; + + if (n_data == 0) { + /* empty domains are rejected. See + * https://tools.ietf.org/html/rfc2132#section-3.14 + * https://tools.ietf.org/html/rfc2132#section-3.17 + * + * Its minimum length is 1. + * + * Note that this is *after* we potentially stripped trailing NULs. + */ + return FALSE; + } + + str1 = nm_strndup_a(300, (char *) data, n_data, &str1_free); + + s = nm_dhcp_lease_data_parse_domain_validate(str1); + if (!s) + return FALSE; + + *out_val = g_steal_pointer(&s); + return TRUE; +} + +gboolean +nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val) +{ + /* - option 1, https://tools.ietf.org/html/rfc2132#section-3.3 + * - option 28, https://tools.ietf.org/html/rfc2132#section-5.3 + */ + + if (n_data != 4) + return FALSE; + + *out_val = unaligned_read_ne32(data); + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +lease_option_print_label(NMStrBuf *sbuf, size_t n_label, const uint8_t **datap, size_t *n_datap) +{ + gsize i; + + for (i = 0; i < n_label; ++i) { + uint8_t c = 0; + + if (!nm_dhcp_lease_data_consume(datap, n_datap, &c, sizeof(c))) + return FALSE; + + switch (c) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '0' ... '9': + case '-': + case '_': + nm_str_buf_append_c(sbuf, c); + break; + case '.': + case '\\': + nm_str_buf_append_c2(sbuf, '\\', c); + break; + default: + nm_str_buf_append_printf(sbuf, "\\%3d", c); + } + } + + return TRUE; +} + +static char * +lease_option_print_domain_name(const uint8_t * cache, + size_t * n_cachep, + const uint8_t **datap, + size_t * n_datap) +{ + nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, FALSE); + const uint8_t * domain; + size_t n_domain; + size_t n_cache = *n_cachep; + const uint8_t ** domainp = datap; + size_t * n_domainp = n_datap; + gboolean first = TRUE; + uint8_t c; + + /* + * We are given two adjacent memory regions. The @cache contains alreday parsed + * domain names, and the @datap contains the remaining data to parse. + * + * A domain name is formed from a sequence of labels. Each label start with + * a length byte, where the two most significant bits are unset. A zero-length + * label indicates the end of the domain name. + * + * Alternatively, a label can be followed by an offset (indicated by the two + * most significant bits being set in the next byte that is read). The offset + * is an offset into the cache, where the next label of the domain name can + * be found. + * + * Note, that each time a jump to an offset is performed, the size of the + * cache shrinks, so this is guaranteed to terminate. + */ + if (cache + n_cache != *datap) + return NULL; + + for (;;) { + if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c))) + return NULL; + + switch (c & 0xC0) { + case 0x00: /* label length */ + { + size_t n_label = c; + + if (n_label == 0) { + /* + * We reached the final label of the domain name. Adjust + * the cache to include the consumed data, and return. + */ + *n_cachep = *datap - cache; + return nm_str_buf_finalize(&sbuf, NULL); + } + + if (!first) + nm_str_buf_append_c(&sbuf, '.'); + else + first = FALSE; + + if (!lease_option_print_label(&sbuf, n_label, domainp, n_domainp)) + return NULL; + + break; + } + case 0xC0: /* back pointer */ + { + size_t offset = (c & 0x3F) << 16; + + /* + * The offset is given as two bytes (in big endian), where the + * two high bits are masked out. + */ + + if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c))) + return NULL; + + offset += c; + + if (offset >= n_cache) + return NULL; + + domain = cache + offset; + n_domain = n_cache - offset; + n_cache = offset; + + domainp = &domain; + n_domainp = &n_domain; + + break; + } + default: + return NULL; + } + } +} + +char ** +nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data) +{ + GPtrArray * array = NULL; + const guint8 *cache = data; + gsize n_cache = 0; + + for (;;) { + gs_free char *s = NULL; + + s = lease_option_print_domain_name(cache, &n_cache, &data, &n_data); + if (!s) + break; + + if (!array) + array = g_ptr_array_new(); + + g_ptr_array_add(array, g_steal_pointer(&s)); + } + + if (!array) + return NULL; + + g_ptr_array_add(array, NULL); + return (char **) g_ptr_array_free(array, FALSE); +} diff --git a/src/core/dhcp/nm-dhcp-utils.h b/src/core/dhcp/nm-dhcp-utils.h new file mode 100644 index 00000000..69715f90 --- /dev/null +++ b/src/core/dhcp/nm-dhcp-utils.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2014 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_UTILS_H__ +#define __NETWORKMANAGER_DHCP_UTILS_H__ + +#include <stdlib.h> + +#include "nm-ip4-config.h" +#include "nm-ip6-config.h" + +NMIP4Config *nm_dhcp_utils_ip4_config_from_options(struct _NMDedupMultiIndex *multi_idx, + int ifindex, + const char * iface, + GHashTable * options, + guint32 route_table, + guint32 route_metric); + +NMIP6Config *nm_dhcp_utils_ip6_config_from_options(struct _NMDedupMultiIndex *multi_idx, + int ifindex, + const char * iface, + GHashTable * options, + gboolean info_only); + +NMPlatformIP6Address nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options); + +char *nm_dhcp_utils_duid_to_string(GBytes *duid); + +GBytes *nm_dhcp_utils_client_id_string_to_bytes(const char *client_id); + +gboolean nm_dhcp_utils_get_leasefile_path(int addr_family, + const char *plugin_name, + const char *iface, + const char *uuid, + char ** out_leasefile_path); + +char *nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease); + +/*****************************************************************************/ + +static inline gboolean +nm_dhcp_lease_data_consume(const uint8_t **datap, size_t *n_datap, void *out, size_t n_out) +{ + if (*n_datap < n_out) + return FALSE; + + memcpy(out, *datap, n_out); + *datap += n_out; + *n_datap -= n_out; + return TRUE; +} + +static inline gboolean +nm_dhcp_lease_data_consume_in_addr(const uint8_t **datap, size_t *n_datap, in_addr_t *addrp) +{ + return nm_dhcp_lease_data_consume(datap, n_datap, addrp, sizeof(struct in_addr)); +} + +char *nm_dhcp_lease_data_parse_domain_validate(const char *str); + +gboolean nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, guint16 *out_val); +gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, guint16 *out_val); +gboolean nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len); +gboolean nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val); +gboolean nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val); +char ** nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data); + +#endif /* __NETWORKMANAGER_DHCP_UTILS_H__ */ diff --git a/src/core/dhcp/tests/meson.build b/src/core/dhcp/tests/meson.build new file mode 100644 index 00000000..e43c8cab --- /dev/null +++ b/src/core/dhcp/tests/meson.build @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +test_units = [ + 'test-dhcp-dhclient', + 'test-dhcp-utils', +] + +foreach test_unit: test_units + exe = executable( + test_unit, + test_unit + '.c', + dependencies: libNetworkManagerTest_dep, + c_args: test_c_flags, + ) + + test( + 'dhcp/' + test_unit, + test_script, + args: test_args + [exe.full_path()], + timeout: default_test_timeout, + ) +endforeach diff --git a/src/core/dhcp/tests/test-dhclient-commented-duid.leases b/src/core/dhcp/tests/test-dhclient-commented-duid.leases new file mode 100644 index 00000000..3e46ae7d --- /dev/null +++ b/src/core/dhcp/tests/test-dhclient-commented-duid.leases @@ -0,0 +1,2 @@ +#default-duid "\000\001\000\001\030y\246\023`g \354Lp"; + diff --git a/src/core/dhcp/tests/test-dhclient-duid.leases b/src/core/dhcp/tests/test-dhclient-duid.leases new file mode 100644 index 00000000..229331d4 --- /dev/null +++ b/src/core/dhcp/tests/test-dhclient-duid.leases @@ -0,0 +1,2 @@ +default-duid "\000\001\000\001\030y\246\023`g \354Lp"; + diff --git a/src/core/dhcp/tests/test-dhcp-dhclient.c b/src/core/dhcp/tests/test-dhcp-dhclient.c new file mode 100644 index 00000000..77626f69 --- /dev/null +++ b/src/core/dhcp/tests/test-dhcp-dhclient.c @@ -0,0 +1,1341 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2010 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <unistd.h> +#include <arpa/inet.h> +#include <linux/rtnetlink.h> + +#include "nm-glib-aux/nm-dedup-multi.h" + +#include "NetworkManagerUtils.h" +#include "dhcp/nm-dhcp-dhclient-utils.h" +#include "dhcp/nm-dhcp-utils.h" +#include "nm-utils.h" +#include "nm-ip4-config.h" +#include "platform/nm-platform.h" + +#include "nm-test-utils-core.h" + +#define TEST_DIR NM_BUILD_SRCDIR "/src/core/dhcp/tests" +#define TEST_MUDURL "https://example.com/mud.json" + +static void +test_config(const char * orig, + const char * expected, + int addr_family, + const char * hostname, + guint32 timeout, + gboolean use_fqdn, + NMDhcpHostnameFlags hostname_flags, + const char * dhcp_client_id, + GBytes * expected_new_client_id, + const char * iface, + const char * anycast_addr, + const char * mud_url) +{ + gs_free char *new = NULL; + gs_unref_bytes GBytes *client_id = NULL; + gs_unref_bytes GBytes *new_client_id = NULL; + + if (dhcp_client_id) { + client_id = nm_dhcp_utils_client_id_string_to_bytes(dhcp_client_id); + g_assert(client_id); + } + + new = nm_dhcp_dhclient_create_config(iface, + addr_family, + client_id, + anycast_addr, + hostname, + timeout, + use_fqdn, + hostname_flags, + mud_url, + NULL, + "/path/to/dhclient.conf", + orig, + &new_client_id); + g_assert(new != NULL); + + if (!nm_streq(new, expected)) { + g_message("\n* OLD ---------------------------------\n" + "%s" + "\n- NEW -----------------------------------\n" + "%s" + "\n+ EXPECTED ++++++++++++++++++++++++++++++\n" + "%s" + "\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + orig, + new, + expected); + } + g_assert_cmpstr(new, ==, expected); + + if (expected_new_client_id) { + g_assert(new_client_id); + g_assert(g_bytes_equal(new_client_id, expected_new_client_id)); + } else + g_assert(new_client_id == NULL); +} + +/*****************************************************************************/ + +static const char *orig_missing_expected = + "# Created by NetworkManager\n" + "\n\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_orig_missing(void) +{ + test_config(NULL, + orig_missing_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *orig_missing_add_mud_url_expected = + "# Created by NetworkManager\n" + "\n" + "option mudurl code 161 = text;\n" + "send mudurl \"https://example.com/mud.json\";\n\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_orig_missing_add_mud_url(void) +{ + test_config(NULL, + orig_missing_add_mud_url_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + TEST_MUDURL); +} + +/*****************************************************************************/ + +static const char *override_client_id_orig = "send dhcp-client-identifier 00:30:04:20:7A:08;\n"; + +static const char *override_client_id_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send dhcp-client-identifier 11:22:33:44:55:66; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_override_client_id(void) +{ + test_config(override_client_id_orig, + override_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "11:22:33:44:55:66", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *quote_client_id_expected = + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier \"\\x00abcd\"; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_quote_client_id(void) +{ + test_config(NULL, + quote_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "abcd", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *quote_client_id_expected_2 = + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier 00:61:5c:62:63; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_quote_client_id_2(void) +{ + test_config(NULL, + quote_client_id_expected_2, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "a\\bc", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *hex_zero_client_id_expected = + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier 00:11:22:33; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_hex_zero_client_id(void) +{ + test_config(NULL, + hex_zero_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "00:11:22:33", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *ascii_client_id_expected = + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier \"\\x00qb:cd:ef:12:34:56\"; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_ascii_client_id(void) +{ + test_config(NULL, + ascii_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "qb:cd:ef:12:34:56", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *hex_single_client_id_expected = + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier ab:cd:0e:12:34:56; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_hex_single_client_id(void) +{ + test_config(NULL, + hex_single_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + "ab:cd:e:12:34:56", + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *existing_hex_client_id_orig = "send dhcp-client-identifier 10:30:04:20:7A:08;\n"; + +static const char *existing_hex_client_id_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send dhcp-client-identifier 10:30:04:20:7A:08;\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_hex_client_id(void) +{ + gs_unref_bytes GBytes *new_client_id = NULL; + const guint8 bytes[] = {0x10, 0x30, 0x04, 0x20, 0x7A, 0x08}; + + new_client_id = g_bytes_new(bytes, sizeof(bytes)); + test_config(existing_hex_client_id_orig, + existing_hex_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + new_client_id, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *existing_escaped_client_id_orig = + "send dhcp-client-identifier \"\\044test\\xfe\";\n"; + +static const char *existing_escaped_client_id_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send dhcp-client-identifier \"\\044test\\xfe\";\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_escaped_client_id(void) +{ + gs_unref_bytes GBytes *new_client_id = NULL; + + new_client_id = g_bytes_new("$test\xfe", 6); + test_config(existing_escaped_client_id_orig, + existing_escaped_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + new_client_id, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +#define EACID "qb:cd:ef:12:34:56" + +static const char *existing_ascii_client_id_orig = + "send dhcp-client-identifier \"\\x00" EACID "\";\n"; + +static const char *existing_ascii_client_id_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send dhcp-client-identifier \"\\x00" EACID "\";\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_ascii_client_id(void) +{ + gs_unref_bytes GBytes *new_client_id = NULL; + char buf[NM_STRLEN(EACID) + 1] = {0}; + + memcpy(buf + 1, EACID, NM_STRLEN(EACID)); + new_client_id = g_bytes_new(buf, sizeof(buf)); + test_config(existing_ascii_client_id_orig, + existing_ascii_client_id_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + new_client_id, + "eth0", + NULL, + NULL); +} +/*****************************************************************************/ + +static const char *fqdn_expected = + "# Created by NetworkManager\n" + "\n" + "send fqdn.fqdn \"foo.bar.com\"; # added by NetworkManager\n" + "send fqdn.encoded on;\n" + "send fqdn.server-update off;\n" + "send fqdn.no-client-update on;\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n\n"; + +static void +test_fqdn(void) +{ + test_config(NULL, + fqdn_expected, + AF_INET, + "foo.bar.com", + 0, + TRUE, + NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED | NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +static const char *fqdn_options_override_orig = + "\n" + "send fqdn.fqdn \"foobar.com\"\n" /* NM must ignore this ... */ + "send fqdn.encoded off;\n" /* ... and honor these */ + "send fqdn.server-update off;\n"; + +static const char *fqdn_options_override_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send fqdn.fqdn \"example2.com\"; # added by NetworkManager\n" + "send fqdn.encoded off;\n" + "send fqdn.server-update on;\n" + "send fqdn.no-client-update off;\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n" + "# FQDN options from /path/to/dhclient.conf\n" + "send fqdn.encoded off;\n" + "send fqdn.server-update off;\n\n"; + +static void +test_fqdn_options_override(void) +{ + test_config(fqdn_options_override_orig, + fqdn_options_override_expected, + AF_INET, + "example2.com", + 0, + NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE, + TRUE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *override_hostname_orig = "send host-name \"foobar\";\n"; + +static const char *override_hostname_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send host-name \"blahblah\"; # added by NetworkManager\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_override_hostname(void) +{ + test_config(override_hostname_orig, + override_hostname_expected, + AF_INET, + "blahblah", + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *override_hostname6_orig = "send fqdn.fqdn \"foobar\";\n"; + +static const char *override_hostname6_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send fqdn.fqdn \"blahblah.local\"; # added by NetworkManager\n" + "send fqdn.server-update on;\n" + "\n" + "also request dhcp6.name-servers;\n" + "also request dhcp6.domain-search;\n" + "also request dhcp6.client-id;\n" + "\n"; + +static void +test_override_hostname6(void) +{ + test_config(override_hostname6_orig, + override_hostname6_expected, + AF_INET6, + "blahblah.local", + 0, + TRUE, + NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *nonfqdn_hostname6_expected = + "# Created by NetworkManager\n" + "\n" + "send fqdn.fqdn \"blahblah\"; # added by NetworkManager\n" + "send fqdn.no-client-update on;\n" + "\n" + "also request dhcp6.name-servers;\n" + "also request dhcp6.domain-search;\n" + "also request dhcp6.client-id;\n" + "\n"; + +static void +test_nonfqdn_hostname6(void) +{ + /* Non-FQDN hostname can now be used with dhclient */ + test_config(NULL, + nonfqdn_hostname6_expected, + AF_INET6, + "blahblah", + 0, + TRUE, + NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *existing_alsoreq_orig = "also request something;\n" + "also request another-thing;\n"; + +static const char *existing_alsoreq_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request something;\n" + "also request another-thing;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_alsoreq(void) +{ + test_config(existing_alsoreq_orig, + existing_alsoreq_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *existing_req_orig = "request something;\n" + "also request some-other-thing;\n" + "request another-thing;\n" + "also request yet-another-thing;\n"; + +static const char *existing_req_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "request; # override dhclient defaults\n" + "also request another-thing;\n" + "also request yet-another-thing;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_req(void) +{ + test_config(existing_req_orig, + existing_req_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *existing_multiline_alsoreq_orig = + "also request something another-thing yet-another-thing\n" + " foobar baz blah;\n"; + +static const char *existing_multiline_alsoreq_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request something;\n" + "also request another-thing;\n" + "also request yet-another-thing;\n" + "also request foobar;\n" + "also request baz;\n" + "also request blah;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_existing_multiline_alsoreq(void) +{ + test_config(existing_multiline_alsoreq_orig, + existing_multiline_alsoreq_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static void +test_one_duid(const char *escaped, const guint8 *unescaped, guint len) +{ + gs_unref_bytes GBytes *t1 = NULL; + gs_unref_bytes GBytes *t2 = NULL; + gs_free char * w = NULL; + + t1 = nm_dhcp_dhclient_unescape_duid(escaped); + g_assert(t1); + g_assert(nm_utils_gbytes_equal_mem(t1, unescaped, len)); + + t2 = g_bytes_new(unescaped, len); + w = nm_dhcp_dhclient_escape_duid(t2); + g_assert(w); + g_assert_cmpstr(escaped, ==, w); +} + +static void +test_duids(void) +{ + const guint8 test1_u[] = + {0x00, 0x01, 0x00, 0x01, 0x13, 0x6f, 0x13, 0x6e, 0x00, 0x22, 0xfa, 0x8c, 0xd6, 0xc2}; + const char *test1_s = "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302"; + + const guint8 test2_u[] = + {0x00, 0x01, 0x00, 0x01, 0x17, 0x57, 0xee, 0x39, 0x00, 0x23, 0x15, 0x08, 0x7E, 0xac}; + const char *test2_s = "\\000\\001\\000\\001\\027W\\3569\\000#\\025\\010~\\254"; + + const guint8 test3_u[] = + {0x00, 0x01, 0x00, 0x01, 0x17, 0x58, 0xe8, 0x58, 0x00, 0x23, 0x15, 0x08, 0x7e, 0xac}; + const char *test3_s = "\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254"; + + const guint8 test4_u[] = + {0x00, 0x01, 0x00, 0x01, 0x15, 0xd5, 0x31, 0x97, 0x00, 0x16, 0xeb, 0x04, 0x45, 0x18}; + const char *test4_s = "\\000\\001\\000\\001\\025\\3251\\227\\000\\026\\353\\004E\\030"; + + const char *bad_s = "\\000\\001\\000\\001\\425\\3251\\227\\000\\026\\353\\004E\\030"; + + test_one_duid(test1_s, test1_u, sizeof(test1_u)); + test_one_duid(test2_s, test2_u, sizeof(test2_u)); + test_one_duid(test3_s, test3_u, sizeof(test3_u)); + test_one_duid(test4_s, test4_u, sizeof(test4_u)); + + /* Invalid octal digit */ + g_assert(nm_dhcp_dhclient_unescape_duid(bad_s) == NULL); +} + +static void +test_read_duid_from_leasefile(void) +{ + const guint8 expected[] = + {0x00, 0x01, 0x00, 0x01, 0x18, 0x79, 0xa6, 0x13, 0x60, 0x67, 0x20, 0xec, 0x4c, 0x70}; + gs_unref_bytes GBytes *duid = NULL; + GError * error = NULL; + + duid = nm_dhcp_dhclient_read_duid(TEST_DIR "/test-dhclient-duid.leases", &error); + nmtst_assert_success(duid, error); + + g_assert(nm_utils_gbytes_equal_mem(duid, expected, G_N_ELEMENTS(expected))); +} + +static void +test_read_commented_duid_from_leasefile(void) +{ + GBytes *duid; + GError *error = NULL; + + duid = nm_dhcp_dhclient_read_duid(TEST_DIR "/test-dhclient-commented-duid.leases", &error); + g_assert_no_error(error); + g_assert(duid == NULL); +} + +/*****************************************************************************/ + +static void +_save_duid(const char *path, const guint8 *duid_bin, gsize duid_len) +{ + gs_unref_bytes GBytes *duid = NULL; + GError * error = NULL; + gboolean success; + + g_assert(path); + g_assert(duid_bin); + g_assert(duid_len > 0); + + duid = g_bytes_new(duid_bin, duid_len); + success = nm_dhcp_dhclient_save_duid(path, duid, &error); + nmtst_assert_success(success, error); +} + +static void +test_write_duid(void) +{ + const guint8 duid[] = {000, 001, 000, 001, 027, 'X', 0350, 'X', 0, '#', 025, 010, '~', 0254}; + const char * expected_contents = + "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"; + GError * error = NULL; + gs_free char *contents = NULL; + gboolean success; + const char * path = "test-dhclient-write-duid.leases"; + + _save_duid(path, duid, G_N_ELEMENTS(duid)); + + success = g_file_get_contents(path, &contents, NULL, &error); + nmtst_assert_success(success, error); + + unlink(path); + + g_assert_cmpstr(expected_contents, ==, contents); +} + +static void +test_write_existing_duid(void) +{ + const guint8 duid[] = + {000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302}; + const char *original_contents = + "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"; + const char *expected_contents = + "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n"; + GError * error = NULL; + gs_free char *contents = NULL; + gboolean success; + const char * path = "test-dhclient-write-existing-duid.leases"; + + success = g_file_set_contents(path, original_contents, -1, &error); + nmtst_assert_success(success, error); + + /* Save other DUID; should be overwritten */ + _save_duid(path, duid, G_N_ELEMENTS(duid)); + + /* reread original contents */ + success = g_file_get_contents(path, &contents, NULL, &error); + nmtst_assert_success(success, error); + + unlink(path); + g_assert_cmpstr(expected_contents, ==, contents); +} + +static const guint8 DUID_BIN[] = + {000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302}; +#define DUID "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302" + +static void +test_write_existing_commented_duid(void) +{ +#define ORIG_CONTENTS "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n" + const char * expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS; + GError * error = NULL; + gs_free char *contents = NULL; + gboolean success; + const char * path = "test-dhclient-write-existing-commented-duid.leases"; + + success = g_file_set_contents(path, ORIG_CONTENTS, -1, &error); + nmtst_assert_success(success, error); + + /* Save other DUID; should be saved on top */ + _save_duid(path, DUID_BIN, G_N_ELEMENTS(DUID_BIN)); + + /* reread original contents */ + success = g_file_get_contents(path, &contents, NULL, &error); + nmtst_assert_success(success, error); + + unlink(path); + g_assert_cmpstr(expected_contents, ==, contents); +#undef ORIG_CONTENTS +} + +static void +test_write_existing_multiline_duid(void) +{ +#define ORIG_CONTENTS \ + "### Commented old DUID ###\n" \ + "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n" + const char * expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS; + GError * error = NULL; + gs_free char * contents = NULL; + gboolean success; + nmtst_auto_unlinkfile char *path = + g_strdup("test-dhclient-write-existing-multiline-duid.leases"); + + success = g_file_set_contents(path, ORIG_CONTENTS, -1, &error); + nmtst_assert_success(success, error); + + _save_duid(path, DUID_BIN, G_N_ELEMENTS(DUID_BIN)); + + success = g_file_get_contents(path, &contents, NULL, &error); + nmtst_assert_success(success, error); + + g_assert_cmpstr(expected_contents, ==, contents); +#undef ORIG_CONTENTS +} + +/*****************************************************************************/ + +static const char *interface1_orig = "interface \"eth0\" {\n" + "\talso request my-option;\n" + "\tinitial-delay 5;\n" + "}\n" + "interface \"eth1\" {\n" + "\talso request another-option;\n" + "\tinitial-delay 0;\n" + "}\n" + "\n" + "also request yet-another-option;\n"; + +static const char *interface1_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "initial-delay 5;\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "also request my-option;\n" + "also request yet-another-option;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_interface1(void) +{ + test_config(interface1_orig, + interface1_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +static const char *interface2_orig = "interface eth0 {\n" + "\talso request my-option;\n" + "\tinitial-delay 5;\n" + " }\n" + "interface eth1 {\n" + "\tinitial-delay 0;\n" + "\trequest another-option;\n" + " } \n" + "\n" + "also request yet-another-option;\n"; + +static const char *interface2_expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "initial-delay 0;\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "request; # override dhclient defaults\n" + "also request another-option;\n" + "also request yet-another-option;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + +static void +test_interface2(void) +{ + test_config(interface2_orig, + interface2_expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth1", + NULL, + NULL); +} + +static void +test_structured(void) +{ + gs_unref_bytes GBytes *new_client_id = NULL; + const guint8 bytes[] = "sad-and-useless"; + + static const char *const orig = + "interface \"eth0\" { \n" + " send host-name \"useless.example.com\";\n" + " hardware ethernet de:ad:80:86:ba:be;\n" + " send dhcp-client-identifier \"sad-and-useless\";\n" + " script \"/bin/useless\";\n" + " send dhcp-lease-time 8086;\n" + " request subnet-mask, broadcast-address, time-offset, routers,\n" + " domain-search, domain-name, host-name;\n" + " require subnet-mask;\n" + "} \n" + "\n" + " interface \"eth1\" { \n" + " send host-name \"sad.example.com\";\n" + " hardware ethernet de:ca:f6:66:ca:fe;\n" + " send dhcp-client-identifier \"useless-and-miserable\";\n" + " script \"/bin/miserable\";\n" + " send dhcp-lease-time 1337;\n" + " request subnet-mask, broadcast-address, time-offset, routers,\n" + " domain-search, domain-name, domain-name-servers, host-name;\n" + " require subnet-mask, domain-name-servers;\n" + " if not option domain-name = \"example.org\" {\n" + " prepend domain-name-servers 127.0.0.1;\n" + " } else {\n" + " prepend domain-name-servers 127.0.0.2;\n" + " } \n" + " } \n" + "\n" + "pseudo \"secondary\" \"eth0\" { \n" + " send dhcp-client-identifier \"sad-useless-and-secondary\";\n" + " script \"/bin/secondary\";\n" + " send host-name \"secondary.useless.example.com\";\n" + " send dhcp-lease-time 666;\n" + " request routers;\n" + " require routers;\n" + " } \n" + "\n" + " pseudo \"tertiary\" \"eth0\" { \n" + " send dhcp-client-identifier \"sad-useless-and-tertiary\";\n" + " script \"/bin/tertiary\";\n" + " send host-name \"tertiary.useless.example.com\";\n" + "} \n" + "\n" + " alias{ \n" + " interface \"eth0\";\n" + " fixed-address 192.0.2.1;\n" + " option subnet-mask 255.255.255.0;\n" + " } \n" + " lease { \n" + " interface \"eth0\";\n" + " fixed-address 192.0.2.2;\n" + " option subnet-mask 255.255.255.0;\n" + " } \n" + "if not option domain-name = \"example.org\" {\n" + " prepend domain-name-servers 127.0.0.1;\n" + " if not option domain-name = \"useless.example.com\" {\n" + " prepend domain-name-servers 127.0.0.2;\n" + " }\n" + "}\n"; + + static const char *const expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "send host-name \"useless.example.com\";\n" + "hardware ethernet de:ad:80:86:ba:be;\n" + "send dhcp-client-identifier \"sad-and-useless\";\n" + "send dhcp-lease-time 8086;\n" + "require subnet-mask;\n" + "if not option domain-name = \"example.org\" {\n" + "prepend domain-name-servers 127.0.0.1;\n" + "if not option domain-name = \"useless.example.com\" {\n" + "prepend domain-name-servers 127.0.0.2;\n" + "}\n" + "}\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "request; # override dhclient defaults\n" + "also request subnet-mask;\n" + "also request broadcast-address;\n" + "also request time-offset;\n" + "also request routers;\n" + "also request domain-search;\n" + "also request domain-name;\n" + "also request host-name;\n" + "also request rfc3442-classless-static-routes;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request ntp-servers;\n" + "also request root-path;\n" + "\n"; + + new_client_id = g_bytes_new(bytes, sizeof(bytes) - 1); + test_config(orig, + expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + new_client_id, + "eth0", + NULL, + NULL); +} + +static void +test_config_req_intf(void) +{ + static const char *const orig = "request subnet-mask, broadcast-address, routers,\n" + "\trfc3442-classless-static-routes,\n" + "\tinterface-mtu, host-name, domain-name, domain-search,\n" + "\tdomain-name-servers, nis-domain, nis-servers,\n" + "\tnds-context, nds-servers, nds-tree-name,\n" + "\tnetbios-name-servers, netbios-dd-server,\n" + "\tnetbios-node-type, netbios-scope, ntp-servers;\n" + ""; + static const char *const expected = + "# Created by NetworkManager\n" + "# Merged from /path/to/dhclient.conf\n" + "\n" + "\n" + "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n" + "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n" + "option wpad code 252 = string;\n" + "\n" + "request; # override dhclient defaults\n" + "also request subnet-mask;\n" + "also request broadcast-address;\n" + "also request routers;\n" + "also request rfc3442-classless-static-routes;\n" + "also request interface-mtu;\n" + "also request host-name;\n" + "also request domain-name;\n" + "also request domain-search;\n" + "also request domain-name-servers;\n" + "also request nis-domain;\n" + "also request nis-servers;\n" + "also request nds-context;\n" + "also request nds-servers;\n" + "also request nds-tree-name;\n" + "also request netbios-name-servers;\n" + "also request netbios-dd-server;\n" + "also request netbios-node-type;\n" + "also request netbios-scope;\n" + "also request ntp-servers;\n" + "also request ms-classless-static-routes;\n" + "also request static-routes;\n" + "also request wpad;\n" + "also request root-path;\n" + "\n"; + + test_config(orig, + expected, + AF_INET, + NULL, + 0, + FALSE, + NM_DHCP_HOSTNAME_FLAG_NONE, + NULL, + NULL, + "eth0", + NULL, + NULL); +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + nmtst_init_with_logging(&argc, &argv, NULL, "DEFAULT"); + + g_test_add_func("/dhcp/dhclient/orig_missing", test_orig_missing); + g_test_add_func("/dhcp/dhclient/orig_missing_add_mud_url", test_orig_missing_add_mud_url); + g_test_add_func("/dhcp/dhclient/override_client_id", test_override_client_id); + g_test_add_func("/dhcp/dhclient/quote_client_id/1", test_quote_client_id); + g_test_add_func("/dhcp/dhclient/quote_client_id/2", test_quote_client_id_2); + g_test_add_func("/dhcp/dhclient/hex_zero_client_id", test_hex_zero_client_id); + g_test_add_func("/dhcp/dhclient/ascii_client_id", test_ascii_client_id); + g_test_add_func("/dhcp/dhclient/hex_single_client_id", test_hex_single_client_id); + g_test_add_func("/dhcp/dhclient/existing-hex-client-id", test_existing_hex_client_id); + g_test_add_func("/dhcp/dhclient/existing-client-id", test_existing_escaped_client_id); + g_test_add_func("/dhcp/dhclient/existing-ascii-client-id", test_existing_ascii_client_id); + g_test_add_func("/dhcp/dhclient/fqdn", test_fqdn); + g_test_add_func("/dhcp/dhclient/fqdn_options_override", test_fqdn_options_override); + g_test_add_func("/dhcp/dhclient/override_hostname", test_override_hostname); + g_test_add_func("/dhcp/dhclient/override_hostname6", test_override_hostname6); + g_test_add_func("/dhcp/dhclient/nonfqdn_hostname6", test_nonfqdn_hostname6); + g_test_add_func("/dhcp/dhclient/existing_req", test_existing_req); + g_test_add_func("/dhcp/dhclient/existing_alsoreq", test_existing_alsoreq); + g_test_add_func("/dhcp/dhclient/existing_multiline_alsoreq", test_existing_multiline_alsoreq); + g_test_add_func("/dhcp/dhclient/duids", test_duids); + g_test_add_func("/dhcp/dhclient/interface/1", test_interface1); + g_test_add_func("/dhcp/dhclient/interface/2", test_interface2); + g_test_add_func("/dhcp/dhclient/config/req_intf", test_config_req_intf); + g_test_add_func("/dhcp/dhclient/structured", test_structured); + + g_test_add_func("/dhcp/dhclient/read_duid_from_leasefile", test_read_duid_from_leasefile); + g_test_add_func("/dhcp/dhclient/read_commented_duid_from_leasefile", + test_read_commented_duid_from_leasefile); + + g_test_add_func("/dhcp/dhclient/write_duid", test_write_duid); + g_test_add_func("/dhcp/dhclient/write_existing_duid", test_write_existing_duid); + g_test_add_func("/dhcp/dhclient/write_existing_commented_duid", + test_write_existing_commented_duid); + g_test_add_func("/dhcp/dhclient/write_existing_multiline_duid", + test_write_existing_multiline_duid); + + return g_test_run(); +} diff --git a/src/core/dhcp/tests/test-dhcp-utils.c b/src/core/dhcp/tests/test-dhcp-utils.c new file mode 100644 index 00000000..9b54e2cd --- /dev/null +++ b/src/core/dhcp/tests/test-dhcp-utils.c @@ -0,0 +1,826 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2008 - 2014 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <linux/rtnetlink.h> + +#include "nm-glib-aux/nm-dedup-multi.h" +#include "nm-utils.h" + +#include "dhcp/nm-dhcp-utils.h" +#include "dhcp/nm-dhcp-options.h" +#include "platform/nm-platform.h" + +#include "nm-test-utils-core.h" + +/*****************************************************************************/ + +static NMIP4Config * +_ip4_config_from_options(int ifindex, const char *iface, GHashTable *options, guint32 route_metric) +{ + nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = nm_dedup_multi_index_new(); + NMIP4Config * config; + + config = nm_dhcp_utils_ip4_config_from_options(multi_idx, + ifindex, + iface, + options, + RT_TABLE_MAIN, + route_metric); + g_assert(config); + return config; +} + +typedef struct { + const char *name; + const char *value; +} Option; + +static GHashTable * +fill_table(const Option *test_options, GHashTable *table) +{ + const Option *opt; + + if (!table) + table = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, NULL); + for (opt = test_options; opt->name; opt++) + g_hash_table_insert(table, (gpointer) opt->name, (gpointer) opt->value); + return table; +} + +static const Option generic_options[] = { + {"subnet_mask", "255.255.255.0"}, + {"ip_address", "192.168.1.106"}, + {"network_number", "192.168.1.0"}, + {"expiry", "1232324877"}, + {"dhcp_lease_time", "3600"}, + {"dhcp_server_identifier", "192.168.1.1"}, + {"routers", "192.168.1.1"}, + {"domain_name_servers", "216.254.95.2 216.231.41.2"}, + {"dhcp_message_type", "5"}, + {"broadcast_address", "192.168.1.255"}, + {"domain_search", "foobar.com blah.foobar.com"}, + {"host_name", "nmreallywhipsthe"}, + {"domain_name", "lamasass.com"}, + {"interface_mtu", "987"}, + {"static_routes", "10.1.1.5 10.1.1.1 100.99.88.56 10.1.1.1"}, + {NULL, NULL}}; + +static void +test_generic_options(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const NMPlatformIP4Address * address; + const NMPlatformIP4Route * route; + guint32 tmp; + const char * expected_addr = "192.168.1.106"; + const char * expected_gw = "192.168.1.1"; + const char * expected_dns1 = "216.254.95.2"; + const char * expected_dns2 = "216.231.41.2"; + const char * expected_search1 = "foobar.com"; + const char * expected_search2 = "blah.foobar.com"; + const char * expected_route1_dest = "10.1.1.5"; + const char * expected_route1_gw = "10.1.1.1"; + const char * expected_route2_dest = "100.99.88.56"; + const char * expected_route2_gw = "10.1.1.1"; + + options = fill_table(generic_options, NULL); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 address */ + g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1); + address = _nmtst_ip4_config_get_address(ip4_config, 0); + g_assert(inet_pton(AF_INET, expected_addr, &tmp) > 0); + g_assert(address->address == tmp); + g_assert(address->peer_address == tmp); + g_assert_cmpint(address->plen, ==, 24); + + /* Gateway */ + g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0); + g_assert(nmtst_ip4_config_get_gateway(ip4_config) == tmp); + + g_assert_cmpint(nm_ip4_config_get_num_wins(ip4_config), ==, 0); + + g_assert_cmpint(nm_ip4_config_get_mtu(ip4_config), ==, 987); + + /* Domain searches */ + g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 2); + g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 0), ==, expected_search1); + g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 1), ==, expected_search2); + + /* DNS servers */ + g_assert_cmpint(nm_ip4_config_get_num_nameservers(ip4_config), ==, 2); + g_assert(inet_pton(AF_INET, expected_dns1, &tmp) > 0); + g_assert(nm_ip4_config_get_nameserver(ip4_config, 0) == tmp); + g_assert(inet_pton(AF_INET, expected_dns2, &tmp) > 0); + g_assert(nm_ip4_config_get_nameserver(ip4_config, 1) == tmp); + + /* Routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + + /* Route #1 */ + route = _nmtst_ip4_config_get_route(ip4_config, 0); + g_assert(inet_pton(AF_INET, expected_route1_dest, &tmp) > 0); + g_assert(route->network == tmp); + g_assert(inet_pton(AF_INET, expected_route1_gw, &tmp) > 0); + g_assert(route->gateway == tmp); + g_assert_cmpint(route->plen, ==, 32); + g_assert_cmpint(route->metric, ==, 0); + + /* Route #2 */ + route = _nmtst_ip4_config_get_route(ip4_config, 1); + g_assert(route->network == nmtst_inet4_from_string(expected_route2_dest)); + g_assert(route->gateway == nmtst_inet4_from_string(expected_route2_gw)); + g_assert_cmpint(route->plen, ==, 32); + g_assert_cmpint(route->metric, ==, 0); + + route = _nmtst_ip4_config_get_route(ip4_config, 2); + g_assert(route->network == nmtst_inet4_from_string("0.0.0.0")); + g_assert(route->gateway == nmtst_inet4_from_string("192.168.1.1")); + g_assert_cmpint(route->plen, ==, 0); + g_assert_cmpint(route->metric, ==, 0); + + g_hash_table_destroy(options); +} + +static void +test_wins_options(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const NMPlatformIP4Address * address; + guint32 tmp; + const char * expected_wins1 = "63.12.199.5"; + const char * expected_wins2 = "150.4.88.120"; + static const Option data[] = {{"netbios_name_servers", "63.12.199.5 150.4.88.120"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 address */ + g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1); + address = _nmtst_ip4_config_get_address(ip4_config, 0); + g_assert(address); + g_assert_cmpint(nm_ip4_config_get_num_wins(ip4_config), ==, 2); + g_assert(inet_pton(AF_INET, expected_wins1, &tmp) > 0); + g_assert(nm_ip4_config_get_wins(ip4_config, 0) == tmp); + g_assert(inet_pton(AF_INET, expected_wins2, &tmp) > 0); + g_assert(nm_ip4_config_get_wins(ip4_config, 1) == tmp); + + g_hash_table_destroy(options); +} + +static void +test_vendor_option_metered(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + static const Option data[] = {{"vendor_encapsulated_options", "ANDROID_METERED"}, {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_assert(nm_ip4_config_get_metered(ip4_config) == FALSE); + g_hash_table_destroy(options); + g_clear_object(&ip4_config); + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_assert(nm_ip4_config_get_metered(ip4_config) == TRUE); + g_hash_table_destroy(options); +} + +static void +test_parse_search_list(void) +{ + guint8 *data; + char ** domains; + + data = (guint8[]){0x05, 'l', 'o', 'c', 'a', 'l', 0x00}; + domains = nm_dhcp_lease_data_parse_search_list(data, 7); + g_assert(domains); + g_assert_cmpint(g_strv_length(domains), ==, 1); + g_assert_cmpstr(domains[0], ==, "local"); + g_strfreev(domains); + + data = (guint8[]){0x04, 't', 'e', 's', 't', 0x07, 'e', 'x', 'a', 'm', 'p', 'l', + 'e', 0x03, 'c', 'o', 'm', 0x00, 0xc0, 0x05, 0x03, 'a', 'b', 'c', + 0xc0, 0x0d, 0x06, 'f', 'o', 'o', 'b', 'a', 'r', 0x00}; + domains = nm_dhcp_lease_data_parse_search_list(data, 34); + g_assert(domains); + g_assert_cmpint(g_strv_length(domains), ==, 4); + g_assert_cmpstr(domains[0], ==, "test.example.com"); + g_assert_cmpstr(domains[1], ==, "example.com"); + g_assert_cmpstr(domains[2], ==, "abc.com"); + g_assert_cmpstr(domains[3], ==, "foobar"); + g_strfreev(domains); + + data = (guint8[]){ + 0x40, + 'b', + 'a', + 'd', + }; + domains = nm_dhcp_lease_data_parse_search_list(data, 4); + g_assert(!domains); + + data = (guint8[]){ + 0x04, + 'o', + 'k', + 'a', + 'y', + 0x00, + 0x40, + 'b', + 'a', + 'd', + }; + domains = nm_dhcp_lease_data_parse_search_list(data, 10); + g_assert(domains); + g_assert_cmpint(g_strv_length(domains), ==, 1); + g_assert_cmpstr(domains[0], ==, "okay"); + g_strfreev(domains); +} + +static void +ip4_test_route(NMIP4Config *ip4_config, + guint route_num, + const char * expected_dest, + const char * expected_gw, + guint expected_prefix) +{ + const NMPlatformIP4Route *route; + guint32 tmp; + + g_assert(expected_prefix <= 32); + + route = _nmtst_ip4_config_get_route(ip4_config, route_num); + g_assert(inet_pton(AF_INET, expected_dest, &tmp) > 0); + g_assert(route->network == tmp); + g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0); + g_assert(route->gateway == tmp); + g_assert_cmpint(route->plen, ==, expected_prefix); + g_assert_cmpint(route->metric, ==, 0); +} + +static void +ip4_test_gateway(NMIP4Config *ip4_config, const char *expected_gw) +{ + guint32 tmp; + + g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1); + g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0); + g_assert(nmtst_ip4_config_get_gateway(ip4_config) == tmp); +} + +static void +test_classless_static_routes_1(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + const char * expected_route2_dest = "10.0.0.0"; + const char * expected_route2_gw = "10.17.66.41"; + static const Option data[] = { + /* dhclient custom format */ + {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 8 10 10 17 66 41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 8); + ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0); + + g_hash_table_destroy(options); +} + +static void +test_classless_static_routes_2(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + const char * expected_route2_dest = "10.0.0.0"; + const char * expected_route2_gw = "10.17.66.41"; + static const Option data[] = { + /* dhcpcd format */ + {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.0.0/8 10.17.66.41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 8); + ip4_test_route(ip4_config, 2, "0.0.0.0", expected_route1_gw, 0); + + g_hash_table_destroy(options); +} + +static void +test_fedora_dhclient_classless_static_routes(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "129.210.177.128"; + const char * expected_route1_gw = "192.168.0.113"; + const char * expected_route2_dest = "2.0.0.0"; + const char * expected_route2_gw = "10.34.255.6"; + const char * expected_gateway = "192.168.0.113"; + static const Option data[] = { + /* Fedora dhclient format */ + {"classless_static_routes", + "0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 25); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 7); + ip4_test_route(ip4_config, 2, "0.0.0.0", expected_route1_gw, 0); + + /* Gateway */ + ip4_test_gateway(ip4_config, expected_gateway); + + g_hash_table_destroy(options); +} + +static void +test_dhclient_invalid_classless_routes_1(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + static const Option data[] = { + /* dhclient format */ + {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 45 10 17 66 41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0); + + g_hash_table_destroy(options); +} + +static void +test_dhcpcd_invalid_classless_routes_1(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "10.1.1.5"; + const char * expected_route1_gw = "10.1.1.1"; + const char * expected_route2_dest = "100.99.88.56"; + const char * expected_route2_gw = "10.1.1.1"; + static const Option data[] = { + /* dhcpcd format */ + {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.adfadf/44 10.17.66.41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* Test falling back to old-style static routes if the classless static + * routes are invalid. + */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32); + ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0); + + g_hash_table_destroy(options); +} + +static void +test_dhclient_invalid_classless_routes_2(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "10.1.1.5"; + const char * expected_route1_gw = "10.1.1.1"; + const char * expected_route2_dest = "100.99.88.56"; + const char * expected_route2_gw = "10.1.1.1"; + static const Option data[] = { + {"rfc3442_classless_static_routes", "45 10 17 66 41 24 192 168 10 192 168 1 1"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* Test falling back to old-style static routes if the classless static + * routes are invalid. + */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32); + ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0); + + g_hash_table_destroy(options); +} + +static void +test_dhcpcd_invalid_classless_routes_2(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "10.1.1.5"; + const char * expected_route1_gw = "10.1.1.1"; + const char * expected_route2_dest = "100.99.88.56"; + const char * expected_route2_gw = "10.1.1.1"; + static const Option data[] = { + {"classless_static_routes", "10.0.adfadf/44 10.17.66.41 192.168.10.0/24 192.168.1.1"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* Test falling back to old-style static routes if the classless static + * routes are invalid. + */ + + /* Routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32); + ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32); + ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0); + + g_hash_table_destroy(options); +} + +static void +test_dhclient_invalid_classless_routes_3(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + static const Option data[] = { + {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 32 128 10 17 66 41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0); + + g_hash_table_destroy(options); +} + +static void +test_dhcpcd_invalid_classless_routes_3(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + static Option data[] = { + {"classless_static_routes", "192.168.10.0/24 192.168.1.1 128/32 10.17.66.41"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*DHCP provided invalid classless static route*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0); + + g_hash_table_destroy(options); +} + +static void +test_dhclient_gw_in_classless_routes(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + const char * expected_gateway = "192.2.3.4"; + static Option data[] = { + {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 0 192 2 3 4"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, "0.0.0.0", "192.2.3.4", 0); + + /* Gateway */ + ip4_test_gateway(ip4_config, expected_gateway); + + g_hash_table_destroy(options); +} + +static void +test_dhcpcd_gw_in_classless_routes(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_route1_dest = "192.168.10.0"; + const char * expected_route1_gw = "192.168.1.1"; + const char * expected_gateway = "192.2.3.4"; + static Option data[] = { + {"classless_static_routes", "192.168.10.0/24 192.168.1.1 0.0.0.0/0 192.2.3.4"}, + {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* IP4 routes */ + g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2); + ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + ip4_test_route(ip4_config, 1, "0.0.0.0", "192.2.3.4", 0); + + /* Gateway */ + ip4_test_gateway(ip4_config, expected_gateway); + + g_hash_table_destroy(options); +} + +static void +test_escaped_domain_searches(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const char * expected_search0 = "host1"; + const char * expected_search1 = "host2"; + const char * expected_search2 = "host3"; + static const Option data[] = {{"domain_search", "host1\\032host2\\032host3"}, {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + /* domain searches */ + g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 3); + g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 0), ==, expected_search0); + g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 1), ==, expected_search1); + g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 2), ==, expected_search2); + + g_hash_table_destroy(options); +} + +static void +test_invalid_escaped_domain_searches(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + static const Option data[] = {{"domain_search", "host1\\aahost2\\032host3"}, {NULL, NULL}}; + + options = fill_table(generic_options, NULL); + options = fill_table(data, options); + + NMTST_EXPECT_NM_WARN("*invalid domain search*"); + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + g_test_assert_expected_messages(); + + /* domain searches */ + g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 0); + + g_hash_table_destroy(options); +} + +static void +test_ip4_missing_prefix(const char *ip, guint32 expected_prefix) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const NMPlatformIP4Address * address; + + options = fill_table(generic_options, NULL); + g_hash_table_insert(options, "ip_address", (gpointer) ip); + g_hash_table_remove(options, "subnet_mask"); + + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1); + address = _nmtst_ip4_config_get_address(ip4_config, 0); + g_assert(address); + g_assert_cmpint(address->plen, ==, expected_prefix); + + g_hash_table_destroy(options); +} + +static void +test_ip4_missing_prefix_24(void) +{ + test_ip4_missing_prefix("192.168.1.10", 24); +} + +static void +test_ip4_missing_prefix_16(void) +{ + test_ip4_missing_prefix("172.16.54.50", 16); +} + +static void +test_ip4_missing_prefix_8(void) +{ + test_ip4_missing_prefix("10.1.2.3", 8); +} + +static void +test_ip4_prefix_classless(void) +{ + GHashTable * options; + gs_unref_object NMIP4Config *ip4_config = NULL; + const NMPlatformIP4Address * address; + + /* Ensure that the missing-subnet-mask handler doesn't mangle classless + * subnet masks at all. The handler should trigger only if the server + * doesn't send the subnet mask. + */ + + options = fill_table(generic_options, NULL); + g_hash_table_insert(options, "ip_address", "172.16.54.22"); + g_hash_table_insert(options, "subnet_mask", "255.255.252.0"); + + ip4_config = _ip4_config_from_options(1, "eth0", options, 0); + + g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1); + address = _nmtst_ip4_config_get_address(ip4_config, 0); + g_assert(address); + g_assert_cmpint(address->plen, ==, 22); + + g_hash_table_destroy(options); +} + +#define COMPARE_ID(src, is_str, expected, expected_len) \ + G_STMT_START \ + { \ + gs_unref_bytes GBytes *b = NULL; \ + const char * p; \ + gsize l; \ + \ + b = nm_dhcp_utils_client_id_string_to_bytes(src); \ + g_assert(b); \ + p = g_bytes_get_data(b, &l); \ + if (is_str) { \ + g_assert_cmpint(l, ==, expected_len + 1); \ + g_assert_cmpint(((const char *) p)[0], ==, 0); \ + g_assert(memcmp(p + 1, expected, expected_len) == 0); \ + } else { \ + g_assert_cmpint(l, ==, expected_len); \ + g_assert(memcmp(p, expected, expected_len) == 0); \ + } \ + } \ + G_STMT_END + +static void +test_client_id_from_string(void) +{ + const char * nothex = "asdfasdfasdfasdfasdfasdfasdf"; + const char * allhex = "00:11:22:33:4:55:66:77:88"; + const guint8 allhex_bin[] = {0x00, 0x11, 0x22, 0x33, 0x04, 0x55, 0x66, 0x77, 0x88}; + const char * somehex = "00:11:22:33:44:55:asdfasdfasdf:99:10"; + const char * nocolons = "0011223344559910"; + const char * endcolon = "00:11:22:33:44:55:"; + + COMPARE_ID(nothex, TRUE, nothex, strlen(nothex)); + COMPARE_ID(allhex, FALSE, allhex_bin, sizeof(allhex_bin)); + COMPARE_ID(somehex, TRUE, somehex, strlen(somehex)); + COMPARE_ID(nocolons, TRUE, nocolons, strlen(nocolons)); + COMPARE_ID(endcolon, TRUE, endcolon, strlen(endcolon)); +} + +/*****************************************************************************/ + +static void +test_dhcp_opt_list(gconstpointer test_data) +{ + const gboolean IS_IPv4 = (GPOINTER_TO_INT(test_data) == 0); + const int addr_family = IS_IPv4 ? AF_INET : AF_INET6; + const NMDhcpOption *const options = + IS_IPv4 ? _nm_dhcp_option_dhcp4_options : _nm_dhcp_option_dhcp6_options; + const guint n = (IS_IPv4 ? G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options) + : G_N_ELEMENTS(_nm_dhcp_option_dhcp6_options)); + guint i; + guint j; + + g_assert(options); + g_assert(n > 0); + + for (i = 0; i < n; i++) { + const NMDhcpOption *const opt = &options[i]; + + g_assert_cmpstr(opt->name, !=, NULL); + g_assert(NM_STR_HAS_PREFIX(opt->name, NM_DHCP_OPTION_REQPREFIX)); + + for (j = 0; j < i; j++) { + const NMDhcpOption *const opt2 = &options[j]; + + g_assert_cmpstr(opt->name, !=, opt2->name); + g_assert_cmpint(opt->option_num, !=, opt2->option_num); + } + } + + for (i = 0; i < n; i++) { + const NMDhcpOption *const opt = &options[i]; + + g_assert(opt == nm_dhcp_option_find(addr_family, opt->option_num)); + } +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + nmtst_init_assert_logging(&argc, &argv, "WARN", "DEFAULT"); + + g_test_add_func("/dhcp/generic-options", test_generic_options); + g_test_add_func("/dhcp/wins-options", test_wins_options); + g_test_add_func("/dhcp/classless-static-routes-1", test_classless_static_routes_1); + g_test_add_func("/dhcp/classless-static-routes-2", test_classless_static_routes_2); + g_test_add_func("/dhcp/fedora-dhclient-classless-static-routes", + test_fedora_dhclient_classless_static_routes); + g_test_add_func("/dhcp/dhclient-invalid-classless-routes-1", + test_dhclient_invalid_classless_routes_1); + g_test_add_func("/dhcp/dhcpcd-invalid-classless-routes-1", + test_dhcpcd_invalid_classless_routes_1); + g_test_add_func("/dhcp/dhclient-invalid-classless-routes-2", + test_dhclient_invalid_classless_routes_2); + g_test_add_func("/dhcp/dhcpcd-invalid-classless-routes-2", + test_dhcpcd_invalid_classless_routes_2); + g_test_add_func("/dhcp/dhclient-invalid-classless-routes-3", + test_dhclient_invalid_classless_routes_3); + g_test_add_func("/dhcp/dhcpcd-invalid-classless-routes-3", + test_dhcpcd_invalid_classless_routes_3); + g_test_add_func("/dhcp/dhclient-gw-in-classless-routes", test_dhclient_gw_in_classless_routes); + g_test_add_func("/dhcp/dhcpcd-gw-in-classless-routes", test_dhcpcd_gw_in_classless_routes); + g_test_add_func("/dhcp/escaped-domain-searches", test_escaped_domain_searches); + g_test_add_func("/dhcp/invalid-escaped-domain-searches", test_invalid_escaped_domain_searches); + g_test_add_func("/dhcp/ip4-missing-prefix-24", test_ip4_missing_prefix_24); + g_test_add_func("/dhcp/ip4-missing-prefix-16", test_ip4_missing_prefix_16); + g_test_add_func("/dhcp/ip4-missing-prefix-8", test_ip4_missing_prefix_8); + g_test_add_func("/dhcp/ip4-prefix-classless", test_ip4_prefix_classless); + g_test_add_func("/dhcp/client-id-from-string", test_client_id_from_string); + g_test_add_func("/dhcp/vendor-option-metered", test_vendor_option_metered); + g_test_add_func("/dhcp/parse-search-list", test_parse_search_list); + g_test_add_data_func("/dhcp/test_dhcp_opt_list/IPv4", GINT_TO_POINTER(0), test_dhcp_opt_list); + g_test_add_data_func("/dhcp/test_dhcp_opt_list/IPv6", GINT_TO_POINTER(1), test_dhcp_opt_list); + + return g_test_run(); +} |