diff options
| author | Michael Biebl <biebl@debian.org> | 2017-01-17 20:25:09 +0100 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2017-01-17 20:25:09 +0100 |
| commit | 58f8be580039b0575b197b9573a1c92745d96d30 (patch) | |
| tree | 2c226233f623a0dcb529be0eb8cdf97e4a2ae0c0 /src/dhcp | |
| parent | 45cb5bb3c0e6edb887cf69b417fcaf7053814a9b (diff) | |
New upstream version 1.5.90 upstream/1.5.90
Diffstat (limited to 'src/dhcp')
24 files changed, 7871 insertions, 0 deletions
diff --git a/src/dhcp/nm-dhcp-client-logging.h b/src/dhcp/nm-dhcp-client-logging.h new file mode 100644 index 00000000..8dd18bf2 --- /dev/null +++ b/src/dhcp/nm-dhcp-client-logging.h @@ -0,0 +1,77 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* NetworkManager -- Network link manager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2016 Red Hat, Inc. + */ + +#ifndef __NETWORKMANAGER_DHCP_CLIENT_LOGGING_H__ +#define __NETWORKMANAGER_DHCP_CLIENT_LOGGING_H__ + +#include "nm-dhcp-client.h" + +#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 = !_self \ + ? LOGD_DHCP \ + : (nm_dhcp_client_get_ipv6 (_self) ? LOGD_DHCP6 : LOGD_DHCP4); \ + \ + nm_log (_level, _domain, \ + "%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, \ + "%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/dhcp/nm-dhcp-client.c b/src/dhcp/nm-dhcp-client.c new file mode 100644 index 00000000..ba7c6dbf --- /dev/null +++ b/src/dhcp/nm-dhcp-client.c @@ -0,0 +1,997 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2005 - 2010 Red Hat, Inc. + * + */ + +#include "nm-default.h" + +#include "nm-dhcp-client.h" + +#include <string.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <errno.h> +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <uuid/uuid.h> + +#include "NetworkManagerUtils.h" +#include "nm-utils.h" +#include "nm-dhcp-utils.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_BASE ( + PROP_IFACE, + PROP_IFINDEX, + PROP_HWADDR, + PROP_IPV6, + PROP_UUID, + PROP_PRIORITY, + PROP_TIMEOUT, +); + +typedef struct _NMDhcpClientPrivate { + char * iface; + int ifindex; + GByteArray * hwaddr; + gboolean ipv6; + char * uuid; + guint32 priority; + guint32 timeout; + GByteArray * duid; + GBytes * client_id; + char * hostname; + char * fqdn; + + NMDhcpState state; + pid_t pid; + guint timeout_id; + guint watch_id; + gboolean info_only; +} NMDhcpClientPrivate; + +G_DEFINE_TYPE_EXTENDED (NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT, G_TYPE_FLAG_ABSTRACT, {}) + +#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; +} + +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; +} + +gboolean +nm_dhcp_client_get_ipv6 (NMDhcpClient *self) +{ + g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), FALSE); + + return NM_DHCP_CLIENT_GET_PRIVATE (self)->ipv6; +} + +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; +} + +const GByteArray * +nm_dhcp_client_get_duid (NMDhcpClient *self) +{ + g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE (self)->duid; +} + +const GByteArray * +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; +} + +guint32 +nm_dhcp_client_get_priority (NMDhcpClient *self) +{ + g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), G_MAXUINT32); + + return NM_DHCP_CLIENT_GET_PRIVATE (self)->priority; +} + +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; +} + +void +nm_dhcp_client_set_client_id (NMDhcpClient *self, GBytes *client_id) +{ + NMDhcpClientPrivate *priv; + + g_return_if_fail (NM_IS_DHCP_CLIENT (self)); + + priv = NM_DHCP_CLIENT_GET_PRIVATE (self); + + if (priv->client_id && client_id && g_bytes_equal (priv->client_id, client_id)) + return; + g_clear_pointer (&priv->client_id, g_bytes_unref); + priv->client_id = client_id ? g_bytes_ref (client_id) : NULL; +} + +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; +} + +const char * +nm_dhcp_client_get_fqdn (NMDhcpClient *self) +{ + g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL); + + return NM_DHCP_CLIENT_GET_PRIVATE (self)->fqdn; +} + +/*****************************************************************************/ + +static const char *state_table[NM_DHCP_STATE_MAX + 1] = { + [NM_DHCP_STATE_UNKNOWN] = "unknown", + [NM_DHCP_STATE_BOUND] = "bound", + [NM_DHCP_STATE_TIMEOUT] = "timeout", + [NM_DHCP_STATE_EXPIRE] = "expire", + [NM_DHCP_STATE_DONE] = "done", + [NM_DHCP_STATE_FAIL] = "fail", +}; + +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 || + 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_BOUND; + 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) + return NM_DHCP_STATE_DONE; + else if (g_ascii_strcasecmp (reason, "fail") == 0 || + g_ascii_strcasecmp (reason, "abend") == 0) + return NM_DHCP_STATE_FAIL; + + _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 ? name : "dhcp-client", NULL, + 1000 / 2, 1000 / 20); + g_free (name); +} + +static void +stop (NMDhcpClient *self, gboolean release, const GByteArray *duid) +{ + 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; + priv->info_only = FALSE; +} + +void +nm_dhcp_client_set_state (NMDhcpClient *self, + NMDhcpState new_state, + GObject *ip_config, + GHashTable *options) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self); + gs_free char *event_id = NULL; + + if (new_state >= NM_DHCP_STATE_BOUND) + timeout_cleanup (self); + if (new_state >= NM_DHCP_STATE_TIMEOUT) + watch_cleanup (self); + + if (new_state == NM_DHCP_STATE_BOUND) { + g_assert ( (priv->ipv6 && NM_IS_IP6_CONFIG (ip_config)) + || (!priv->ipv6 && NM_IS_IP4_CONFIG (ip_config))); + g_assert (options); + g_assert_cmpint (g_hash_table_size (options), >, 0); + } else { + g_assert (ip_config == NULL); + g_assert (options == NULL); + } + + /* The client may send same-state transitions for RENEW/REBIND events and + * the lease may have changed, so handle same-state transitions for the + * BOUND state. 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) && (new_state != NM_DHCP_STATE_BOUND)) + return; + + if (priv->ipv6 && new_state == NM_DHCP_STATE_BOUND) { + char *start, *iaid; + + iaid = g_hash_table_lookup (options, "iaid"); + start = g_hash_table_lookup (options, "life_starts"); + if (iaid && start) + event_id = g_strdup_printf ("%s|%s", iaid, start); + } + + _LOGI ("state changed %s -> %s%s%s%s", + state_to_string (priv->state), + state_to_string (new_state), + NM_PRINT_FMT_QUOTED (event_id, ", event ID=\"", event_id, "\"", "")); + + priv->state = new_state; + g_signal_emit (G_OBJECT (self), + signals[SIGNAL_STATE_CHANGED], 0, + new_state, + ip_config, + options, + event_id); +} + +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, gint status, gpointer user_data) +{ + NMDhcpClient *self = NM_DHCP_CLIENT (user_data); + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self); + NMDhcpState new_state; + + 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"); + + if (!WIFEXITED (status)) + new_state = NM_DHCP_STATE_FAIL; + else + new_state = NM_DHCP_STATE_DONE; + + priv->pid = -1; + + nm_dhcp_client_set_state (self, new_state, NULL, NULL); +} + +void +nm_dhcp_client_start_timeout (NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self); + + /* Set up a timeout on the transaction to kill it after the timeout */ + g_assert (priv->timeout_id == 0); + 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); +} + +gboolean +nm_dhcp_client_start_ip4 (NMDhcpClient *self, + const char *dhcp_client_id, + const char *dhcp_anycast_addr, + const char *hostname, + const char *fqdn, + const char *last_ip4_address) +{ + NMDhcpClientPrivate *priv; + gs_unref_bytes GBytes *tmp = NULL; + + 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->ipv6 == FALSE, FALSE); + g_return_val_if_fail (priv->uuid != NULL, FALSE); + + _LOGI ("activation: beginning transaction (timeout in %d seconds)", priv->timeout); + + if (dhcp_client_id) + tmp = nm_dhcp_utils_client_id_string_to_bytes (dhcp_client_id); + nm_dhcp_client_set_client_id (self, tmp); + + g_clear_pointer (&priv->hostname, g_free); + priv->hostname = g_strdup (hostname); + g_free (priv->fqdn); + priv->fqdn = g_strdup (fqdn); + + return NM_DHCP_CLIENT_GET_CLASS (self)->ip4_start (self, dhcp_anycast_addr, last_ip4_address); +} + +static GByteArray * +generate_duid_from_machine_id (void) +{ + GByteArray *duid; + GChecksum *sum; + guint8 buffer[32]; /* SHA256 digest size */ + gsize sumlen = sizeof (buffer); + const guint16 duid_type = g_htons (4); + uuid_t uuid; + GRand *generator; + guint i; + gs_free char *machine_id_s = NULL; + gs_free char *str = NULL; + + machine_id_s = nm_utils_machine_id_read (); + if (nm_utils_machine_id_parse (machine_id_s, uuid)) { + /* Hash the machine ID so it's not leaked to the network */ + sum = g_checksum_new (G_CHECKSUM_SHA256); + g_checksum_update (sum, (const guchar *) &uuid, sizeof (uuid)); + g_checksum_get_digest (sum, buffer, &sumlen); + g_checksum_free (sum); + } else { + nm_log_warn (LOGD_DHCP, "dhcp: failed to read " SYSCONFDIR "/machine-id " + "or " LOCALSTATEDIR "/lib/dbus/machine-id to generate " + "DHCPv6 DUID; creating non-persistent random DUID."); + + generator = g_rand_new (); + for (i = 0; i < sizeof (buffer) / sizeof (guint32); i++) + ((guint32 *) buffer)[i] = g_rand_int (generator); + g_rand_free (generator); + } + + /* Generate a DHCP Unique Identifier for DHCPv6 using the + * DUID-UUID method (see RFC 6355 section 4). Format is: + * + * u16: type (DUID-UUID = 4) + * u8[16]: UUID bytes + */ + duid = g_byte_array_sized_new (18); + g_byte_array_append (duid, (guint8 *) &duid_type, sizeof (duid_type)); + + /* Since SHA256 is 256 bits, but UUID is 128 bits, we just take the first + * 128 bits of the SHA256 as the DUID-UUID. + */ + g_byte_array_append (duid, buffer, 16); + + nm_log_dbg (LOGD_DHCP, "dhcp: generated DUID %s", + (str = nm_dhcp_utils_duid_to_string (duid))); + return duid; +} + +static GByteArray * +get_duid (NMDhcpClient *self) +{ + static GByteArray *duid = NULL; + GByteArray *copy = NULL; + + if (G_UNLIKELY (duid == NULL)) { + duid = generate_duid_from_machine_id (); + g_assert (duid); + } + + if (G_LIKELY (duid)) { + copy = g_byte_array_sized_new (duid->len); + g_byte_array_append (copy, duid->data, duid->len); + } + + return copy; +} + +gboolean +nm_dhcp_client_start_ip6 (NMDhcpClient *self, + const char *dhcp_anycast_addr, + const struct in6_addr *ll_addr, + const char *hostname, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes) +{ + NMDhcpClientPrivate *priv; + gs_free char *str = NULL; + + 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->ipv6 == TRUE, FALSE); + g_return_val_if_fail (priv->uuid != NULL, FALSE); + + /* If we don't have one yet, read the default DUID for this DHCPv6 client + * from the client-specific persistent configuration. + */ + if (!priv->duid) + priv->duid = NM_DHCP_CLIENT_GET_CLASS (self)->get_duid (self); + + _LOGD ("DUID is '%s'", (str = nm_dhcp_utils_duid_to_string (priv->duid))); + + g_clear_pointer (&priv->hostname, g_free); + priv->hostname = g_strdup (hostname); + + priv->info_only = info_only; + + _LOGI ("activation: beginning transaction (timeout in %d seconds)", + priv->timeout); + + return NM_DHCP_CLIENT_GET_CLASS (self)->ip6_start (self, + dhcp_anycast_addr, + ll_addr, + info_only, + privacy, + priv->duid, + needed_prefixes); +} + +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", (long unsigned) 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) { + nm_log_dbg (LOGD_DHCP, "dhcp: could not remove pid file \"%s\": %d (%s)", + pid_file, errno, g_strerror (errno)); + } +} + +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, priv->duid); + if (old_pid > 0) + _LOGI ("canceled DHCP transaction, DHCP client pid %d", old_pid); + else + _LOGI ("canceled DHCP transaction"); + g_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; +} + +#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; + const char **p; + static const char *ignored_keys[] = { + "interface", + "pid", + "reason", + "dhcp_message_type", + 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 */ + for (p = ignored_keys; *p; p++) { + if (!strcmp (*p, key)) + return; + } + + if (g_str_has_prefix (key, NEW_TAG)) + key += NM_STRLEN (NEW_TAG); + if (!key[0]) + return; + + str_value = bytearray_variant_to_string (self, value, key); + if (str_value) + g_hash_table_insert (hash, g_strdup (key), str_value); +} + +gboolean +nm_dhcp_client_handle_event (gpointer unused, + const char *iface, + gint pid, + GVariant *options, + const char *reason, + NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv; + guint32 old_state; + guint32 new_state; + GHashTable *str_options = NULL; + GObject *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 reason '%s' -> state '%s'", + reason, state_to_string (new_state)); + + if (new_state == NM_DHCP_STATE_BOUND) { + GVariantIter iter; + const char *name; + GVariant *value; + + /* Copy options */ + str_options = g_hash_table_new_full (g_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); + } + + if (nm_logging_enabled (LOGL_DEBUG, LOGD_DHCP6)) { + GHashTableIter hash_iter; + gpointer key, val; + + g_hash_table_iter_init (&hash_iter, str_options); + while (g_hash_table_iter_next (&hash_iter, &key, &val)) + _LOGD ("option '%s'=>'%s'", (const char *) key, (const char *) val); + } + + /* Create the IP config */ + g_warn_if_fail (g_hash_table_size (str_options)); + if (g_hash_table_size (str_options)) { + if (priv->ipv6) { + prefix = nm_dhcp_utils_ip6_prefix_from_options (str_options); + ip_config = (GObject *) nm_dhcp_utils_ip6_config_from_options (priv->ifindex, + priv->iface, + str_options, + priv->priority, + priv->info_only); + } else { + ip_config = (GObject *) nm_dhcp_utils_ip4_config_from_options (priv->ifindex, + priv->iface, + str_options, + priv->priority); + } + } + } + + 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. */ + g_signal_emit (G_OBJECT (self), + signals[SIGNAL_PREFIX_DELEGATED], 0, + &prefix); + } else { + /* Fail if no valid IP config was received */ + if (new_state == NM_DHCP_STATE_BOUND && ip_config == NULL) { + _LOGW ("client bound but IP config not received"); + new_state = NM_DHCP_STATE_FAIL; + g_clear_pointer (&str_options, g_hash_table_unref); + } + + nm_dhcp_client_set_state (self, new_state, ip_config, str_options); + } + + if (str_options) + g_hash_table_destroy (str_options); + g_clear_object (&ip_config); + + return TRUE; +} + +/*****************************************************************************/ + +static void +get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE ((NMDhcpClient *) 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_IPV6: + g_value_set_boolean (value, priv->ipv6); + break; + case PROP_UUID: + g_value_set_string (value, priv->uuid); + break; + case PROP_PRIORITY: + g_value_set_uint (value, priv->priority); + 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 ((NMDhcpClient *) object); + + switch (prop_id) { + case PROP_IFACE: + /* construct-only */ + priv->iface = g_value_dup_string (value); + break; + case PROP_IFINDEX: + /* construct-only */ + priv->ifindex = g_value_get_int (value); + g_warn_if_fail (priv->ifindex > 0); + break; + case PROP_HWADDR: + /* construct-only */ + priv->hwaddr = g_value_dup_boxed (value); + break; + case PROP_IPV6: + /* construct-only */ + priv->ipv6 = g_value_get_boolean (value); + break; + case PROP_UUID: + /* construct-only */ + priv->uuid = g_value_dup_string (value); + break; + case PROP_PRIORITY: + /* construct-only */ + priv->priority = g_value_get_uint (value); + break; + case PROP_TIMEOUT: + priv->timeout = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_dhcp_client_init (NMDhcpClient *self) +{ + NMDhcpClientPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE (self, NM_TYPE_DHCP_CLIENT, NMDhcpClientPrivate); + self->_priv = priv; + + priv->pid = -1; +} + +static void +dispose (GObject *object) +{ + NMDhcpClient *self = NM_DHCP_CLIENT (object); + NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self); + + /* Stopping the client is left up to the controlling device + * explicitly since we may want to quit NetworkManager but not terminate + * the DHCP client. + */ + + watch_cleanup (self); + timeout_cleanup (self); + + g_clear_pointer (&priv->iface, g_free); + g_clear_pointer (&priv->hostname, g_free); + g_clear_pointer (&priv->fqdn, g_free); + g_clear_pointer (&priv->uuid, g_free); + g_clear_pointer (&priv->client_id, g_bytes_unref); + + if (priv->hwaddr) { + g_byte_array_free (priv->hwaddr, TRUE); + priv->hwaddr = NULL; + } + + if (priv->duid) { + g_byte_array_free (priv->duid, TRUE); + priv->duid = NULL; + } + + G_OBJECT_CLASS (nm_dhcp_client_parent_class)->dispose (object); +} + +static void +nm_dhcp_client_class_init (NMDhcpClientClass *client_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (client_class); + + g_type_class_add_private (client_class, sizeof (NMDhcpClientPrivate)); + + object_class->dispose = dispose; + object_class->get_property = get_property; + object_class->set_property = set_property; + + client_class->stop = stop; + client_class->get_duid = get_duid; + + 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_BYTE_ARRAY, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_IPV6] = + g_param_spec_boolean (NM_DHCP_CLIENT_IPV6, "", "", + FALSE, + 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_PRIORITY] = + g_param_spec_uint (NM_DHCP_CLIENT_PRIORITY, "", "", + 0, G_MAXUINT32, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_TIMEOUT] = + g_param_spec_uint (NM_DHCP_CLIENT_TIMEOUT, "", "", + 0, G_MAXUINT, 45, + G_PARAM_READWRITE | 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, + G_STRUCT_OFFSET (NMDhcpClientClass, state_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 4, G_TYPE_UINT, G_TYPE_OBJECT, G_TYPE_HASH_TABLE, G_TYPE_STRING); + + signals[SIGNAL_PREFIX_DELEGATED] = + g_signal_new (NM_DHCP_CLIENT_SIGNAL_PREFIX_DELEGATED, + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NMDhcpClientClass, state_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_POINTER); +} diff --git a/src/dhcp/nm-dhcp-client.h b/src/dhcp/nm-dhcp-client.h new file mode 100644 index 00000000..7a083ae7 --- /dev/null +++ b/src/dhcp/nm-dhcp-client.h @@ -0,0 +1,185 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 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> + +#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_INTERFACE "iface" +#define NM_DHCP_CLIENT_IFINDEX "ifindex" +#define NM_DHCP_CLIENT_HWADDR "hwaddr" +#define NM_DHCP_CLIENT_IPV6 "ipv6" +#define NM_DHCP_CLIENT_UUID "uuid" +#define NM_DHCP_CLIENT_PRIORITY "priority" +#define NM_DHCP_CLIENT_TIMEOUT "timeout" + +#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 or lease changed */ + 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_MAX, + NM_DHCP_STATE_MAX = __NM_DHCP_STATE_MAX - 1, +} NMDhcpState; + +struct _NMDhcpClientPrivate; + +typedef struct { + GObject parent; + struct _NMDhcpClientPrivate *_priv; +} NMDhcpClient; + +typedef struct { + GObjectClass parent; + + /* Methods */ + + gboolean (*ip4_start) (NMDhcpClient *self, + const char *anycast_addr, + const char *last_ip4_address); + + gboolean (*ip6_start) (NMDhcpClient *self, + const char *anycast_addr, + const struct in6_addr *ll_addr, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + const GByteArray *duid, + guint needed_prefixes); + + void (*stop) (NMDhcpClient *self, + gboolean release, + const GByteArray *duid); + + /** + * 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. + */ + GByteArray * (*get_duid) (NMDhcpClient *self); + + /* Signals */ + void (*state_changed) (NMDhcpClient *self, + NMDhcpState state, + GObject *ip_config, + GHashTable *options); +} NMDhcpClientClass; + +GType nm_dhcp_client_get_type (void); + +pid_t nm_dhcp_client_get_pid (NMDhcpClient *self); + +const char *nm_dhcp_client_get_iface (NMDhcpClient *self); + +int nm_dhcp_client_get_ifindex (NMDhcpClient *self); + +gboolean nm_dhcp_client_get_ipv6 (NMDhcpClient *self); + +const char *nm_dhcp_client_get_uuid (NMDhcpClient *self); + +const GByteArray *nm_dhcp_client_get_duid (NMDhcpClient *self); + +const GByteArray *nm_dhcp_client_get_hw_addr (NMDhcpClient *self); + +guint32 nm_dhcp_client_get_priority (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_fqdn (NMDhcpClient *self); + +gboolean nm_dhcp_client_start_ip4 (NMDhcpClient *self, + const char *dhcp_client_id, + const char *dhcp_anycast_addr, + const char *hostname, + const char *fqdn, + const char *last_ip4_address); + +gboolean nm_dhcp_client_start_ip6 (NMDhcpClient *self, + const char *dhcp_anycast_addr, + const struct in6_addr *ll_addr, + const char *hostname, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes); + +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_set_state (NMDhcpClient *self, + NMDhcpState new_state, + GObject *ip_config, /* NMIP4Config or NMIP6Config */ + GHashTable *options); /* str:str hash */ + +gboolean nm_dhcp_client_handle_event (gpointer unused, + const char *iface, + gint pid, + GVariant *options, + const char *reason, + NMDhcpClient *self); + +void nm_dhcp_client_set_client_id (NMDhcpClient *self, GBytes *client_id); + +/***************************************************************************** + * Client data + *****************************************************************************/ + +typedef struct { + GType (*get_type)(void); + const char *name; + const char *(*get_path) (void); + GSList *(*get_lease_ip_configs) (const char *iface, + int ifindex, + const char *uuid, + gboolean ipv6, + guint32 default_route_metric); +} NMDhcpClientFactory; + +extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_dhcpcd; +extern const NMDhcpClientFactory _nm_dhcp_client_factory_internal; + +#endif /* __NETWORKMANAGER_DHCP_CLIENT_H__ */ diff --git a/src/dhcp/nm-dhcp-dhclient-utils.c b/src/dhcp/nm-dhcp-dhclient-utils.c new file mode 100644 index 00000000..8cc25ab5 --- /dev/null +++ b/src/dhcp/nm-dhcp-dhclient-utils.c @@ -0,0 +1,775 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2011 Red Hat, Inc. + */ + +#include "nm-default.h" + +#include "nm-dhcp-dhclient-utils.h" + +#include <string.h> +#include <ctype.h> +#include <arpa/inet.h> + +#include "nm-dhcp-utils.h" +#include "nm-ip4-config.h" +#include "nm-utils.h" +#include "platform/nm-platform.h" +#include "NetworkManagerUtils.h" + +#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 " + +static void +add_request (GPtrArray *array, const char *item) +{ + int i; + + for (i = 0; i < array->len; i++) { + if (!strcmp (g_ptr_array_index (array, i), item)) + return; + } + g_ptr_array_add (array, g_strdup (item)); +} + +static gboolean +grab_request_options (GPtrArray *store, const char* line) +{ + char **areq, **aiter; + gboolean end = FALSE; + + /* Grab each 'request' or 'also request' option and save for later */ + areq = g_strsplit_set (line, "\t ,", -1); + for (aiter = areq; aiter && *aiter; aiter++) { + if (!strlen (g_strstrip (*aiter))) + continue; + + if (*aiter[0] == ';') { + /* all done */ + end = TRUE; + break; + } + + if (!g_ascii_isalnum ((*aiter)[0])) + continue; + + if ((*aiter)[strlen (*aiter) - 1] == ';') { + /* Remove the EOL marker */ + (*aiter)[strlen (*aiter) - 1] = '\0'; + end = TRUE; + } + + add_request (store, *aiter); + } + + if (areq) + g_strfreev (areq); + + return end; +} + + +static void +add_hostname4 (GString *str, const char *hostname, const char *fqdn) +{ + char *plain_hostname, *dot; + + if (fqdn) { + g_string_append_printf (str, FQDN_FORMAT "\n", fqdn); + g_string_append (str, + "send fqdn.encoded on;\n" + "send fqdn.server-update on;\n"); + } else if (hostname) { + plain_hostname = g_strdup (hostname); + dot = strchr (plain_hostname, '.'); + /* get rid of the domain */ + if (dot) + *dot = '\0'; + + g_string_append_printf (str, HOSTNAME4_FORMAT "\n", plain_hostname); + g_free (plain_hostname); + } +} + +static void +add_ip4_config (GString *str, GBytes *client_id, const char *hostname, const char *fqdn) +{ + if (client_id) { + const char *p; + gsize l; + guint i; + + p = g_bytes_get_data (client_id, &l); + g_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])) + 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 minus the 'type' */ + g_string_append_c (str, '"'); + g_string_append_len (str, p + 1, l - 1); + g_string_append_c (str, '"'); + } + g_string_append (str, "; # added by NetworkManager\n"); + } + + add_hostname4 (str, hostname, fqdn); + + 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) +{ + /* dhclient only supports the fqdn.fqdn for DHCPv6 and requires a fully- + * qualified name for this option, so we must require one here too. + */ + if (hostname && strchr (hostname, '.')) { + g_string_append_printf (str, FQDN_FORMAT "\n", hostname); + g_string_append (str, + "send fqdn.encoded on;\n" + "send fqdn.server-update on;\n"); + g_string_append_c (str, '\n'); + } +} + +static GBytes * +read_client_id (const char *str) +{ + gs_free char *s = NULL; + char *p; + + g_assert (!strncmp (str, CLIENTID_TAG, NM_STRLEN (CLIENTID_TAG))); + + str += NM_STRLEN (CLIENTID_TAG); + while (g_ascii_isspace (*str)) + str++; + + if (*str == '"') { + s = g_strdup (str + 1); + p = strrchr (s, '"'); + if (p) + *p = '\0'; + else + return NULL; + } else + s = g_strdup (str); + + g_strchomp (s); + if (s[strlen (s) - 1] == ';') + s[strlen (s) - 1] = '\0'; + + return nm_dhcp_utils_client_id_string_to_bytes (s); +} + +GBytes * +nm_dhcp_dhclient_get_client_id_from_config_file (const char *path) +{ + gs_free char *contents = NULL; + gs_strfreev char **lines = NULL; + char **line; + + g_return_val_if_fail (path != NULL, NULL); + + if (!g_file_test (path, G_FILE_TEST_EXISTS)) + return NULL; + + if (!g_file_get_contents (path, &contents, NULL, NULL)) + return NULL; + + lines = g_strsplit_set (contents, "\n\r", 0); + for (line = lines; lines && *line; line++) { + if (!strncmp (*line, CLIENTID_TAG, NM_STRLEN (CLIENTID_TAG))) + return read_client_id (*line); + } + return NULL; +} + +char * +nm_dhcp_dhclient_create_config (const char *interface, + gboolean is_ip6, + GBytes *client_id, + const char *anycast_addr, + const char *hostname, + const char *fqdn, + const char *orig_path, + const char *orig_contents, + GBytes **out_new_client_id) +{ + GString *new_contents; + GPtrArray *fqdn_opts, *reqs; + int i; + + g_return_val_if_fail (!anycast_addr || nm_utils_hwaddr_valid (anycast_addr, ETH_ALEN), NULL); + + new_contents = g_string_new (_("# Created by NetworkManager\n")); + fqdn_opts = g_ptr_array_sized_new (5); + reqs = g_ptr_array_new_full (5, g_free); + + if (orig_contents) { + char **lines, **line; + gboolean in_alsoreq = FALSE; + gboolean in_req = FALSE; + + g_string_append_printf (new_contents, _("# Merged from %s\n\n"), orig_path); + + lines = g_strsplit_set (orig_contents, "\n\r", 0); + for (line = lines; lines && *line; line++) { + char *p = *line; + + if (!strlen (g_strstrip (p))) + 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) + *out_new_client_id = read_client_id (p); + } + + /* Override config file hostname and use one from the connection */ + if (hostname || fqdn) { + 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))) { + 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); + } + + /* 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'); + } + + if (lines) + g_strfreev (lines); + } else + g_string_append_c (new_contents, '\n'); + + if (is_ip6) { + add_hostname6 (new_contents, hostname); + add_request (reqs, "dhcp6.name-servers"); + add_request (reqs, "dhcp6.domain-search"); + add_request (reqs, "dhcp6.client-id"); + } else { + add_ip4_config (new_contents, client_id, hostname, fqdn); + 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"); + } + + /* 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]); + g_ptr_array_free (reqs, TRUE); + + for (i = 0; i < fqdn_opts->len; i++) { + char *t = g_ptr_array_index (fqdn_opts, 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_free (t); + } + g_ptr_array_free (fqdn_opts, TRUE); + + 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 (new_contents, FALSE); +} + +/* Roughly follow what dhclient's quotify_buf() and pretty_escape() functions do */ +char * +nm_dhcp_dhclient_escape_duid (const GByteArray *duid) +{ + char *escaped; + const guint8 *s = duid->data; + char *d; + + d = escaped = g_malloc0 ((duid->len * 4) + 1); + while (s < (duid->data + duid->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++; + } + return escaped; +} + +static inline gboolean +isoctal (const guint8 *p) +{ + return ( p[0] >= '0' && p[0] <= '3' + && p[1] >= '0' && p[1] <= '7' + && p[2] >= '0' && p[2] <= '7'); +} + +GByteArray * +nm_dhcp_dhclient_unescape_duid (const char *duid) +{ + GByteArray *unescaped; + const guint8 *p = (const guint8 *) duid; + guint i, len; + guint8 octal; + + 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 { + /* 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 unescaped; + +error: + g_byte_array_free (unescaped, TRUE); + return NULL; +} + +#define DUID_PREFIX "default-duid \"" + +GByteArray * +nm_dhcp_dhclient_read_duid (const char *leasefile, GError **error) +{ + GByteArray *duid = NULL; + char *contents; + char **line, **split, *p, *e; + + if (!g_file_test (leasefile, G_FILE_TEST_EXISTS)) + return NULL; + + if (!g_file_get_contents (leasefile, &contents, NULL, error)) + return NULL; + + split = g_strsplit_set (contents, "\n\r", -1); + for (line = split; line && *line && (duid == NULL); line++) { + p = g_strstrip (*line); + if (g_str_has_prefix (p, DUID_PREFIX)) { + p += strlen (DUID_PREFIX); + + /* look for trailing "; */ + e = p + strlen (p) - 2; + if (strcmp (e, "\";") != 0) + continue; + *e = '\0'; + + duid = nm_dhcp_dhclient_unescape_duid (p); + } + } + g_free (contents); + g_strfreev (split); + + return duid; +} + +gboolean +nm_dhcp_dhclient_save_duid (const char *leasefile, + const char *escaped_duid, + GError **error) +{ + char **lines = NULL, **iter, *l; + GString *s; + gboolean success; + gsize len = 0; + + g_return_val_if_fail (leasefile != NULL, FALSE); + g_return_val_if_fail (escaped_duid != NULL, FALSE); + + if (g_file_test (leasefile, G_FILE_TEST_EXISTS)) { + 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; + } + + /* If the file already contains an uncommented DUID, leave it */ + g_assert (contents); + lines = g_strsplit_set (contents, "\n\r", -1); + g_free (contents); + for (iter = lines; iter && *iter; iter++) { + l = *iter; + while (g_ascii_isspace (*l)) + l++; + if (g_str_has_prefix (l, DUID_PREFIX)) { + g_strfreev (lines); + return TRUE; + } + } + } + + 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; iter++) + g_string_append (s, *iter[0] ? *iter : "\n"); + g_strfreev (lines); + } + + success = g_file_set_contents (leasefile, s->str, -1, error); + if (!success) + g_prefix_error (error, "failed to set DUID in lease file %s: ", leasefile); + + g_string_free (s, TRUE); + return success; +} + +static void +add_lease_option (GHashTable *hash, char *line) +{ + char *spc; + size_t len; + + /* Find the space after "option" */ + spc = strchr (line, ' '); + if (!spc) + return; + + /* Find the option tag's data, which is after the second space */ + if (g_str_has_prefix (line, "option ")) { + while (g_ascii_isspace (*spc)) + spc++; + spc = strchr (spc + 1, ' '); + if (!spc) + return; + } + + /* Split the line at the space */ + *spc = '\0'; + spc++; + + /* Kill the ';' at the end of the line, if any */ + len = strlen (spc); + if (*(spc + len - 1) == ';') + *(spc + len - 1) = '\0'; + + /* Strip leading quote */ + while (g_ascii_isspace (*spc)) + spc++; + if (*spc == '"') + spc++; + + /* Strip trailing quote */ + len = strlen (spc); + if (len > 0 && spc[len - 1] == '"') + spc[len - 1] = '\0'; + + if (spc[0]) + g_hash_table_insert (hash, g_strdup (line), g_strdup (spc)); +} + +#define LEASE_INVALID G_MININT64 +static GTimeSpan +lease_validity_span (const char *str_expire, GDateTime *now) +{ + GDateTime *expire = NULL; + struct tm expire_tm; + GTimeSpan span; + + g_return_val_if_fail (now != NULL, LEASE_INVALID); + g_return_val_if_fail (str_expire != NULL, LEASE_INVALID); + + /* Skip initial number (day of week?) */ + if (!isdigit (*str_expire++)) + return LEASE_INVALID; + if (!isspace (*str_expire++)) + return LEASE_INVALID; + /* Read lease expiration (in UTC) */ + if (!strptime (str_expire, "%t%Y/%m/%d %H:%M:%S", &expire_tm)) + return LEASE_INVALID; + + expire = g_date_time_new_utc (expire_tm.tm_year + 1900, + expire_tm.tm_mon + 1, + expire_tm.tm_mday, + expire_tm.tm_hour, + expire_tm.tm_min, + expire_tm.tm_sec); + if (!expire) + return LEASE_INVALID; + + span = g_date_time_difference (expire, now); + g_date_time_unref (expire); + + /* GDateTime only supports a range of less then 10000 years, so span can + * not overflow or be equal to LEASE_INVALID */ + return span; +} + +/** + * nm_dhcp_dhclient_read_lease_ip_configs: + * @iface: the interface name to match leases with + * @ifindex: interface index of @iface + * @contents: the contents of a dhclient leasefile + * @ipv6: whether to read IPv4 or IPv6 leases + * @now: the current UTC date/time; pass %NULL to automatically use current + * UTC time. Testcases may need a different value for 'now' + * + * Reads dhclient leases from @contents and parses them into either + * #NMIP4Config or #NMIP6Config objects depending on the value of @ipv6. + * + * Returns: a #GSList of #NMIP4Config objects (if @ipv6 is %FALSE) or a list of + * #NMIP6Config objects (if @ipv6 is %TRUE) containing the lease data. + */ +GSList * +nm_dhcp_dhclient_read_lease_ip_configs (const char *iface, + int ifindex, + const char *contents, + gboolean ipv6, + GDateTime *now) +{ + GSList *parsed = NULL, *iter, *leases = NULL; + char **line, **split = NULL; + GHashTable *hash = NULL; + gint32 now_monotonic_ts; + + g_return_val_if_fail (contents != NULL, NULL); + + split = g_strsplit_set (contents, "\n\r", -1); + if (!split) + return NULL; + + for (line = split; line && *line; line++) { + *line = g_strstrip (*line); + + if (*line[0] == '#') { + /* Comment */ + } else if (!strcmp (*line, "}")) { + /* Lease ends */ + parsed = g_slist_append (parsed, hash); + hash = NULL; + } else if (!strcmp (*line, "lease {")) { + /* Beginning of a new lease */ + if (hash) { + /* Ignore malformed lease that doesn't end before new one starts */ + g_hash_table_destroy (hash); + } + + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + } else if (hash && strlen (*line)) + add_lease_option (hash, *line); + } + g_strfreev (split); + + /* Check if the last lease in the file was properly ended */ + if (hash) { + /* Ignore malformed lease that doesn't end before new one starts */ + g_hash_table_destroy (hash); + hash = NULL; + } + + if (now) + g_date_time_ref (now); + else + now = g_date_time_new_now_utc (); + now_monotonic_ts = nm_utils_get_monotonic_timestamp_s (); + + for (iter = parsed; iter; iter = g_slist_next (iter)) { + NMIP4Config *ip4; + NMPlatformIP4Address address; + const char *value; + GTimeSpan expiry; + guint32 tmp, gw = 0; + + hash = iter->data; + + /* Make sure this lease is for the interface we want */ + value = g_hash_table_lookup (hash, "interface"); + if (!value || strcmp (value, iface)) + continue; + + value = g_hash_table_lookup (hash, "expire"); + if (!value) + continue; + expiry = lease_validity_span (value, now); + if (expiry == LEASE_INVALID) + continue; + + /* scale expiry to seconds (and CLAMP into the range of guint32) */ + expiry = CLAMP (expiry / G_TIME_SPAN_SECOND, 0, NM_PLATFORM_LIFETIME_PERMANENT-1); + if (expiry <= 0) { + /* the address is already expired. Don't even add it. */ + continue; + } + + memset (&address, 0, sizeof (address)); + + /* IP4 address */ + value = g_hash_table_lookup (hash, "fixed-address"); + if (!value) + continue; + if (!inet_pton (AF_INET, value, &address.address)) + continue; + address.peer_address = address.address; + + /* Gateway */ + value = g_hash_table_lookup (hash, "option routers"); + if (!value) + continue; + if (!inet_pton (AF_INET, value, &gw)) + continue; + + /* Netmask */ + value = g_hash_table_lookup (hash, "option subnet-mask"); + if (value && inet_pton (AF_INET, value, &tmp)) + address.plen = nm_utils_ip4_netmask_to_prefix (tmp); + + /* Get default netmask for the IP according to appropriate class. */ + if (!address.plen) + address.plen = nm_utils_ip4_get_default_prefix (address.address); + + address.timestamp = now_monotonic_ts; + address.lifetime = address.preferred = expiry; + address.addr_source = NM_IP_CONFIG_SOURCE_DHCP; + + ip4 = nm_ip4_config_new (ifindex); + nm_ip4_config_add_address (ip4, &address); + nm_ip4_config_set_gateway (ip4, gw); + + value = g_hash_table_lookup (hash, "option domain-name-servers"); + if (value) { + char **dns, **dns_iter; + + dns = g_strsplit_set (value, ",", -1); + for (dns_iter = dns; dns_iter && *dns_iter; dns_iter++) { + if (inet_pton (AF_INET, *dns_iter, &tmp)) + nm_ip4_config_add_nameserver (ip4, tmp); + } + if (dns) + g_strfreev (dns); + } + + value = g_hash_table_lookup (hash, "option domain-name"); + if (value && value[0]) + nm_ip4_config_add_domain (ip4, value); + + /* FIXME: static routes */ + + leases = g_slist_append (leases, ip4); + } + + g_date_time_unref (now); + g_slist_free_full (parsed, (GDestroyNotify) g_hash_table_destroy); + return leases; +} + diff --git a/src/dhcp/nm-dhcp-dhclient-utils.h b/src/dhcp/nm-dhcp-dhclient-utils.h new file mode 100644 index 00000000..83d5a23d --- /dev/null +++ b/src/dhcp/nm-dhcp-dhclient-utils.h @@ -0,0 +1,54 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 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, + gboolean is_ip6, + GBytes *client_id, + const char *anycast_addr, + const char *hostname, + const char *fqdn, + const char *orig_path, + const char *orig_contents, + GBytes **out_new_client_id); + +char *nm_dhcp_dhclient_escape_duid (const GByteArray *duid); + +GByteArray *nm_dhcp_dhclient_unescape_duid (const char *duid); + +GByteArray *nm_dhcp_dhclient_read_duid (const char *leasefile, GError **error); + +gboolean nm_dhcp_dhclient_save_duid (const char *leasefile, + const char *escaped_duid, + GError **error); + +GSList *nm_dhcp_dhclient_read_lease_ip_configs (const char *iface, + int ifindex, + const char *contents, + gboolean ipv6, + GDateTime *now); + +GBytes *nm_dhcp_dhclient_get_client_id_from_config_file (const char *path); + +#endif /* __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ */ + diff --git a/src/dhcp/nm-dhcp-dhclient.c b/src/dhcp/nm-dhcp-dhclient.c new file mode 100644 index 00000000..64d93744 --- /dev/null +++ b/src/dhcp/nm-dhcp-dhclient.c @@ -0,0 +1,706 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-dhcp-dhclient.c - dhclient specific hooks for NetworkManager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2005 - 2012 Red Hat, Inc. + */ + +#include <config.h> +#define __CONFIG_H__ + +#define _XOPEN_SOURCE +#include <time.h> +#undef _XOPEN_SOURCE + +#include "nm-default.h" + +#if WITH_DHCLIENT + +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <stdio.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <ctype.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" + +/*****************************************************************************/ + +#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(): + * @iface: the interface name of the device on which DHCP will be done + * @uuid: the connection UUID to which the returned lease should belong + * @ipv6: %TRUE for IPv6, %FALSE for IPv4 + * @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 (const char *iface, + const char *uuid, + gboolean ipv6, + char **out_preferred_path) +{ + char *path; + + /* /var/lib/NetworkManager is the preferred leasefile path */ + path = g_strdup_printf (NMSTATEDIR "/dhclient%s-%s-%s.lease", + ipv6 ? "6" : "", + uuid, + iface); + if (out_preferred_path) + *out_preferred_path = g_strdup (path); + + if (g_file_test (path, G_FILE_TEST_EXISTS)) + return 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", + ipv6 ? "6" : "", uuid, iface); + if (g_file_test (path, G_FILE_TEST_EXISTS)) + return path; + + /* Old Red Hat and Fedora location */ + g_free (path); + path = g_strdup_printf (LOCALSTATEDIR "/lib/dhclient/dhclient%s-%s-%s.lease", + ipv6 ? "6" : "", uuid, iface); + if (g_file_test (path, G_FILE_TEST_EXISTS)) + return path; + + /* Fail */ + g_free (path); + return NULL; +} + +static GSList * +nm_dhcp_dhclient_get_lease_ip_configs (const char *iface, + int ifindex, + const char *uuid, + gboolean ipv6, + guint32 default_route_metric) +{ + char *contents = NULL; + char *leasefile; + GSList *leases = NULL; + + leasefile = get_dhclient_leasefile (iface, uuid, FALSE, NULL); + if (!leasefile) + return NULL; + + if ( g_file_test (leasefile, G_FILE_TEST_EXISTS) + && g_file_get_contents (leasefile, &contents, NULL, NULL) + && contents + && contents[0]) + leases = nm_dhcp_dhclient_read_lease_ip_configs (iface, ifindex, contents, ipv6, NULL); + + g_free (leasefile); + g_free (contents); + + return leases; +} + +static gboolean +merge_dhclient_config (NMDhcpDhclient *self, + const char *iface, + const char *conf_file, + gboolean is_ip6, + GBytes *client_id, + const char *anycast_addr, + const char *hostname, + const char *fqdn, + const char *orig_path, + GBytes **out_new_client_id, + GError **error) +{ + char *orig = NULL, *new; + gboolean success = FALSE; + + g_return_val_if_fail (iface != NULL, FALSE); + g_return_val_if_fail (conf_file != NULL, 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); + } + } + + if (is_ip6 && hostname && !strchr (hostname, '.')) + _LOGW ("hostname is not a FQDN, it will be ignored"); + + new = nm_dhcp_dhclient_create_config (iface, is_ip6, client_id, anycast_addr, hostname, fqdn, orig_path, orig, out_new_client_id); + g_assert (new); + success = g_file_set_contents (conf_file, new, -1, error); + g_free (new); + g_free (orig); + + return success; +} + +static char * +find_existing_config (NMDhcpDhclient *self, const char *iface, const char *uuid, gboolean ipv6) +{ + 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", ipv6 ? "6" : "", 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", ipv6 ? "6" : "", 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", ipv6 ? "6" : ""); + _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", ipv6 ? "6" : "", 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", ipv6 ? "6" : "", 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", ipv6 ? "6" : ""); + _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", ipv6 ? "6" : ""); + _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, + const char *iface, + gboolean is_ip6, + const char *uuid, + GBytes *client_id, + const char *dhcp_anycast_addr, + const char *hostname, + const char *fqdn, + GBytes **out_new_client_id) +{ + char *orig = NULL, *new = NULL; + GError *error = NULL; + gboolean success = FALSE; + + g_return_val_if_fail (iface != NULL, NULL); + + new = g_strdup_printf (NMSTATEDIR "/dhclient%s-%s.conf", is_ip6 ? "6" : "", iface); + _LOGD ("creating composite dhclient config %s", new); + + orig = find_existing_config (self, iface, uuid, is_ip6); + if (orig) + _LOGD ("merging existing dhclient config %s", orig); + else + _LOGD ("no existing dhclient configuration to merge"); + + error = NULL; + success = merge_dhclient_config (self, iface, new, is_ip6, client_id, dhcp_anycast_addr, + hostname, fqdn, orig, out_new_client_id, &error); + if (!success) { + _LOGW ("error creating dhclient configuration: %s", error->message); + g_error_free (error); + } + + g_free (orig); + return new; +} + + +static gboolean +dhclient_start (NMDhcpClient *client, + const char *mode_opt, + const GByteArray *duid, + gboolean release, + pid_t *out_pid, + int prefixes) +{ + NMDhcpDhclient *self = NM_DHCP_DHCLIENT (client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE (self); + GPtrArray *argv = NULL; + pid_t pid; + GError *error = NULL; + const char *iface, *uuid, *system_bus_address, *dhclient_path = NULL; + char *binary_name, *cmd_str, *pid_file = NULL, *system_bus_address_env = NULL; + gboolean ipv6, success; + char *escaped, *preferred_leasefile_path = NULL; + + g_return_val_if_fail (priv->pid_file == NULL, FALSE); + + iface = nm_dhcp_client_get_iface (client); + uuid = nm_dhcp_client_get_uuid (client); + ipv6 = nm_dhcp_client_get_ipv6 (client); + + dhclient_path = nm_dhcp_dhclient_get_path (); + if (!dhclient_path) { + _LOGW ("dhclient could not be found"); + return FALSE; + } + + pid_file = g_strdup_printf (RUNSTATEDIR "/dhclient%s-%s.pid", + ipv6 ? "6" : "", + 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 */ + g_free (pid_file); + pid_file = NULL; + } + + g_free (priv->lease_file); + priv->lease_file = get_dhclient_leasefile (iface, uuid, ipv6, &preferred_leasefile_path); + if (!priv->lease_file) { + /* No existing leasefile, dhclient will create one at the preferred path */ + priv->lease_file = g_strdup (preferred_leasefile_path); + } else if (g_strcmp0 (priv->lease_file, preferred_leasefile_path) != 0) { + GFile *src = g_file_new_for_path (priv->lease_file); + 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, &error)) { + /* Success; use the preferred leasefile path */ + g_free (priv->lease_file); + priv->lease_file = g_strdup (g_file_get_path (dst)); + } else { + /* Failure; just use the existing leasefile */ + _LOGW ("failed to copy leasefile %s to %s: %s", + g_file_get_path (src), g_file_get_path (dst), + error->message); + g_clear_error (&error); + } + g_object_unref (src); + g_object_unref (dst); + } + g_free (preferred_leasefile_path); + + /* Save the DUID to the leasefile dhclient will actually use */ + if (ipv6) { + escaped = nm_dhcp_dhclient_escape_duid (duid); + success = nm_dhcp_dhclient_save_duid (priv->lease_file, escaped, &error); + g_free (escaped); + if (!success) { + _LOGW ("failed to save DUID to %s: %s", priv->lease_file, error->message); + g_free (pid_file); + 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 (ipv6) { + g_ptr_array_add (argv, (gpointer) "-6"); + 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); + + cmd_str = g_strjoinv (" ", (gchar **) argv->pdata); + _LOGD ("running: %s", cmd_str); + g_free (cmd_str); + + 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, &error)) { + g_assert (pid > 0); + _LOGI ("dhclient started with pid %d", pid); + if (release == FALSE) + nm_dhcp_client_watch_child (client, pid); + priv->pid_file = pid_file; + } else { + _LOGW ("dhclient failed to start: '%s'", error->message); + g_error_free (error); + g_free (pid_file); + } + + if (out_pid) + *out_pid = pid; + + g_ptr_array_free (argv, TRUE); + g_free (system_bus_address_env); + return pid > 0 ? TRUE : FALSE; +} + +static gboolean +ip4_start (NMDhcpClient *client, const char *dhcp_anycast_addr, const char *last_ip4_address) +{ + 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; + const char *iface, *uuid, *hostname, *fqdn; + gboolean success = FALSE; + + iface = nm_dhcp_client_get_iface (client); + uuid = nm_dhcp_client_get_uuid (client); + client_id = nm_dhcp_client_get_client_id (client); + hostname = nm_dhcp_client_get_hostname (client); + fqdn = nm_dhcp_client_get_fqdn (client); + + priv->conf_file = create_dhclient_config (self, iface, FALSE, uuid, client_id, dhcp_anycast_addr, + hostname, fqdn, &new_client_id); + if (priv->conf_file) { + if (new_client_id) + nm_dhcp_client_set_client_id (client, new_client_id); + success = dhclient_start (client, NULL, NULL, FALSE, NULL, 0); + } else + _LOGW ("error creating dhclient configuration file"); + + return success; +} + +static gboolean +ip6_start (NMDhcpClient *client, + const char *dhcp_anycast_addr, + const struct in6_addr *ll_addr, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + const GByteArray *duid, + guint needed_prefixes) +{ + NMDhcpDhclient *self = NM_DHCP_DHCLIENT (client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE (self); + const char *iface, *uuid, *hostname; + + iface = nm_dhcp_client_get_iface (client); + uuid = nm_dhcp_client_get_uuid (client); + hostname = nm_dhcp_client_get_hostname (client); + + priv->conf_file = create_dhclient_config (self, iface, TRUE, uuid, NULL, dhcp_anycast_addr, hostname, NULL, NULL); + if (!priv->conf_file) { + _LOGW ("error creating dhclient configuration file"); + return FALSE; + } + + return dhclient_start (client, info_only ? "-S" : "-N", duid, FALSE, NULL, needed_prefixes); +} + +static void +stop (NMDhcpClient *client, gboolean release, const GByteArray *duid) +{ + NMDhcpDhclient *self = NM_DHCP_DHCLIENT (client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE (self); + + /* Chain up to parent */ + NM_DHCP_CLIENT_CLASS (nm_dhcp_dhclient_parent_class)->stop (client, release, duid); + + if (priv->conf_file) + if (remove (priv->conf_file) == -1) + _LOGD ("could not remove dhcp config file \"%s\": %d (%s)", priv->conf_file, errno, g_strerror (errno)); + if (priv->pid_file) { + if (remove (priv->pid_file) == -1) + _LOGD ("could not remove dhcp pid file \"%s\": %d (%s)", priv->pid_file, errno, g_strerror (errno)); + g_free (priv->pid_file); + priv->pid_file = NULL; + } + + if (release) { + pid_t rpid = -1; + + if (dhclient_start (client, NULL, duid, TRUE, &rpid, 0)) { + /* Wait a few seconds for the release to happen */ + nm_dhcp_client_stop_pid (rpid, nm_dhcp_client_get_iface (client)); + } + } +} + +static void +state_changed (NMDhcpClient *client, + NMDhcpState state, + GObject *ip_config, + GHashTable *options) +{ + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE ((NMDhcpDhclient *) client); + gs_unref_bytes GBytes *client_id = NULL; + + if (nm_dhcp_client_get_client_id (client)) + return; + if (state != NM_DHCP_STATE_BOUND) + return; + + client_id = nm_dhcp_dhclient_get_client_id_from_config_file (priv->conf_file); + nm_dhcp_client_set_client_id (client, client_id); +} + +static GByteArray * +get_duid (NMDhcpClient *client) +{ + NMDhcpDhclient *self = NM_DHCP_DHCLIENT (client); + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE (self); + GByteArray *duid = NULL; + char *leasefile; + GError *error = NULL; + + /* Look in interface-specific leasefile first for backwards compat */ + leasefile = get_dhclient_leasefile (nm_dhcp_client_get_iface (client), + nm_dhcp_client_get_uuid (client), + TRUE, + 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); + } + g_free (leasefile); + } + + if (!duid && priv->def_leasefile) { + /* 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 our DUID, otherwise let the parent class make a default DUID */ + return duid ? duid : NM_DHCP_CLIENT_CLASS (nm_dhcp_dhclient_parent_class)->get_duid (client); +} + +/*****************************************************************************/ + +static const char *def_leasefiles[] = { + SYSCONFDIR "/dhclient6.leases", + LOCALSTATEDIR "/lib/dhcp/dhclient6.leases", + LOCALSTATEDIR "/lib/dhclient/dhclient6.leases", + NULL +}; + +static void +nm_dhcp_dhclient_init (NMDhcpDhclient *self) +{ + NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE (self); + const char **iter = &def_leasefiles[0]; + + while (iter && *iter) { + if (g_file_test (*iter, G_FILE_TEST_EXISTS)) { + priv->def_leasefile = *iter; + break; + } + iter++; + } + + /* Fallback option */ + if (!priv->def_leasefile) + priv->def_leasefile = SYSCONFDIR "/dhclient6.leases"; + + 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 ((NMDhcpDhclient *) 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; + client_class->state_changed = state_changed; +} + +const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient = { + .name = "dhclient", + .get_type = nm_dhcp_dhclient_get_type, + .get_path = nm_dhcp_dhclient_get_path, + .get_lease_ip_configs = nm_dhcp_dhclient_get_lease_ip_configs, +}; + +#endif /* WITH_DHCLIENT */ diff --git a/src/dhcp/nm-dhcp-dhcpcd.c b/src/dhcp/nm-dhcp-dhcpcd.c new file mode 100644 index 00000000..c8643881 --- /dev/null +++ b/src/dhcp/nm-dhcp-dhcpcd.c @@ -0,0 +1,265 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-dhcp-dhcpcd.c - dhcpcd specific hooks for NetworkManager + * + * Copyright (C) 2008 Roy Marples + * Copyright (C) 2010 Dan Williams <dcbw@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "nm-default.h" + +#if WITH_DHCPCD + +#include <string.h> +#include <stdlib.h> +#include <errno.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 { + char *pid_file; + 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) +{ + NMDhcpDhcpcd *self = NM_DHCP_DHCPCD (client); + NMDhcpDhcpcdPrivate *priv = NM_DHCP_DHCPCD_GET_PRIVATE (self); + GPtrArray *argv = NULL; + pid_t pid = -1; + GError *error = NULL; + char *pid_contents = NULL, *binary_name, *cmd_str, *dot; + const char *iface, *dhcpcd_path, *hostname, *fqdn; + gs_free char *prefix = NULL; + + g_return_val_if_fail (priv->pid_file == NULL, FALSE); + + iface = nm_dhcp_client_get_iface (client); + + /* dhcpcd does not allow custom pidfiles; the pidfile is always + * RUNDIR "dhcpcd-<ifname>.pid". + */ + priv->pid_file = g_strdup_printf (RUNDIR "/dhcpcd-%s.pid", iface); + + dhcpcd_path = nm_dhcp_dhcpcd_get_path (); + if (!dhcpcd_path) { + _LOGW ("dhcpcd could not be found"); + return FALSE; + } + + /* Kill any existing dhcpcd from the pidfile */ + binary_name = g_path_get_basename (dhcpcd_path); + nm_dhcp_client_stop_existing (priv->pid_file, binary_name); + g_free (binary_name); + + argv = g_ptr_array_new (); + g_ptr_array_add (argv, (gpointer) dhcpcd_path); + + 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) "-G"); /* Let NM handle routing */ + + g_ptr_array_add (argv, (gpointer) "-c"); /* Set script file */ + g_ptr_array_add (argv, (gpointer) nm_dhcp_helper_path); + +#ifdef DHCPCD_SUPPORTS_IPV6 + /* 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"); +#endif + + hostname = nm_dhcp_client_get_hostname (client); + fqdn = nm_dhcp_client_get_fqdn (client); + + if (fqdn) { + g_ptr_array_add (argv, (gpointer) "-h"); + g_ptr_array_add (argv, (gpointer) fqdn); + g_ptr_array_add (argv, (gpointer) "-F"); + g_ptr_array_add (argv, (gpointer) "both"); + } else if (hostname) { + prefix = strdup (hostname); + dot = strchr (prefix, '.'); + /* get rid of the domain */ + if (dot) + *dot = '\0'; + + g_ptr_array_add (argv, (gpointer) "-h"); /* Send hostname to DHCP server */ + g_ptr_array_add (argv, (gpointer) prefix); + } + + g_ptr_array_add (argv, (gpointer) iface); + g_ptr_array_add (argv, NULL); + + cmd_str = g_strjoinv (" ", (gchar **) argv->pdata); + _LOGD ("running: %s", cmd_str); + g_free (cmd_str); + + 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, &error)) { + g_assert (pid > 0); + _LOGI ("dhcpcd started with pid %d", pid); + nm_dhcp_client_watch_child (client, pid); + } else { + _LOGW ("dhcpcd failed to start, error: '%s'", error->message); + g_error_free (error); + } + + g_free (pid_contents); + g_ptr_array_free (argv, TRUE); + return pid > 0 ? TRUE : FALSE; +} + +static gboolean +ip6_start (NMDhcpClient *client, + const char *dhcp_anycast_addr, + const struct in6_addr *ll_addr, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + const GByteArray *duid, + guint needed_prefixes) +{ + NMDhcpDhcpcd *self = NM_DHCP_DHCPCD (client); + + _LOGW ("the dhcpcd backend does not support IPv6"); + return FALSE; +} + +static void +stop (NMDhcpClient *client, gboolean release, const GByteArray *duid) +{ + NMDhcpDhcpcd *self = NM_DHCP_DHCPCD (client); + NMDhcpDhcpcdPrivate *priv = NM_DHCP_DHCPCD_GET_PRIVATE (self); + + /* Chain up to parent */ + NM_DHCP_CLIENT_CLASS (nm_dhcp_dhcpcd_parent_class)->stop (client, release, duid); + + if (priv->pid_file) { + if (remove (priv->pid_file) == -1) + _LOGD ("could not remove dhcp pid file \"%s\": %d (%s)", priv->pid_file, errno, g_strerror (errno)); + } + + /* FIXME: implement release... */ +} + +/*****************************************************************************/ + +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 ((NMDhcpDhcpcd *) 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); + } + + nm_clear_g_free (&priv->pid_file); + + 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->ip6_start = ip6_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, + .get_lease_ip_configs = NULL, +}; + +#endif /* WITH_DHCPCD */ diff --git a/src/dhcp/nm-dhcp-helper-api.h b/src/dhcp/nm-dhcp-helper-api.h new file mode 100644 index 00000000..58344215 --- /dev/null +++ b/src/dhcp/nm-dhcp-helper-api.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* NetworkManager -- Network link manager + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + * (C) Copyright 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/dhcp/nm-dhcp-helper.c b/src/dhcp/nm-dhcp-helper.c new file mode 100644 index 00000000..f50c5cec --- /dev/null +++ b/src/dhcp/nm-dhcp-helper.c @@ -0,0 +1,227 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* NetworkManager -- Network link manager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2007 - 2013 Red Hat, Inc. + */ + +#include "nm-default.h" + +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <signal.h> + +#include "nm-utils/nm-vpn-plugin-macros.h" + +#include "nm-dhcp-helper-api.h" + +/*****************************************************************************/ + +#ifdef 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 const char * ignore[] = {"PATH", "SHLVL", "_", "PWD", "dhc_dbus", NULL}; + +static GVariant * +build_signal_parameters (void) +{ + char **item; + GVariantBuilder builder; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + + /* List environment and format for dbus dict */ + for (item = environ; *item; item++) { + char *name, *val, **p; + + /* Split on the = */ + name = g_strdup (*item); + val = strchr (name, '='); + if (!val || val == name) + goto next; + *val++ = '\0'; + + /* Ignore non-DCHP-related environment variables */ + for (p = (char **) ignore; *p; p++) { + if (strncmp (name, *p, strlen (*p)) == 0) + goto next; + } + + /* 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: + g_free (name); + } + + 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 occured, kill dhclient instance with pid %d\n", 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 = 0; + gint64 time_end; + + nm_g_type_init (); + + /* FIXME: g_dbus_connection_new_for_address_sync() tries to connect to the socket in + * non-blocking mode, which can easily fail with EAGAIN, causing the creation of the + * socket to fail with "Could not connect: Resource temporarily unavailable". + * + * We should instead create the GIOStream ourself and block on connecting to + * the socket. */ + connection = g_dbus_connection_new_for_address_sync ("unix:path=" NMRUNDIR "/private-dhcp", + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, NULL, &error); + if (!connection) { + 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 */ + +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 (); + + /* 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); + g_usleep (NM_MIN (NM_CLAMP ((gint64) (100L * (1L << try_count)), 5000, 25000), 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 compatibilty, 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/dhcp/nm-dhcp-listener.c b/src/dhcp/nm-dhcp-listener.c new file mode 100644 index 00000000..56bd9d17 --- /dev/null +++ b/src/dhcp/nm-dhcp-listener.c @@ -0,0 +1,345 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright 2014 - 2016 Red Hat, Inc. + * + */ + +#include "nm-default.h" + +#include "nm-dhcp-listener.h" + +#include <sys/socket.h> +#include <sys/wait.h> +#include <signal.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> + +#include "nm-dhcp-helper-api.h" +#include "nm-dhcp-client.h" +#include "nm-core-internal.h" +#include "nm-bus-manager.h" +#include "NetworkManagerUtils.h" + +#define PRIV_SOCK_PATH NMRUNDIR "/private-dhcp" +#define PRIV_SOCK_TAG "dhcp" + +/*****************************************************************************/ + +const NMDhcpClientFactory *const _nm_dhcp_manager_factories[3] = { + /* the order here matters, as we will try the plugins in this order to find + * the first available plugin. */ + +#if WITH_DHCLIENT + &_nm_dhcp_client_factory_dhclient, +#endif +#if WITH_DHCPCD + &_nm_dhcp_client_factory_dhcpcd, +#endif + &_nm_dhcp_client_factory_internal, +}; + +/*****************************************************************************/ + +typedef struct { + NMBusManager * 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), \ + "%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 (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); + char *iface = NULL; + char *pid_str = NULL; + char *reason = NULL; + gint pid; + gboolean handled = FALSE; + GVariant *options; + + if (!nm_streq0 (interface_name, NM_DHCP_HELPER_SERVER_INTERFACE_NAME)) + g_return_if_reached (); + if (!nm_streq0 (method_name, NM_DHCP_HELPER_SERVER_METHOD_NOTIFY)) + g_return_if_reached (); + if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(a{sv})"))) + g_return_if_reached (); + + g_variant_get (parameters, "(@a{sv})", &options); + + iface = get_option (options, "interface"); + if (iface == NULL) { + _LOGW ("dhcp-event: didn't have associated interface."); + goto out; + } + + 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 ? pid_str : "(null)"); + goto out; + } + + reason = get_option (options, "reason"); + if (reason == NULL) { + _LOGW ("dhcp-event: (pid %d) DHCP event didn't have a reason", pid); + goto out; + } + + 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); + } + +out: + g_free (iface); + g_free (pid_str); + g_free (reason); + g_variant_unref (options); + g_dbus_method_invocation_return_value (invocation, NULL); +} + +static guint +_dbus_connection_register_object (NMDhcpListener *self, + GDBusConnection *connection, + GError **error) +{ + static GDBusArgInfo arg_info_notify_in = { + .ref_count = -1, + .name = "data", + .signature = "a{sv}", + .annotations = NULL, + }; + static GDBusArgInfo *arg_infos_notify[] = { + &arg_info_notify_in, + NULL, + }; + static GDBusMethodInfo method_info_notify = { + .ref_count = -1, + .name = NM_DHCP_HELPER_SERVER_METHOD_NOTIFY, + .in_args = arg_infos_notify, + .out_args = NULL, + .annotations = NULL, + }; + static GDBusMethodInfo *method_infos[] = { + &method_info_notify, + NULL, + }; + static GDBusInterfaceInfo interface_info = { + .ref_count = -1, + .name = NM_DHCP_HELPER_SERVER_INTERFACE_NAME, + .methods = method_infos, + .signals = NULL, + .properties = NULL, + .annotations = NULL, + }; + + static GDBusInterfaceVTable interface_vtable = { + .method_call = _method_call, + .get_property = NULL, + .set_property = NULL, + }; + + return g_dbus_connection_register_object (connection, + NM_DHCP_HELPER_SERVER_OBJECT_PATH, + &interface_info, + &interface_vtable, + self, + NULL, + error); +} + +static void +new_connection_cb (NMBusManager *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 (NMBusManager *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 (NULL, NULL); + + priv->dbus_mgr = nm_bus_manager_get (); + + /* Register the socket our DHCP clients will return lease info on */ + nm_bus_manager_private_server_register (priv->dbus_mgr, PRIV_SOCK_PATH, PRIV_SOCK_TAG); + priv->new_conn_id = g_signal_connect (priv->dbus_mgr, + NM_BUS_MANAGER_PRIVATE_CONNECTION_NEW "::" PRIV_SOCK_TAG, + G_CALLBACK (new_connection_cb), + self); + priv->dis_conn_id = g_signal_connect (priv->dbus_mgr, + NM_BUS_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 ((NMDhcpListener *) 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); + priv->dbus_mgr = NULL; + + g_clear_pointer (&priv->connections, g_hash_table_destroy); + + 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/dhcp/nm-dhcp-listener.h b/src/dhcp/nm-dhcp-listener.h new file mode 100644 index 00000000..3018b97a --- /dev/null +++ b/src/dhcp/nm-dhcp-listener.h @@ -0,0 +1,36 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright 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/dhcp/nm-dhcp-manager.c b/src/dhcp/nm-dhcp-manager.c new file mode 100644 index 00000000..9c1fbb38 --- /dev/null +++ b/src/dhcp/nm-dhcp-manager.c @@ -0,0 +1,442 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-dhcp-manager.c - Handle the DHCP daemon for NetworkManager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2005 - 2013 Red Hat, Inc. + * Copyright (C) 2006 - 2008 Novell, Inc. + * + */ + +#include "nm-default.h" + +#include "nm-dhcp-manager.h" + +#include <sys/socket.h> +#include <sys/wait.h> +#include <signal.h> +#include <string.h> +#include <stdlib.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdio.h> + +#include "nm-config.h" +#include "NetworkManagerUtils.h" + +#define DHCP_TIMEOUT 45 /* default DHCP timeout, in seconds */ + +/*****************************************************************************/ + +typedef struct { + const NMDhcpClientFactory *client_factory; + GHashTable * clients; + char * default_hostname; +} 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) + +/*****************************************************************************/ + +/* 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 NMDhcpClient * +get_client_for_ifindex (NMDhcpManager *manager, int ifindex, gboolean ip6) +{ + NMDhcpManagerPrivate *priv; + GHashTableIter iter; + gpointer value; + + 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); + + g_hash_table_iter_init (&iter, priv->clients); + while (g_hash_table_iter_next (&iter, NULL, &value)) { + NMDhcpClient *candidate = NM_DHCP_CLIENT (value); + + if ( nm_dhcp_client_get_ifindex (candidate) == ifindex + && nm_dhcp_client_get_ipv6 (candidate) == ip6) + return candidate; + } + + return NULL; +} + +static void client_state_changed (NMDhcpClient *client, + NMDhcpState state, + GObject *ip_config, + GVariant *options, + const char *event_id, + NMDhcpManager *self); + +static void +remove_client (NMDhcpManager *self, NMDhcpClient *client) +{ + g_signal_handlers_disconnect_by_func (client, client_state_changed, 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. + */ + + g_hash_table_remove (NM_DHCP_MANAGER_GET_PRIVATE (self)->clients, client); +} + +static void +client_state_changed (NMDhcpClient *client, + NMDhcpState state, + GObject *ip_config, + GVariant *options, + const char *event_id, + NMDhcpManager *self) +{ + if (state >= NM_DHCP_STATE_TIMEOUT) + remove_client (self, client); +} + +static NMDhcpClient * +client_start (NMDhcpManager *self, + const char *iface, + int ifindex, + const GByteArray *hwaddr, + const char *uuid, + guint32 priority, + gboolean ipv6, + const struct in6_addr *ipv6_ll_addr, + const char *dhcp_client_id, + guint32 timeout, + const char *dhcp_anycast_addr, + const char *hostname, + const char *fqdn, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + const char *last_ip4_address, + guint needed_prefixes) +{ + NMDhcpManagerPrivate *priv; + NMDhcpClient *client; + gboolean success = FALSE; + + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (NM_IS_DHCP_MANAGER (self), NULL); + g_return_val_if_fail (ifindex > 0, NULL); + g_return_val_if_fail (uuid != NULL, NULL); + + priv = NM_DHCP_MANAGER_GET_PRIVATE (self); + + /* Ensure we have a usable DHCP client */ + if (!priv->client_factory) + return NULL; + + /* Kill any old client instance */ + client = get_client_for_ifindex (self, ifindex, ipv6); + if (client) { + g_object_ref (client); + remove_client (self, client); + nm_dhcp_client_stop (client, FALSE); + g_object_unref (client); + } + + /* And make a new one */ + client = g_object_new (priv->client_factory->get_type (), + NM_DHCP_CLIENT_INTERFACE, iface, + NM_DHCP_CLIENT_IFINDEX, ifindex, + NM_DHCP_CLIENT_HWADDR, hwaddr, + NM_DHCP_CLIENT_IPV6, ipv6, + NM_DHCP_CLIENT_UUID, uuid, + NM_DHCP_CLIENT_PRIORITY, priority, + NM_DHCP_CLIENT_TIMEOUT, timeout ? timeout : DHCP_TIMEOUT, + NULL); + g_hash_table_insert (NM_DHCP_MANAGER_GET_PRIVATE (self)->clients, client, g_object_ref (client)); + g_signal_connect (client, NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED, G_CALLBACK (client_state_changed), self); + + if (ipv6) + success = nm_dhcp_client_start_ip6 (client, dhcp_anycast_addr, ipv6_ll_addr, hostname, info_only, privacy, needed_prefixes); + else + success = nm_dhcp_client_start_ip4 (client, dhcp_client_id, dhcp_anycast_addr, hostname, fqdn, last_ip4_address); + + if (!success) { + remove_client (self, client); + client = NULL; + } + + return client; +} + +static const char * +get_send_hostname (NMDhcpManager *self, const char *setting_hostname) +{ + NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE (self); + + /* Always prefer the explicit dhcp-send-hostname if given */ + return setting_hostname ? setting_hostname : priv->default_hostname; +} + +/* Caller owns a reference to the NMDhcpClient on return */ +NMDhcpClient * +nm_dhcp_manager_start_ip4 (NMDhcpManager *self, + const char *iface, + int ifindex, + const GByteArray *hwaddr, + const char *uuid, + guint32 priority, + gboolean send_hostname, + const char *dhcp_hostname, + const char *dhcp_fqdn, + const char *dhcp_client_id, + guint32 timeout, + const char *dhcp_anycast_addr, + const char *last_ip_address) +{ + const char *hostname = NULL; + const char *fqdn = NULL; + + g_return_val_if_fail (NM_IS_DHCP_MANAGER (self), NULL); + + if (send_hostname) { + hostname = get_send_hostname (self, dhcp_hostname); + fqdn = dhcp_fqdn; + } + return client_start (self, iface, ifindex, hwaddr, uuid, priority, FALSE, NULL, + dhcp_client_id, timeout, dhcp_anycast_addr, hostname, + fqdn, FALSE, 0, last_ip_address, 0); +} + +/* Caller owns a reference to the NMDhcpClient on return */ +NMDhcpClient * +nm_dhcp_manager_start_ip6 (NMDhcpManager *self, + const char *iface, + int ifindex, + const GByteArray *hwaddr, + const struct in6_addr *ll_addr, + const char *uuid, + guint32 priority, + gboolean send_hostname, + const char *dhcp_hostname, + guint32 timeout, + const char *dhcp_anycast_addr, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes) +{ + const char *hostname = NULL; + + g_return_val_if_fail (NM_IS_DHCP_MANAGER (self), NULL); + + if (send_hostname) + hostname = get_send_hostname (self, dhcp_hostname); + return client_start (self, iface, ifindex, hwaddr, uuid, priority, TRUE, + ll_addr, NULL, timeout, dhcp_anycast_addr, hostname, NULL, info_only, + privacy, NULL, needed_prefixes); +} + +void +nm_dhcp_manager_set_default_hostname (NMDhcpManager *manager, const char *hostname) +{ + NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE (manager); + + g_clear_pointer (&priv->default_hostname, g_free); + + /* Never send 'localhost'-type names to the DHCP server */ + if (!nm_utils_is_specific_hostname (hostname)) + return; + + priv->default_hostname = g_strdup (hostname); +} + +GSList * +nm_dhcp_manager_get_lease_ip_configs (NMDhcpManager *self, + const char *iface, + int ifindex, + const char *uuid, + gboolean ipv6, + guint32 default_route_metric) +{ + NMDhcpManagerPrivate *priv; + + g_return_val_if_fail (NM_IS_DHCP_MANAGER (self), NULL); + g_return_val_if_fail (iface != NULL, NULL); + g_return_val_if_fail (ifindex >= -1, NULL); + g_return_val_if_fail (uuid != NULL, NULL); + + priv = NM_DHCP_MANAGER_GET_PRIVATE (self); + if ( priv->client_factory + && priv->client_factory->get_lease_ip_configs) + return priv->client_factory->get_lease_ip_configs (iface, ifindex, uuid, ipv6, default_route_metric); + return NULL; +} + +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); + +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; + + 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, g_type_name (f->get_type ()), + _client_factory_available (f) ? "" : " (not available)"); + } + + /* 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)) { + 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; + } + } + } + + nm_assert (client_factory); + + nm_log_info (LOGD_DHCP, "dhcp-init: Using DHCP client '%s'", client_factory->name); + + priv->client_factory = client_factory; + priv->clients = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, + (GDestroyNotify) g_object_unref); +} + +static void +dispose (GObject *object) +{ + NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE ((NMDhcpManager *) object); + GList *values, *iter; + + if (priv->clients) { + values = g_hash_table_get_values (priv->clients); + for (iter = values; iter; iter = g_list_next (iter)) + remove_client (NM_DHCP_MANAGER (object), NM_DHCP_CLIENT (iter->data)); + g_list_free (values); + } + + G_OBJECT_CLASS (nm_dhcp_manager_parent_class)->dispose (object); +} + +static void +finalize (GObject *object) +{ + NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE ((NMDhcpManager *) object); + + g_free (priv->default_hostname); + + if (priv->clients) + g_hash_table_destroy (priv->clients); + + G_OBJECT_CLASS (nm_dhcp_manager_parent_class)->finalize (object); +} + +static void +nm_dhcp_manager_class_init (NMDhcpManagerClass *manager_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (manager_class); + + object_class->finalize = finalize; + object_class->dispose = dispose; +} diff --git a/src/dhcp/nm-dhcp-manager.h b/src/dhcp/nm-dhcp-manager.h new file mode 100644 index 00000000..66fdd145 --- /dev/null +++ b/src/dhcp/nm-dhcp-manager.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* nm-dhcp-manager.c - Handle the DHCP daemon for NetworkManager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 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-dhcp4-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, + const char *iface, + int ifindex, + const GByteArray *hwaddr, + const char *uuid, + guint32 priority, + gboolean send_hostname, + const char *dhcp_hostname, + const char *dhcp_fqdn, + const char *dhcp_client_id, + guint32 timeout, + const char *dhcp_anycast_addr, + const char *last_ip_address); + +NMDhcpClient * nm_dhcp_manager_start_ip6 (NMDhcpManager *manager, + const char *iface, + int ifindex, + const GByteArray *hwaddr, + const struct in6_addr *ll_addr, + const char *uuid, + guint32 priority, + gboolean send_hostname, + const char *dhcp_hostname, + guint32 timeout, + const char *dhcp_anycast_addr, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + guint needed_prefixes); + +GSList * nm_dhcp_manager_get_lease_ip_configs (NMDhcpManager *self, + const char *iface, + int ifindex, + const char *uuid, + gboolean ipv6, + guint32 default_route_metric); + +/* For testing only */ +extern const char* nm_dhcp_helper_path; + +extern const NMDhcpClientFactory *const _nm_dhcp_manager_factories[3]; + +#endif /* __NETWORKMANAGER_DHCP_MANAGER_H__ */ diff --git a/src/dhcp/nm-dhcp-systemd.c b/src/dhcp/nm-dhcp-systemd.c new file mode 100644 index 00000000..7067275b --- /dev/null +++ b/src/dhcp/nm-dhcp-systemd.c @@ -0,0 +1,1070 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include "nm-default.h" + +#include <string.h> +#include <stdlib.h> +#include <errno.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-utils.h" +#include "nm-dhcp-utils.h" +#include "NetworkManagerUtils.h" +#include "platform/nm-platform.h" +#include "nm-dhcp-client-logging.h" +#include "systemd/nm-sd.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; + + gboolean privacy; + gboolean info_only; +} 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) + +/*****************************************************************************/ + +#define DHCP_OPTION_NIS_DOMAIN 40 +#define DHCP_OPTION_NIS_SERVERS 41 +#define DHCP_OPTION_DOMAIN_SEARCH 119 +#define DHCP_OPTION_MS_ROUTES 249 +#define DHCP_OPTION_WPAD 252 + +/* Internal values */ +#define DHCP_OPTION_IP_ADDRESS 1024 +#define DHCP_OPTION_EXPIRY 1025 +#define DHCP6_OPTION_IP_ADDRESS 1026 +#define DHCP6_OPTION_PREFIXLEN 1027 +#define DHCP6_OPTION_PREFERRED_LIFE 1028 +#define DHCP6_OPTION_MAX_LIFE 1029 +#define DHCP6_OPTION_STARTS 1030 +#define DHCP6_OPTION_LIFE_STARTS 1031 +#define DHCP6_OPTION_RENEW 1032 +#define DHCP6_OPTION_REBIND 1033 +#define DHCP6_OPTION_IAID 1034 + +typedef struct { + guint num; + const char *name; + gboolean include; +} ReqOption; + +#define REQPREFIX "requested_" + +static const ReqOption dhcp4_requests[] = { + { SD_DHCP_OPTION_SUBNET_MASK, REQPREFIX "subnet_mask", TRUE }, + { SD_DHCP_OPTION_TIME_OFFSET, REQPREFIX "time_offset", TRUE }, + { SD_DHCP_OPTION_ROUTER, REQPREFIX "routers", TRUE }, + { SD_DHCP_OPTION_DOMAIN_NAME_SERVER, REQPREFIX "domain_name_servers", TRUE }, + { SD_DHCP_OPTION_HOST_NAME, REQPREFIX "host_name", TRUE }, + { SD_DHCP_OPTION_DOMAIN_NAME, REQPREFIX "domain_name", TRUE }, + { SD_DHCP_OPTION_INTERFACE_MTU, REQPREFIX "interface_mtu", TRUE }, + { SD_DHCP_OPTION_BROADCAST, REQPREFIX "broadcast_address", TRUE }, + { SD_DHCP_OPTION_STATIC_ROUTE, REQPREFIX "static_routes", TRUE }, + { DHCP_OPTION_NIS_DOMAIN, REQPREFIX "nis_domain", TRUE }, + { DHCP_OPTION_NIS_SERVERS, REQPREFIX "nis_servers", TRUE }, + { SD_DHCP_OPTION_NTP_SERVER, REQPREFIX "ntp_servers", TRUE }, + { SD_DHCP_OPTION_SERVER_IDENTIFIER, REQPREFIX "dhcp_server_identifier", TRUE }, + { DHCP_OPTION_DOMAIN_SEARCH, REQPREFIX "domain_search", TRUE }, + { SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE, REQPREFIX "rfc3442_classless_static_routes", TRUE }, + { DHCP_OPTION_MS_ROUTES, REQPREFIX "ms_classless_static_routes", TRUE }, + { DHCP_OPTION_WPAD, REQPREFIX "wpad", TRUE }, + + /* Internal values */ + { SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, REQPREFIX "expiry", FALSE }, + { SD_DHCP_OPTION_CLIENT_IDENTIFIER, REQPREFIX "dhcp_client_identifier", FALSE }, + { DHCP_OPTION_IP_ADDRESS, REQPREFIX "ip_address", FALSE }, + { 0, NULL, FALSE } +}; + +static const ReqOption dhcp6_requests[] = { + { SD_DHCP6_OPTION_CLIENTID, REQPREFIX "dhcp6_client_id", TRUE }, + + /* Don't request server ID by default; some servers don't reply to + * Information Requests that request the Server ID. + */ + { SD_DHCP6_OPTION_SERVERID, REQPREFIX "dhcp6_server_id", FALSE }, + + { SD_DHCP6_OPTION_DNS_SERVERS, REQPREFIX "dhcp6_name_servers", TRUE }, + { SD_DHCP6_OPTION_DOMAIN_LIST, REQPREFIX "dhcp6_domain_search", TRUE }, + { SD_DHCP6_OPTION_SNTP_SERVERS, REQPREFIX "dhcp6_sntp_servers", TRUE }, + + /* Internal values */ + { DHCP6_OPTION_IP_ADDRESS, REQPREFIX "ip6_address", FALSE }, + { DHCP6_OPTION_PREFIXLEN, REQPREFIX "ip6_prefixlen", FALSE }, + { DHCP6_OPTION_PREFERRED_LIFE, REQPREFIX "preferred_life", FALSE }, + { DHCP6_OPTION_MAX_LIFE, REQPREFIX "max_life", FALSE }, + { DHCP6_OPTION_STARTS, REQPREFIX "starts", FALSE }, + { DHCP6_OPTION_LIFE_STARTS, REQPREFIX "life_starts", FALSE }, + { DHCP6_OPTION_RENEW, REQPREFIX "renew", FALSE }, + { DHCP6_OPTION_REBIND, REQPREFIX "rebind", FALSE }, + { DHCP6_OPTION_IAID, REQPREFIX "iaid", FALSE }, + { 0, NULL, FALSE } +}; + +static void +take_option (GHashTable *options, + const ReqOption *requests, + guint option, + char *value) +{ + guint i; + + g_return_if_fail (value != NULL); + + for (i = 0; requests[i].name; i++) { + if (requests[i].num == option) { + g_hash_table_insert (options, + (gpointer) (requests[i].name + NM_STRLEN (REQPREFIX)), + value); + break; + } + } + /* Option should always be found */ + g_assert (requests[i].name); +} + +static void +add_option (GHashTable *options, const ReqOption *requests, guint option, const char *value) +{ + if (options) + take_option (options, requests, option, g_strdup (value)); +} + +static void +add_option_u32 (GHashTable *options, const ReqOption *requests, guint option, guint32 value) +{ + if (options) + take_option (options, requests, option, g_strdup_printf ("%u", value)); +} + +static void +add_option_u64 (GHashTable *options, const ReqOption *requests, guint option, guint64 value) +{ + if (options) + take_option (options, requests, option, g_strdup_printf ("%" G_GUINT64_FORMAT, value)); +} + +static void +add_requests_to_options (GHashTable *options, const ReqOption *requests) +{ + guint i; + + for (i = 0; options && requests[i].name; i++) { + if (requests[i].include) + g_hash_table_insert (options, (gpointer) requests[i].name, g_strdup ("1")); + } +} + +#define LOG_LEASE(domain, ...) \ +G_STMT_START { \ + if (log_lease) { \ + _LOG2I ((domain), (iface), __VA_ARGS__); \ + } \ +} G_STMT_END + +static NMIP4Config * +lease_to_ip4_config (const char *iface, + int ifindex, + sd_dhcp_lease *lease, + GHashTable *options, + guint32 default_priority, + gboolean log_lease, + GError **error) +{ + NMIP4Config *ip4_config = NULL; + struct in_addr tmp_addr; + const struct in_addr *addr_list; + char buf[INET_ADDRSTRLEN]; + const char *str; + guint32 lifetime = 0, i; + NMPlatformIP4Address address; + GString *l; + gs_free sd_dhcp_route **routes = NULL; + guint16 mtu; + int r, num; + guint64 end_time; + const void *data; + gsize data_len; + gboolean metered = FALSE; + gboolean static_default_gateway = FALSE; + + g_return_val_if_fail (lease != NULL, NULL); + + ip4_config = nm_ip4_config_new (ifindex); + + /* Address */ + sd_dhcp_lease_get_address (lease, &tmp_addr); + memset (&address, 0, sizeof (address)); + address.address = tmp_addr.s_addr; + address.peer_address = tmp_addr.s_addr; + str = nm_utils_inet4_ntop (tmp_addr.s_addr, NULL); + LOG_LEASE (LOGD_DHCP4, " address %s", str); + add_option (options, dhcp4_requests, DHCP_OPTION_IP_ADDRESS, str); + + /* Prefix/netmask */ + sd_dhcp_lease_get_netmask (lease, &tmp_addr); + address.plen = nm_utils_ip4_netmask_to_prefix (tmp_addr.s_addr); + LOG_LEASE (LOGD_DHCP4, " plen %d", address.plen); + add_option (options, + dhcp4_requests, + SD_DHCP_OPTION_SUBNET_MASK, + nm_utils_inet4_ntop (tmp_addr.s_addr, NULL)); + + /* Lease time */ + sd_dhcp_lease_get_lifetime (lease, &lifetime); + address.timestamp = nm_utils_get_monotonic_timestamp_s (); + address.lifetime = address.preferred = lifetime; + end_time = (guint64) time (NULL) + lifetime; + LOG_LEASE (LOGD_DHCP4, " expires in %" G_GUINT32_FORMAT " seconds", lifetime); + add_option_u64 (options, + dhcp4_requests, + SD_DHCP_OPTION_IP_ADDRESS_LEASE_TIME, + end_time); + + address.addr_source = NM_IP_CONFIG_SOURCE_DHCP; + nm_ip4_config_add_address (ip4_config, &address); + + /* DNS Servers */ + num = sd_dhcp_lease_get_dns (lease, &addr_list); + if (num > 0) { + l = g_string_sized_new (30); + for (i = 0; i < num; i++) { + if (addr_list[i].s_addr) { + nm_ip4_config_add_nameserver (ip4_config, addr_list[i].s_addr); + str = nm_utils_inet4_ntop (addr_list[i].s_addr, NULL); + LOG_LEASE (LOGD_DHCP4, " nameserver '%s'", str); + g_string_append_printf (l, "%s%s", l->len ? " " : "", str); + } + } + if (l->len) + add_option (options, dhcp4_requests, SD_DHCP_OPTION_DOMAIN_NAME_SERVER, l->str); + g_string_free (l, TRUE); + } + + /* Domain Name */ + r = sd_dhcp_lease_get_domainname (lease, &str); + if (r == 0) { + /* Multiple domains sometimes stuffed into option 15 "Domain Name". + * As systemd escapes such characters, split them at \\032. */ + char **domains = g_strsplit (str, "\\032", 0); + char **s; + + for (s = domains; *s; s++) { + LOG_LEASE (LOGD_DHCP4, " domain name '%s'", *s); + nm_ip4_config_add_domain (ip4_config, *s); + } + g_strfreev (domains); + add_option (options, dhcp4_requests, SD_DHCP_OPTION_DOMAIN_NAME, str); + } + + /* Hostname */ + r = sd_dhcp_lease_get_hostname (lease, &str); + if (r == 0) { + LOG_LEASE (LOGD_DHCP4, " hostname '%s'", str); + add_option (options, dhcp4_requests, SD_DHCP_OPTION_HOST_NAME, str); + } + + /* Routes */ + num = sd_dhcp_lease_get_routes (lease, &routes); + if (num > 0) { + l = g_string_sized_new (30); + for (i = 0; i < num; i++) { + NMPlatformIP4Route route = { 0 }; + const char *gw_str; + guint8 plen; + struct in_addr a; + + if (sd_dhcp_route_get_destination (routes[i], &a) < 0) + continue; + route.network = a.s_addr; + + if ( sd_dhcp_route_get_destination_prefix_length (routes[i], &plen) < 0 + || plen > 32) + continue; + route.plen = plen; + + if (sd_dhcp_route_get_gateway (routes[i], &a) < 0) + continue; + route.gateway = a.s_addr; + + if (route.plen) { + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = default_priority; + nm_ip4_config_add_route (ip4_config, &route); + + str = nm_utils_inet4_ntop (route.network, buf); + gw_str = nm_utils_inet4_ntop (route.gateway, NULL); + LOG_LEASE (LOGD_DHCP4, " static route %s/%d gw %s", str, route.plen, gw_str); + + g_string_append_printf (l, "%s%s/%d %s", l->len ? " " : "", str, route.plen, gw_str); + } else { + if (!static_default_gateway) { + static_default_gateway = TRUE; + nm_ip4_config_set_gateway (ip4_config, route.gateway); + + str = nm_utils_inet4_ntop (route.gateway, NULL); + LOG_LEASE (LOGD_DHCP4, " gateway %s", str); + add_option (options, dhcp4_requests, SD_DHCP_OPTION_ROUTER, str); + } + } + } + if (l->len) + add_option (options, dhcp4_requests, SD_DHCP_OPTION_CLASSLESS_STATIC_ROUTE, l->str); + g_string_free (l, TRUE); + } + + /* 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). + */ + /* Gateway */ + if (!static_default_gateway) { + r = sd_dhcp_lease_get_router (lease, &tmp_addr); + if (r == 0) { + nm_ip4_config_set_gateway (ip4_config, tmp_addr.s_addr); + str = nm_utils_inet4_ntop (tmp_addr.s_addr, NULL); + LOG_LEASE (LOGD_DHCP4, " gateway %s", str); + add_option (options, dhcp4_requests, SD_DHCP_OPTION_ROUTER, str); + } + } + + /* MTU */ + r = sd_dhcp_lease_get_mtu (lease, &mtu); + if (r == 0 && mtu) { + nm_ip4_config_set_mtu (ip4_config, mtu, NM_IP_CONFIG_SOURCE_DHCP); + add_option_u32 (options, dhcp4_requests, SD_DHCP_OPTION_INTERFACE_MTU, mtu); + LOG_LEASE (LOGD_DHCP4, " mtu %u", mtu); + } + + /* NTP servers */ + num = sd_dhcp_lease_get_ntp (lease, &addr_list); + if (num > 0) { + l = g_string_sized_new (30); + for (i = 0; i < num; i++) { + str = nm_utils_inet4_ntop (addr_list[i].s_addr, buf); + LOG_LEASE (LOGD_DHCP4, " ntp server '%s'", str); + g_string_append_printf (l, "%s%s", l->len ? " " : "", str); + } + add_option (options, dhcp4_requests, SD_DHCP_OPTION_NTP_SERVER, l->str); + g_string_free (l, TRUE); + } + + r = sd_dhcp_lease_get_vendor_specific (lease, &data, &data_len); + if (r >= 0) + metered = !!memmem (data, data_len, "ANDROID_METERED", NM_STRLEN ("ANDROID_METERED")); + nm_ip4_config_set_metered (ip4_config, metered); + + return ip4_config; +} + +/*****************************************************************************/ + +static char * +get_leasefile_path (const char *iface, const char *uuid, gboolean ipv6) +{ + return g_strdup_printf (NMSTATEDIR "/internal%s-%s-%s.lease", + ipv6 ? "6" : "", + uuid, + iface); +} + +static GSList * +nm_dhcp_systemd_get_lease_ip_configs (const char *iface, + int ifindex, + const char *uuid, + gboolean ipv6, + guint32 default_route_metric) +{ + GSList *leases = NULL; + gs_free char *path = NULL; + sd_dhcp_lease *lease = NULL; + NMIP4Config *ip4_config; + int r; + + if (ipv6) + return NULL; + + path = get_leasefile_path (iface, uuid, FALSE); + r = dhcp_lease_load (&lease, path); + if (r == 0 && lease) { + ip4_config = lease_to_ip4_config (iface, ifindex, lease, NULL, default_route_metric, FALSE, NULL); + if (ip4_config) + leases = g_slist_append (leases, ip4_config); + sd_dhcp_lease_unref (lease); + } + + return leases; +} + +/*****************************************************************************/ + +static void +_save_client_id (NMDhcpSystemd *self, + uint8_t type, + const uint8_t *client_id, + size_t len) +{ + gs_unref_bytes GBytes *b = NULL; + gs_free char *buf = NULL; + + g_return_if_fail (self != NULL); + g_return_if_fail (client_id != NULL); + g_return_if_fail (len > 0); + + if (!nm_dhcp_client_get_client_id (NM_DHCP_CLIENT (self))) { + buf = g_malloc (len + 1); + buf[0] = type; + memcpy (buf + 1, client_id, len); + b = g_bytes_new (buf, len + 1); + nm_dhcp_client_set_client_id (NM_DHCP_CLIENT (self), b); + } +} + +static void +bound4_handle (NMDhcpSystemd *self) +{ + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE (self); + const char *iface = nm_dhcp_client_get_iface (NM_DHCP_CLIENT (self)); + sd_dhcp_lease *lease; + NMIP4Config *ip4_config; + GHashTable *options; + GError *error = NULL; + int r; + + r = sd_dhcp_client_get_lease (priv->client4, &lease); + if (r < 0 || !lease) { + _LOGW ("no lease!"); + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + _LOGD ("lease available"); + + options = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); + ip4_config = lease_to_ip4_config (iface, + nm_dhcp_client_get_ifindex (NM_DHCP_CLIENT (self)), + lease, + options, + nm_dhcp_client_get_priority (NM_DHCP_CLIENT (self)), + TRUE, + &error); + if (ip4_config) { + const uint8_t *client_id = NULL; + size_t client_id_len = 0; + uint8_t type = 0; + + add_requests_to_options (options, dhcp4_requests); + dhcp_lease_save (lease, priv->lease_file); + + sd_dhcp_client_get_client_id(priv->client4, &type, &client_id, &client_id_len); + if (client_id) + _save_client_id (self, type, client_id, client_id_len); + + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), + NM_DHCP_STATE_BOUND, + G_OBJECT (ip4_config), + options); + } else { + _LOGW ("%s", error->message); + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL); + g_clear_error (&error); + } + + g_hash_table_destroy (options); + g_clear_object (&ip4_config); +} + +static void +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); + + g_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: + case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE: + bound4_handle (self); + break; + default: + _LOGW ("unhandled DHCP event %d", event); + break; + } +} + +static guint16 +get_arp_type (const GByteArray *hwaddr) +{ + if (hwaddr->len == ETH_ALEN) + return ARPHRD_ETHER; + else if (hwaddr->len == INFINIBAND_ALEN) + return ARPHRD_INFINIBAND; + else + return ARPHRD_NONE; +} + +static gboolean +ip4_start (NMDhcpClient *client, const char *dhcp_anycast_addr, const char *last_ip4_address) +{ + NMDhcpSystemd *self = NM_DHCP_SYSTEMD (client); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE (self); + const char *iface = nm_dhcp_client_get_iface (client); + const GByteArray *hwaddr; + sd_dhcp_lease *lease = NULL; + GBytes *override_client_id; + const uint8_t *client_id = NULL; + size_t client_id_len = 0; + struct in_addr last_addr = { 0 }; + const char *hostname, *fqdn; + int r, i; + gboolean success = FALSE; + guint16 arp_type; + + g_assert (priv->client4 == NULL); + g_assert (priv->client6 == NULL); + + g_free (priv->lease_file); + priv->lease_file = get_leasefile_path (iface, nm_dhcp_client_get_uuid (client), FALSE); + + r = sd_dhcp_client_new (&priv->client4); + if (r < 0) { + _LOGW ("failed to create client (%d)", r); + return FALSE; + } + + _LOGT ("dhcp-client4: set %p", priv->client4); + + r = sd_dhcp_client_attach_event (priv->client4, NULL, 0); + if (r < 0) { + _LOGW ("failed to attach event (%d)", r); + goto error; + } + + hwaddr = nm_dhcp_client_get_hw_addr (client); + if (hwaddr) { + arp_type= get_arp_type (hwaddr); + if (arp_type == ARPHRD_NONE) { + _LOGW ("failed to determine ARP type"); + goto error; + } + + r = sd_dhcp_client_set_mac (priv->client4, + hwaddr->data, + hwaddr->len, + arp_type); + if (r < 0) { + _LOGW ("failed to set MAC address (%d)", r); + goto error; + } + } + + r = sd_dhcp_client_set_ifindex (priv->client4, nm_dhcp_client_get_ifindex (client)); + if (r < 0) { + _LOGW ("failed to set ififindex (%d)", r); + goto error; + } + + r = sd_dhcp_client_set_callback (priv->client4, dhcp_event_cb, client); + if (r < 0) { + _LOGW ("failed to set callback (%d)", r); + goto error; + } + + r = sd_dhcp_client_set_request_broadcast (priv->client4, true); + if (r < 0) { + _LOGW ("failed to enable broadcast mode (%d)", r); + goto error; + } + + dhcp_lease_load (&lease, priv->lease_file); + + if (last_ip4_address) + inet_pton (AF_INET, last_ip4_address, &last_addr); + else if (lease) + sd_dhcp_lease_get_address (lease, &last_addr); + + if (last_addr.s_addr) { + r = sd_dhcp_client_set_request_address (priv->client4, &last_addr); + if (r < 0) { + _LOGW ("failed to set last IPv4 address (%d)", r); + goto error; + } + } + + override_client_id = nm_dhcp_client_get_client_id (client); + if (override_client_id) { + client_id = g_bytes_get_data (override_client_id, &client_id_len); + g_assert (client_id && client_id_len); + sd_dhcp_client_set_client_id (priv->client4, + client_id[0], + client_id + 1, + client_id_len - 1); + } else if (lease) { + r = sd_dhcp_lease_get_client_id (lease, (const void **) &client_id, &client_id_len); + if (r == 0 && client_id_len) { + sd_dhcp_client_set_client_id (priv->client4, + client_id[0], + client_id + 1, + client_id_len - 1); + _save_client_id (NM_DHCP_SYSTEMD (client), + client_id[0], + client_id + 1, + client_id_len - 1); + } + } + + + /* Add requested options */ + for (i = 0; dhcp4_requests[i].name; i++) { + if (dhcp4_requests[i].include) + sd_dhcp_client_set_request_option (priv->client4, dhcp4_requests[i].num); + } + + hostname = nm_dhcp_client_get_hostname (client); + if (hostname) { + char *prefix, *dot; + + prefix = strdup (hostname); + dot = strchr (prefix, '.'); + /* get rid of the domain */ + if (dot) + *dot = '\0'; + + r = sd_dhcp_client_set_hostname (priv->client4, prefix); + free (prefix); + + if (r < 0) { + _LOGW ("failed to set DHCP hostname (%d)", r); + goto error; + } + } + + fqdn = nm_dhcp_client_get_fqdn (client); + if (fqdn) { + r = sd_dhcp_client_set_hostname (priv->client4, fqdn); + if (r < 0) { + _LOGW ("failed to set DHCP FQDN (%d)", r); + goto error; + } + } + + r = sd_dhcp_client_start (priv->client4); + if (r < 0) { + _LOGW ("failed to start client (%d)", r); + goto error; + } + + nm_dhcp_client_start_timeout (client); + + success = TRUE; + +error: + sd_dhcp_lease_unref (lease); + if (!success) + priv->client4 = sd_dhcp_client_unref (priv->client4); + return success; +} + +static NMIP6Config * +lease_to_ip6_config (const char *iface, + int ifindex, + sd_dhcp6_lease *lease, + GHashTable *options, + gboolean log_lease, + gboolean info_only, + GError **error) +{ + struct in6_addr tmp_addr, *dns; + uint32_t lft_pref, lft_valid; + NMIP6Config *ip6_config; + const char *addr_str; + char **domains; + GString *str; + int num, i; + gint32 ts; + + g_return_val_if_fail (lease, NULL); + ip6_config = nm_ip6_config_new (ifindex); + ts = nm_utils_get_monotonic_timestamp_s (); + str = g_string_sized_new (30); + + /* Addresses */ + sd_dhcp6_lease_reset_address_iter (lease); + while (sd_dhcp6_lease_get_address (lease, &tmp_addr, &lft_pref, &lft_valid) >= 0) { + 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); + + addr_str = nm_utils_inet6_ntop (&tmp_addr, NULL); + g_string_append_printf (str, "%s%s", str->len ? " " : "", addr_str); + + LOG_LEASE (LOGD_DHCP6, + " address %s", + nm_platform_ip6_address_to_string (&address, NULL, 0)); + }; + + if (str->len) { + add_option (options, dhcp6_requests, DHCP6_OPTION_IP_ADDRESS, str->str); + g_string_set_size (str , 0); + } + + if (!info_only && nm_ip6_config_get_num_addresses (ip6_config) == 0) { + g_string_free (str, TRUE); + g_object_unref (ip6_config); + g_set_error_literal (error, + NM_MANAGER_ERROR, + NM_MANAGER_ERROR_FAILED, + "no address received in managed mode"); + return NULL; + } + + /* DNS servers */ + num = sd_dhcp6_lease_get_dns (lease, &dns); + if (num > 0) { + for (i = 0; i < num; i++) { + nm_ip6_config_add_nameserver (ip6_config, &dns[i]); + addr_str = nm_utils_inet6_ntop (&dns[i], NULL); + g_string_append_printf (str, "%s%s", str->len ? " " : "", addr_str); + LOG_LEASE (LOGD_DHCP6, " nameserver %s", addr_str); + } + add_option (options, dhcp6_requests, SD_DHCP6_OPTION_DNS_SERVERS, str->str); + g_string_set_size (str, 0); + } + + /* Search domains */ + num = sd_dhcp6_lease_get_domains (lease, &domains); + if (num > 0) { + for (i = 0; i < num; i++) { + nm_ip6_config_add_search (ip6_config, domains[i]); + g_string_append_printf (str, "%s%s", str->len ? " " : "", domains[i]); + LOG_LEASE (LOGD_DHCP6, " domain name '%s'", domains[i]); + } + add_option (options, dhcp6_requests, SD_DHCP6_OPTION_DOMAIN_LIST, str->str); + g_string_set_size (str, 0); + } + + g_string_free (str, TRUE); + + return ip6_config; +} + +static void +bound6_handle (NMDhcpSystemd *self) +{ + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE (self); + 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; + sd_dhcp6_lease *lease; + int r; + + r = sd_dhcp6_client_get_lease (priv->client6, &lease); + if (r < 0 || !lease) { + _LOGW (" no lease!"); + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL); + return; + } + + _LOGD ("lease available"); + + options = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); + ip6_config = lease_to_ip6_config (iface, + nm_dhcp_client_get_ifindex (NM_DHCP_CLIENT (self)), + lease, + options, + TRUE, + priv->info_only, + &error); + + if (ip6_config) { + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), + NM_DHCP_STATE_BOUND, + G_OBJECT (ip6_config), + options); + } else { + _LOGW ("%s", error->message); + nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL); + } +} + +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); + + g_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, + gboolean info_only, + NMSettingIP6ConfigPrivacy privacy, + const GByteArray *duid, + guint needed_prefixes) +{ + NMDhcpSystemd *self = NM_DHCP_SYSTEMD (client); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE (self); + const char *iface = nm_dhcp_client_get_iface (client); + const GByteArray *hwaddr; + int r, i; + + g_assert (priv->client4 == NULL); + g_assert (priv->client6 == NULL); + g_return_val_if_fail (duid != NULL, FALSE); + + g_free (priv->lease_file); + priv->lease_file = get_leasefile_path (iface, nm_dhcp_client_get_uuid (client), TRUE); + priv->info_only = info_only; + + r = sd_dhcp6_client_new (&priv->client6); + if (r < 0) { + _LOGW ("failed to create client (%d)", r); + return FALSE; + } + + if (needed_prefixes > 0) { + _LOGW ("dhcp-client6: prefix delegation not yet supported, won't supply %d prefixes\n", + needed_prefixes); + } + + _LOGT ("dhcp-client6: set %p", priv->client6); + + if (info_only) + sd_dhcp6_client_set_information_request (priv->client6, 1); + + /* NM stores the entire DUID which includes the uint16 "type", while systemd + * wants the type passed separately from the following data. + */ + r = sd_dhcp6_client_set_duid (priv->client6, + ntohs (((const guint16 *) duid->data)[0]), + duid->data + 2, + duid->len - 2); + if (r < 0) { + _LOGW ("failed to set DUID (%d)", r); + return FALSE; + } + + r = sd_dhcp6_client_attach_event (priv->client6, NULL, 0); + if (r < 0) { + _LOGW ("failed to attach event (%d)", r); + goto error; + } + + hwaddr = nm_dhcp_client_get_hw_addr (client); + if (hwaddr) { + r = sd_dhcp6_client_set_mac (priv->client6, + hwaddr->data, + hwaddr->len, + get_arp_type (hwaddr)); + if (r < 0) { + _LOGW ("failed to set MAC address (%d)", r); + goto error; + } + } + + r = sd_dhcp6_client_set_ifindex (priv->client6, nm_dhcp_client_get_ifindex (client)); + if (r < 0) { + _LOGW ("failed to set ifindex (%d)", r); + goto error; + } + + r = sd_dhcp6_client_set_callback (priv->client6, dhcp6_event_cb, client); + if (r < 0) { + _LOGW ("failed to set callback (%d)", r); + goto error; + } + + /* Add requested options */ + for (i = 0; dhcp6_requests[i].name; i++) { + if (dhcp6_requests[i].include) + sd_dhcp6_client_set_request_option (priv->client6, dhcp6_requests[i].num); + } + + r = sd_dhcp6_client_set_local_address (priv->client6, ll_addr); + if (r < 0) { + _LOGW ("failed to set local address (%d)", r); + goto error; + } + + r = sd_dhcp6_client_start (priv->client6); + if (r < 0) { + _LOGW ("failed to start client (%d)", r); + goto error; + } + + nm_dhcp_client_start_timeout (client); + + return TRUE; + +error: + sd_dhcp6_client_unref (priv->client6); + priv->client6 = NULL; + return FALSE; +} + +static void +stop (NMDhcpClient *client, gboolean release, const GByteArray *duid) +{ + NMDhcpSystemd *self = NM_DHCP_SYSTEMD (client); + NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE (self); + int r = 0; + + _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 ((NMDhcpSystemd *) object); + + g_clear_pointer (&priv->lease_file, g_free); + + 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_internal = { + .name = "internal", + .get_type = nm_dhcp_systemd_get_type, + .get_path = NULL, + .get_lease_ip_configs = nm_dhcp_systemd_get_lease_ip_configs, +}; diff --git a/src/dhcp/nm-dhcp-utils.c b/src/dhcp/nm-dhcp-utils.c new file mode 100644 index 00000000..e020ca33 --- /dev/null +++ b/src/dhcp/nm-dhcp-utils.c @@ -0,0 +1,779 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2005 - 2010 Red Hat, Inc. + * + */ + +#include "nm-default.h" + +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <arpa/inet.h> + +#include "nm-dhcp-utils.h" +#include "nm-utils.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 priority, + NMIP4Config *ip4_config, + guint32 *gwaddr) +{ + char **routes, **r; + gboolean have_routes = FALSE; + + routes = g_strsplit (str, " ", 0); + if (g_strv_length (routes) == 0) + goto out; + + if ((g_strv_length (routes) % 2) != 0) { + _LOG2W (LOGD_DHCP4, iface, " classless static routes provided, but invalid"); + goto out; + } + + 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 = rt_addr; + route.plen = rt_cidr; + route.gateway = rt_route; + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = priority; + nm_ip4_config_add_route (ip4_config, &route); + } + } + +out: + g_strfreev (routes); + return have_routes; +} + +static const char ** +process_dhclient_rfc3442_route (const char **octets, + NMPlatformIP4Route *route, + gboolean *success) +{ + const char **o = octets; + int addr_len = 0, i = 0; + long int tmp; + char *next_hop; + guint32 tmp_addr; + + *success = FALSE; + + if (!*o) + return o; /* no prefix */ + + tmp = strtol (*o, NULL, 10); + if (tmp < 0 || tmp > 32) /* 32 == max IP4 prefix length */ + return o; + + memset (route, 0, sizeof (*route)); + route->plen = tmp; + o++; + + if (tmp > 0) + addr_len = ((tmp - 1) / 8) + 1; + + /* ensure there's at least the address + next hop left */ + if (g_strv_length ((char **) o) < addr_len + 4) + goto error; + + if (tmp) { + const char *addr[4] = { "0", "0", "0", "0" }; + char *str_addr; + + 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) { + g_free (str_addr); + goto error; + } + g_free (str_addr); + tmp_addr &= nm_utils_ip4_prefix_to_netmask ((guint32) tmp); + route->network = tmp_addr; + } + + /* Handle next hop */ + next_hop = g_strjoin (".", o[0], o[1], o[2], o[3], NULL); + if (inet_pton (AF_INET, next_hop, &tmp_addr) <= 0) { + g_free (next_hop); + goto error; + } + route->gateway = tmp_addr; + g_free (next_hop); + + *success = TRUE; + return o + 4; /* advance to past the next hop */ + +error: + return o; +} + +static gboolean +ip4_process_dhclient_rfc3442_routes (const char *iface, + const char *str, + guint32 priority, + NMIP4Config *ip4_config, + guint32 *gwaddr) +{ + char **octets, **o; + gboolean have_routes = FALSE; + NMPlatformIP4Route route; + gboolean success; + + o = octets = g_strsplit_set (str, " .", 0); + if (g_strv_length (octets) < 5) { + _LOG2W (LOGD_DHCP4, iface, "ignoring invalid classless static routes '%s'", str); + goto out; + } + + while (*o) { + memset (&route, 0, sizeof (route)); + o = (char **) process_dhclient_rfc3442_route ((const char **) o, &route, &success); + if (!success) { + _LOG2W (LOGD_DHCP4, iface, "ignoring invalid classless static routes"); + break; + } + + have_routes = TRUE; + if (!route.plen) { + /* gateway passed as classless static route */ + *gwaddr = route.gateway; + } else { + char addr[INET_ADDRSTRLEN]; + + /* normal route */ + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = priority; + nm_ip4_config_add_route (ip4_config, &route); + + _LOG2I (LOGD_DHCP4, iface, " classless static route %s/%d gw %s", + nm_utils_inet4_ntop (route.network, addr), route.plen, + nm_utils_inet4_ntop (route.gateway, NULL)); + } + } + +out: + g_strfreev (octets); + return have_routes; +} + +static gboolean +ip4_process_classless_routes (const char *iface, + GHashTable *options, + guint32 priority, + 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, priority, ip4_config, gwaddr); + } + + return ip4_process_dhclient_rfc3442_routes (iface, str, priority, ip4_config, gwaddr); +} + +static void +process_classful_routes (const char *iface, + GHashTable *options, + guint32 priority, + NMIP4Config *ip4_config) +{ + const char *str; + char **searches, **s; + + str = g_hash_table_lookup (options, "static_routes"); + if (!str) + return; + + searches = g_strsplit (str, " ", 0); + if ((g_strv_length (searches) % 2)) { + _LOG2I (LOGD_DHCP, iface, " static routes provided, but invalid"); + goto out; + } + + 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 = priority; + + nm_ip4_config_add_route (ip4_config, &route); + _LOG2I (LOGD_DHCP, iface, " static route %s", + nm_platform_ip4_route_to_string (&route, NULL, 0)); + } + +out: + g_strfreev (searches); +} + +static void +process_domain_search (const char *iface, + const char *str, + GFunc add_func, + gpointer user_data) +{ + char **searches, **s; + char *unescaped, *p; + int i; + + g_return_if_fail (str != NULL); + g_return_if_fail (add_func != NULL); + + p = unescaped = g_strdup (str); + 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); + goto out; + } + + searches = g_strsplit (unescaped, " ", 0); + for (s = searches; *s; s++) { + if (strlen (*s)) { + _LOG2I (LOGD_DHCP, iface, " domain search '%s'", *s); + add_func (*s, user_data); + } + } + g_strfreev (searches); + +out: + g_free (unescaped); +} + +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 (int ifindex, + const char *iface, + GHashTable *options, + guint32 priority) +{ + NMIP4Config *ip4_config = NULL; + guint32 tmp_addr; + in_addr_t addr; + NMPlatformIP4Address address; + char *str = NULL; + guint32 gwaddr = 0; + guint8 plen = 0; + + g_return_val_if_fail (options != NULL, NULL); + + ip4_config = nm_ip4_config_new (ifindex); + memset (&address, 0, sizeof (address)); + address.timestamp = nm_utils_get_monotonic_timestamp_s (); + + 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 + goto error; + + 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, priority, ip4_config, &gwaddr)) + process_classful_routes (iface, options, priority, ip4_config); + + if (gwaddr) { + _LOG2I (LOGD_DHCP4, iface, " gateway %s", nm_utils_inet4_ntop (gwaddr, NULL)); + nm_ip4_config_set_gateway (ip4_config, gwaddr); + } 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) { + char **routers = g_strsplit (str, " ", 0); + char **s; + + for (s = routers; *s; s++) { + /* FIXME: how to handle multiple routers? */ + if (inet_pton (AF_INET, *s, &gwaddr) > 0) { + nm_ip4_config_set_gateway (ip4_config, gwaddr); + _LOG2I (LOGD_DHCP4, iface, " gateway %s", *s); + break; + } else + _LOG2W (LOGD_DHCP4, iface, "ignoring invalid gateway '%s'", *s); + } + g_strfreev (routers); + } + } + + /* + * RFC 2132, section 9.7 + * DHCP clients use the contents of the 'server identifier' field + * as the destination address for any DHCP messages unicast to + * the DHCP server. + * + * Some ISP's provide leases from central servers that are on + * different subnets that the address offered. If the host + * does not configure the interface as the default route, the + * dhcp server may not be reachable via unicast, and a host + * specific route is needed. + **/ + str = g_hash_table_lookup (options, "dhcp_server_identifier"); + if (str) { + if (inet_pton (AF_INET, str, &tmp_addr) > 0) { + + _LOG2I (LOGD_DHCP4, iface, " server identifier %s", str); + if ( nm_utils_ip4_address_clear_host_address(tmp_addr, address.plen) != nm_utils_ip4_address_clear_host_address(address.address, address.plen) + && !nm_ip4_config_get_direct_route_for_host (ip4_config, tmp_addr)) { + /* DHCP server not on assigned subnet and the no direct route was returned. Add route */ + NMPlatformIP4Route route = { 0 }; + + route.network = tmp_addr; + route.plen = 32; + /* this will be a device route if gwaddr is 0 */ + route.gateway = gwaddr; + route.rt_source = NM_IP_CONFIG_SOURCE_DHCP; + route.metric = priority; + nm_ip4_config_add_route (ip4_config, &route); + _LOG2D (LOGD_IP, iface, "adding route for server identifier: %s", + nm_platform_ip4_route_to_string (&route, NULL, 0)); + } + } + else + _LOG2W (LOGD_DHCP4, iface, "ignoring invalid server identifier '%s'", str); + } + + 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) { + char **dns = g_strsplit (str, " ", 0); + char **s; + + for (s = 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); + } + g_strfreev (dns); + } + + str = g_hash_table_lookup (options, "domain_name"); + if (str) { + char **domains = g_strsplit (str, " ", 0); + char **s; + + for (s = domains; *s; s++) { + _LOG2I (LOGD_DHCP4, iface, " domain name '%s'", *s); + nm_ip4_config_add_domain (ip4_config, *s); + } + g_strfreev (domains); + } + + 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) { + char **nbns = g_strsplit (str, " ", 0); + char **s; + + for (s = 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); + } + g_strfreev (nbns); + } + + str = g_hash_table_lookup (options, "interface_mtu"); + if (str) { + int int_mtu; + + errno = 0; + int_mtu = strtol (str, NULL, 10); + if ((errno == EINVAL) || (errno == ERANGE)) + goto error; + + 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) { + char **nis = g_strsplit (str, " ", 0); + char **s; + + for (s = 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); + } + g_strfreev (nis); + } + + str = g_hash_table_lookup (options, "vendor_encapsulated_options"); + nm_ip4_config_set_metered (ip4_config, str && strstr (str, "ANDROID_METERED")); + + return ip4_config; + +error: + g_object_unref (ip4_config); + return NULL; +} + +/*****************************************************************************/ + +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 gchar **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_s (); + + 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 (int ifindex, + const char *iface, + GHashTable *options, + guint32 priority, + gboolean info_only) +{ + 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_s (); + + ip6_config = nm_ip6_config_new (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); + goto error; + } + + 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 */ + goto error; + } + + 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) { + char **dns = g_strsplit (str, " ", 0); + char **s; + + for (s = 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); + } + g_strfreev (dns); + } + + str = g_hash_table_lookup (options, "dhcp6_domain_search"); + if (str) + process_domain_search (iface, str, ip6_add_domain_search, ip6_config); + + return ip6_config; + +error: + g_object_unref (ip6_config); + return NULL; +} + +char * +nm_dhcp_utils_duid_to_string (const GByteArray *duid) +{ + g_return_val_if_fail (duid != NULL, NULL); + + return _nm_utils_bin2str (duid->data, duid->len, FALSE); +} + +/** + * 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); + 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; +} + diff --git a/src/dhcp/nm-dhcp-utils.h b/src/dhcp/nm-dhcp-utils.h new file mode 100644 index 00000000..b45c5e89 --- /dev/null +++ b/src/dhcp/nm-dhcp-utils.h @@ -0,0 +1,45 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * 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 (int ifindex, + const char *iface, + GHashTable *options, + guint priority); + +NMIP6Config *nm_dhcp_utils_ip6_config_from_options (int ifindex, + const char *iface, + GHashTable *options, + guint priority, + gboolean info_only); + +NMPlatformIP6Address nm_dhcp_utils_ip6_prefix_from_options (GHashTable *options); + +char * nm_dhcp_utils_duid_to_string (const GByteArray *duid); + +GBytes * nm_dhcp_utils_client_id_string_to_bytes (const char *client_id); + +#endif /* __NETWORKMANAGER_DHCP_UTILS_H__ */ + diff --git a/src/dhcp/tests/leases/basic.leases b/src/dhcp/tests/leases/basic.leases new file mode 100644 index 00000000..703d9247 --- /dev/null +++ b/src/dhcp/tests/leases/basic.leases @@ -0,0 +1,31 @@ +lease { + interface "wlan0"; + fixed-address 192.168.1.180; + option subnet-mask 255.255.255.0; + option routers 192.168.1.1; + option dhcp-lease-time 600; + option dhcp-message-type 5; + option domain-name-servers 192.168.1.1; + option dhcp-server-identifier 192.168.1.1; + option broadcast-address 192.168.1.255; + renew 5 2013/11/01 19:56:15; + rebind 5 2013/11/01 20:00:44; + expire 5 2013/11/01 20:01:59; +} +lease { + interface "wlan0"; + fixed-address 10.77.52.141; + option subnet-mask 255.0.0.0; + option dhcp-lease-time 1200; + option routers 10.77.52.254; + option dhcp-message-type 5; + option dhcp-server-identifier 10.77.52.254; + option domain-name-servers 8.8.8.8,8.8.4.4; + option dhcp-renewal-time 600; + option dhcp-rebinding-time 1050; + option domain-name "morriesguest.local"; + renew 5 2013/11/01 20:01:08; + rebind 5 2013/11/01 20:05:00; + expire 5 2013/11/01 20:06:15; +} + diff --git a/src/dhcp/tests/leases/malformed1.leases b/src/dhcp/tests/leases/malformed1.leases new file mode 100644 index 00000000..401d982a --- /dev/null +++ b/src/dhcp/tests/leases/malformed1.leases @@ -0,0 +1,15 @@ +# missing fixed-address option +lease { + interface "wlan0"; + option subnet-mask 255.255.255.0; + option routers 192.168.1.1; + option dhcp-lease-time 600; + option dhcp-message-type 5; + option domain-name-servers 192.168.1.1; + option dhcp-server-identifier 192.168.1.1; + option broadcast-address 192.168.1.255; + renew 5 2013/11/01 19:56:15; + rebind 5 2013/11/01 20:00:44; + expire 5 2013/11/01 20:01:59; +} + diff --git a/src/dhcp/tests/leases/malformed2.leases b/src/dhcp/tests/leases/malformed2.leases new file mode 100644 index 00000000..adf5f6de --- /dev/null +++ b/src/dhcp/tests/leases/malformed2.leases @@ -0,0 +1,15 @@ +# missing routers option +lease { + interface "wlan0"; + fixed-address 192.168.1.180; + option subnet-mask 255.255.255.0; + option dhcp-lease-time 600; + option dhcp-message-type 5; + option domain-name-servers 192.168.1.1; + option dhcp-server-identifier 192.168.1.1; + option broadcast-address 192.168.1.255; + renew 5 2013/11/01 19:56:15; + rebind 5 2013/11/01 20:00:44; + expire 5 2013/11/01 20:01:59; +} + diff --git a/src/dhcp/tests/leases/malformed3.leases b/src/dhcp/tests/leases/malformed3.leases new file mode 100644 index 00000000..a2afc8b6 --- /dev/null +++ b/src/dhcp/tests/leases/malformed3.leases @@ -0,0 +1,15 @@ +# missing expire time +lease { + interface "wlan0"; + fixed-address 192.168.1.180; + option subnet-mask 255.255.255.0; + option routers 192.168.1.1; + option dhcp-lease-time 600; + option dhcp-message-type 5; + option domain-name-servers 192.168.1.1; + option dhcp-server-identifier 192.168.1.1; + option broadcast-address 192.168.1.255; + renew 5 2013/11/01 19:56:15; + rebind 5 2013/11/01 20:00:44; +} + diff --git a/src/dhcp/tests/test-dhclient-commented-duid.leases b/src/dhcp/tests/test-dhclient-commented-duid.leases new file mode 100644 index 00000000..3e46ae7d --- /dev/null +++ b/src/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/dhcp/tests/test-dhclient-duid.leases b/src/dhcp/tests/test-dhclient-duid.leases new file mode 100644 index 00000000..229331d4 --- /dev/null +++ b/src/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/dhcp/tests/test-dhcp-dhclient.c b/src/dhcp/tests/test-dhcp-dhclient.c new file mode 100644 index 00000000..82f0c080 --- /dev/null +++ b/src/dhcp/tests/test-dhcp-dhclient.c @@ -0,0 +1,915 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2010 Red Hat, Inc. + * + */ + +#include "nm-default.h" + +#include <string.h> +#include <unistd.h> +#include <arpa/inet.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 DEBUG 1 + +static void +test_config (const char *orig, + const char *expected, + gboolean ipv6, + const char *hostname, + const char *fqdn, + const char *dhcp_client_id, + GBytes *expected_new_client_id, + const char *iface, + const char *anycast_addr) +{ + 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, + ipv6, + client_id, + anycast_addr, + hostname, + fqdn, + "/path/to/dhclient.conf", + orig, + &new_client_id); + g_assert (new != NULL); + +#if DEBUG + if ( strlen (new) != strlen (expected) + || strcmp (new, expected)) { + g_message ("\n- NEW ---------------------------------\n" + "%s" + "+ EXPECTED ++++++++++++++++++++++++++++++\n" + "%s" + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + new, expected); + } +#endif + 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" + "\n"; + +static void +test_orig_missing (void) +{ + test_config (NULL, orig_missing_expected, FALSE, NULL, NULL, NULL, NULL, "eth0", NULL); +} + +/*****************************************************************************/ + +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" + "\n"; + +static void +test_override_client_id (void) +{ + test_config (override_client_id_orig, override_client_id_expected, + FALSE, NULL, NULL, + "11:22:33:44:55:66", + NULL, + "eth0", + NULL); +} + +/*****************************************************************************/ + +static const char *quote_client_id_expected = \ + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier \"1234\"; # 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" + "\n"; + +static void +test_quote_client_id (void) +{ + test_config (NULL, quote_client_id_expected, + FALSE, NULL, NULL, + "1234", + NULL, + "eth0", + NULL); +} + +/*****************************************************************************/ + +static const char *ascii_client_id_expected = \ + "# Created by NetworkManager\n" + "\n" + "send dhcp-client-identifier \"qb: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" + "\n"; + +static void +test_ascii_client_id (void) +{ + test_config (NULL, ascii_client_id_expected, + FALSE, NULL, NULL, + "qb:cd:ef:12:34:56", + NULL, + "eth0", + 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" + "\n"; + +static void +test_hex_single_client_id (void) +{ + test_config (NULL, hex_single_client_id_expected, + FALSE, NULL, NULL, + "ab:cd:e:12:34:56", + NULL, + "eth0", + NULL); +} + +/*****************************************************************************/ + +static const char *existing_hex_client_id_orig = \ + "send dhcp-client-identifier 00: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 00: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" + "\n"; + +static void +test_existing_hex_client_id (void) +{ + gs_unref_bytes GBytes *new_client_id = NULL; + const guint8 bytes[] = { 0x00, 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, + FALSE, NULL, NULL, + NULL, + new_client_id, + "eth0", + NULL); +} + +/*****************************************************************************/ + +#define EACID "qb:cd:ef:12:34:56" + +static const char *existing_ascii_client_id_orig = \ + "send dhcp-client-identifier \"" 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 \"" 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" + "\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, + FALSE, NULL, NULL, + NULL, + new_client_id, + "eth0", + 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 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\n"; + +static void +test_fqdn (void) +{ + test_config (NULL, fqdn_expected, + FALSE, NULL, + "foo.bar.com", NULL, + NULL, + "eth0", + 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 on;\n" + "send fqdn.server-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" + "\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, + FALSE, NULL, + "example2.com", NULL, + NULL, + "eth0", + 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" + "\n"; + +static void +test_override_hostname (void) +{ + test_config (override_hostname_orig, override_hostname_expected, + FALSE, "blahblah", NULL, + NULL, + NULL, + "eth0", + 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.encoded on;\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, + TRUE, "blahblah.local", NULL, + NULL, + NULL, + "eth0", + NULL); +} + +/*****************************************************************************/ + +static const char *nonfqdn_hostname6_expected = \ + "# Created by NetworkManager\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't be used with dhclient */ + test_config (NULL, nonfqdn_hostname6_expected, + TRUE, "blahblah", + NULL, NULL, + NULL, + "eth0", + 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" + "\n"; + +static void +test_existing_alsoreq (void) +{ + test_config (existing_alsoreq_orig, existing_alsoreq_expected, + FALSE, NULL, + NULL, + NULL, + NULL, + "eth0", + 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" + "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" + "\n"; + +static void +test_existing_req (void) +{ + test_config (existing_req_orig, existing_req_expected, + FALSE, NULL, + NULL, + NULL, + NULL, + "eth0", + 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" + "\n"; + +static void +test_existing_multiline_alsoreq (void) +{ + test_config (existing_multiline_alsoreq_orig, existing_multiline_alsoreq_expected, + FALSE, NULL, NULL, + NULL, + NULL, + "eth0", + NULL); +} + +/*****************************************************************************/ + +static void +test_one_duid (const char *escaped, const guint8 *unescaped, guint len) +{ + GByteArray *t; + char *w; + + t = nm_dhcp_dhclient_unescape_duid (escaped); + g_assert (t); + g_assert_cmpint (t->len, ==, len); + g_assert_cmpint (memcmp (t->data, unescaped, len), ==, 0); + g_byte_array_free (t, TRUE); + + t = g_byte_array_sized_new (len); + g_byte_array_append (t, unescaped, len); + w = nm_dhcp_dhclient_escape_duid (t); + g_assert (w); + g_assert_cmpint (strlen (escaped), ==, strlen (w)); + g_assert_cmpstr (escaped, ==, w); + + g_byte_array_free (t, TRUE); + g_free (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 }; + GByteArray *duid; + GError *error = NULL; + + duid = nm_dhcp_dhclient_read_duid (TESTDIR "/test-dhclient-duid.leases", &error); + g_assert_no_error (error); + g_assert (duid); + g_assert_cmpint (duid->len, ==, sizeof (expected)); + g_assert_cmpint (memcmp (duid->data, expected, duid->len), ==, 0); + + g_byte_array_free (duid, TRUE); +} + +static void +test_read_commented_duid_from_leasefile (void) +{ + GByteArray *duid; + GError *error = NULL; + + duid = nm_dhcp_dhclient_read_duid (TESTDIR "/test-dhclient-commented-duid.leases", &error); + g_assert_no_error (error); + g_assert (duid == NULL); +} + +static void +test_write_duid (void) +{ + const char *duid = "\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254"; + const char *expected_contents = "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"; + GError *error = NULL; + char *contents = NULL; + gboolean success; + const char *path = "test-dhclient-write-duid.leases"; + + success = nm_dhcp_dhclient_save_duid (path, duid, &error); + g_assert_no_error (error); + g_assert (success); + + success = g_file_get_contents (path, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + unlink (path); + g_assert_cmpstr (expected_contents, ==, contents); + + g_free (contents); +} + +static void +test_write_existing_duid (void) +{ + const char *duid = "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302"; + const char *expected_contents = "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"; + GError *error = NULL; + char *contents = NULL; + gboolean success; + const char *path = "test-dhclient-write-existing-duid.leases"; + + success = g_file_set_contents (path, expected_contents, -1, &error); + g_assert_no_error (error); + g_assert (success); + + /* Save other DUID; should be a no-op */ + success = nm_dhcp_dhclient_save_duid (path, duid, &error); + g_assert_no_error (error); + g_assert (success); + + /* reread original contents */ + success = g_file_get_contents (path, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + unlink (path); + g_assert_cmpstr (expected_contents, ==, contents); + + g_free (contents); +} + +static void +test_write_existing_commented_duid (void) +{ + #define DUID "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302" + #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; + 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); + g_assert_no_error (error); + g_assert (success); + + /* Save other DUID; should be a no-op */ + success = nm_dhcp_dhclient_save_duid (path, DUID, &error); + g_assert_no_error (error); + g_assert (success); + + /* reread original contents */ + success = g_file_get_contents (path, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + unlink (path); + g_assert_cmpstr (expected_contents, ==, contents); + + g_free (contents); +} + +/*****************************************************************************/ + +static void +test_read_lease_ip4_config_basic (void) +{ + GError *error = NULL; + char *contents = NULL; + gboolean success; + const char *path = TESTDIR "/leases/basic.leases"; + GSList *leases; + GDateTime *now; + NMIP4Config *config; + const NMPlatformIP4Address *addr; + guint32 expected_addr; + + success = g_file_get_contents (path, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + /* Date from before the least expiration */ + now = g_date_time_new_utc (2013, 11, 1, 19, 55, 32); + leases = nm_dhcp_dhclient_read_lease_ip_configs ("wlan0", -1, contents, FALSE, now); + g_assert_cmpint (g_slist_length (leases), ==, 2); + + /* IP4Config #1 */ + config = g_slist_nth_data (leases, 0); + g_assert (NM_IS_IP4_CONFIG (config)); + + /* Address */ + g_assert_cmpint (nm_ip4_config_get_num_addresses (config), ==, 1); + g_assert (inet_aton ("192.168.1.180", (struct in_addr *) &expected_addr)); + addr = nm_ip4_config_get_address (config, 0); + g_assert_cmpint (addr->address, ==, expected_addr); + g_assert_cmpint (addr->peer_address, ==, expected_addr); + g_assert_cmpint (addr->plen, ==, 24); + + /* Gateway */ + g_assert (inet_aton ("192.168.1.1", (struct in_addr *) &expected_addr)); + g_assert_cmpint (nm_ip4_config_get_gateway (config), ==, expected_addr); + + /* DNS */ + g_assert_cmpint (nm_ip4_config_get_num_nameservers (config), ==, 1); + g_assert (inet_aton ("192.168.1.1", (struct in_addr *) &expected_addr)); + g_assert_cmpint (nm_ip4_config_get_nameserver (config, 0), ==, expected_addr); + + g_assert_cmpint (nm_ip4_config_get_num_domains (config), ==, 0); + + /* IP4Config #2 */ + config = g_slist_nth_data (leases, 1); + g_assert (NM_IS_IP4_CONFIG (config)); + + /* Address */ + g_assert_cmpint (nm_ip4_config_get_num_addresses (config), ==, 1); + g_assert (inet_aton ("10.77.52.141", (struct in_addr *) &expected_addr)); + addr = nm_ip4_config_get_address (config, 0); + g_assert_cmpint (addr->address, ==, expected_addr); + g_assert_cmpint (addr->peer_address, ==, expected_addr); + g_assert_cmpint (addr->plen, ==, 8); + + /* Gateway */ + g_assert (inet_aton ("10.77.52.254", (struct in_addr *) &expected_addr)); + g_assert_cmpint (nm_ip4_config_get_gateway (config), ==, expected_addr); + + /* DNS */ + g_assert_cmpint (nm_ip4_config_get_num_nameservers (config), ==, 2); + g_assert (inet_aton ("8.8.8.8", (struct in_addr *) &expected_addr)); + g_assert_cmpint (nm_ip4_config_get_nameserver (config, 0), ==, expected_addr); + g_assert (inet_aton ("8.8.4.4", (struct in_addr *) &expected_addr)); + g_assert_cmpint (nm_ip4_config_get_nameserver (config, 1), ==, expected_addr); + + /* Domains */ + g_assert_cmpint (nm_ip4_config_get_num_domains (config), ==, 1); + g_assert_cmpstr (nm_ip4_config_get_domain (config, 0), ==, "morriesguest.local"); + + g_slist_free_full (leases, g_object_unref); + g_date_time_unref (now); + g_free (contents); +} + +static void +test_read_lease_ip4_config_expired (void) +{ + GError *error = NULL; + char *contents = NULL; + gboolean success; + const char *path = TESTDIR "/leases/basic.leases"; + GSList *leases; + GDateTime *now; + + success = g_file_get_contents (path, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + /* Date from *after* the lease expiration */ + now = g_date_time_new_utc (2013, 12, 1, 19, 55, 32); + leases = nm_dhcp_dhclient_read_lease_ip_configs ("wlan0", -1, contents, FALSE, now); + g_assert (leases == NULL); + + g_date_time_unref (now); + g_free (contents); +} + +static void +test_read_lease_ip4_config_expect_failure (gconstpointer user_data) +{ + GError *error = NULL; + char *contents = NULL; + gboolean success; + GSList *leases; + GDateTime *now; + + success = g_file_get_contents ((const char *) user_data, &contents, NULL, &error); + g_assert_no_error (error); + g_assert (success); + + /* Date from before the least expiration */ + now = g_date_time_new_utc (2013, 11, 1, 1, 1, 1); + leases = nm_dhcp_dhclient_read_lease_ip_configs ("wlan0", -1, contents, FALSE, now); + g_assert (leases == NULL); + + g_date_time_unref (now); + g_free (contents); +} + +/*****************************************************************************/ + +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/override_client_id", test_override_client_id); + g_test_add_func ("/dhcp/dhclient/quote_client_id", test_quote_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-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/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/leases/ip4-config/basic", test_read_lease_ip4_config_basic); + g_test_add_func ("/dhcp/dhclient/leases/ip4-config/expired", test_read_lease_ip4_config_expired); + g_test_add_data_func ("/dhcp/dhclient/leases/ip4-config/missing-address", + TESTDIR "/leases/malformed1.leases", + test_read_lease_ip4_config_expect_failure); + g_test_add_data_func ("/dhcp/dhclient/leases/ip4-config/missing-gateway", + TESTDIR "/leases/malformed2.leases", + test_read_lease_ip4_config_expect_failure); + g_test_add_data_func ("/dhcp/dhclient/leases/ip4-config/missing-expire", + TESTDIR "/leases/malformed3.leases", + test_read_lease_ip4_config_expect_failure); + + return g_test_run (); +} + diff --git a/src/dhcp/tests/test-dhcp-utils.c b/src/dhcp/tests/test-dhcp-utils.c new file mode 100644 index 00000000..ffd63493 --- /dev/null +++ b/src/dhcp/tests/test-dhcp-utils.c @@ -0,0 +1,748 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2008 - 2014 Red Hat, Inc. + * + */ + +#include "nm-default.h" + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <string.h> + +#include "nm-utils.h" + +#include "dhcp/nm-dhcp-utils.h" +#include "platform/nm-platform.h" + +#include "nm-test-utils-core.h" + +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 (g_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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* IP4 address */ + g_assert_cmpint (nm_ip4_config_get_num_addresses (ip4_config), ==, 1); + address = nm_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 (nm_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), ==, 2); + + /* Route #1 */ + route = nm_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 = nm_ip4_config_get_route (ip4_config, 1); + g_assert (inet_pton (AF_INET, expected_route2_dest, &tmp) > 0); + g_assert (route->network == tmp); + g_assert (inet_pton (AF_INET, expected_route2_gw, &tmp) > 0); + g_assert (route->gateway == tmp); + g_assert_cmpint (route->plen, ==, 32); + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* IP4 address */ + g_assert_cmpint (nm_ip4_config_get_num_addresses (ip4_config), ==, 1); + address = nm_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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + g_assert (nm_ip4_config_get_metered (ip4_config) == TRUE); + g_hash_table_destroy (options); +} + +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 = nm_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 (nm_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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* 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, expected_route2_dest, expected_route2_gw, 8); + + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* 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, expected_route2_dest, expected_route2_gw, 8); + + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* 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, 25); + ip4_test_route (ip4_config, 1, expected_route2_dest, expected_route2_gw, 7); + + /* 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*ignoring invalid classless static routes*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + g_test_assert_expected_messages (); + + /* IP4 routes */ + g_assert_cmpint (nm_ip4_config_get_num_routes (ip4_config), ==, 1); + ip4_test_route (ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + + 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*ignoring invalid classless static routes*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + 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), ==, 2); + 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); + + 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*ignoring invalid classless static routes*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + 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), ==, 2); + 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); + + 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*ignoring invalid classless static routes*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + 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), ==, 2); + 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); + + 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*ignoring invalid classless static routes*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + g_test_assert_expected_messages (); + + /* IP4 routes */ + g_assert_cmpint (nm_ip4_config_get_num_routes (ip4_config), ==, 1); + ip4_test_route (ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + + 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*DHCP provided invalid classless static route*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + g_test_assert_expected_messages (); + + /* IP4 routes */ + g_assert_cmpint (nm_ip4_config_get_num_routes (ip4_config), ==, 1); + ip4_test_route (ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* IP4 routes */ + g_assert_cmpint (nm_ip4_config_get_num_routes (ip4_config), ==, 1); + ip4_test_route (ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + + /* 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* IP4 routes */ + g_assert_cmpint (nm_ip4_config_get_num_routes (ip4_config), ==, 1); + ip4_test_route (ip4_config, 0, expected_route1_dest, expected_route1_gw, 24); + + /* 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + /* 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); + + g_test_expect_message ("NetworkManager", G_LOG_LEVEL_MESSAGE, + "*invalid domain search*"); + ip4_config = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + 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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + g_assert_cmpint (nm_ip4_config_get_num_addresses (ip4_config), ==, 1); + address = nm_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 = nm_dhcp_utils_ip4_config_from_options (1, "eth0", options, 0); + g_assert (ip4_config); + + g_assert_cmpint (nm_ip4_config_get_num_addresses (ip4_config), ==, 1); + address = nm_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)); +} + +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); + + return g_test_run (); +} + |