summary refs log tree commit diff
path: root/src/dhcp/nm-dhcp-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/dhcp/nm-dhcp-client.c')
-rw-r--r--src/dhcp/nm-dhcp-client.c997
1 files changed, 997 insertions, 0 deletions
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);
+}