summary refs log tree commit diff
path: root/src/dhcp-manager/nm-dhcp-client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/dhcp-manager/nm-dhcp-client.c')
-rw-r--r--src/dhcp-manager/nm-dhcp-client.c1524
1 files changed, 401 insertions, 1123 deletions
diff --git a/src/dhcp-manager/nm-dhcp-client.c b/src/dhcp-manager/nm-dhcp-client.c
index 03c11c74..47fa5317 100644
--- a/src/dhcp-manager/nm-dhcp-client.c
+++ b/src/dhcp-manager/nm-dhcp-client.c
@@ -17,7 +17,8 @@
  *
  */
 
-#include <config.h>
+#include "config.h"
+
 #include <glib.h>
 #include <string.h>
 #include <sys/types.h>
@@ -33,35 +34,35 @@
 #include "nm-logging.h"
 #include "nm-dbus-glib-types.h"
 #include "nm-dhcp-client.h"
+#include "nm-dhcp-utils.h"
+#include "nm-platform.h"
 
 typedef struct {
 	char *       iface;
+	int          ifindex;
 	GByteArray * hwaddr;
 	gboolean     ipv6;
 	char *       uuid;
-	guint        priority;
+	guint32      priority;
 	guint32      timeout;
 	GByteArray * duid;
+	GBytes *     client_id;
+	char *       hostname;
 
-	guchar       state;
-	GPid         pid;
-	gboolean     dead;
+	NMDhcpState  state;
+	pid_t        pid;
 	guint        timeout_id;
 	guint        watch_id;
-	guint32      remove_id;
-	GHashTable * options;
 	gboolean     info_only;
 
-} NMDHCPClientPrivate;
+} NMDhcpClientPrivate;
 
-#define NM_DHCP_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_DHCP_CLIENT, NMDHCPClientPrivate))
+#define NM_DHCP_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), NM_TYPE_DHCP_CLIENT, NMDhcpClientPrivate))
 
-G_DEFINE_TYPE_EXTENDED (NMDHCPClient, nm_dhcp_client, G_TYPE_OBJECT, G_TYPE_FLAG_ABSTRACT, {})
+G_DEFINE_TYPE_EXTENDED (NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT, G_TYPE_FLAG_ABSTRACT, {})
 
 enum {
 	SIGNAL_STATE_CHANGED,
-	SIGNAL_TIMEOUT,
-	SIGNAL_REMOVE,
 	LAST_SIGNAL
 };
 
@@ -70,6 +71,7 @@ static guint signals[LAST_SIGNAL] = { 0 };
 enum {
 	PROP_0,
 	PROP_IFACE,
+	PROP_IFINDEX,
 	PROP_HWADDR,
 	PROP_IPV6,
 	PROP_UUID,
@@ -80,8 +82,8 @@ enum {
 
 /********************************************/
 
-GPid
-nm_dhcp_client_get_pid (NMDHCPClient *self)
+pid_t
+nm_dhcp_client_get_pid (NMDhcpClient *self)
 {
 	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), -1);
 
@@ -89,15 +91,23 @@ nm_dhcp_client_get_pid (NMDHCPClient *self)
 }
 
 const char *
-nm_dhcp_client_get_iface (NMDHCPClient *self)
+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)
+nm_dhcp_client_get_ipv6 (NMDhcpClient *self)
 {
 	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), FALSE);
 
@@ -105,19 +115,120 @@ nm_dhcp_client_get_ipv6 (NMDHCPClient *self)
 }
 
 const char *
-nm_dhcp_client_get_uuid (NMDHCPClient *self)
+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;
+}
+
+/********************************************/
+
+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 (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;
+
+	nm_log_dbg (LOGD_DHCP, "(%s): unmapped DHCP state '%s'", iface, reason);
+	return NM_DHCP_STATE_UNKNOWN;
+}
+
 /********************************************/
 
 static void
-timeout_cleanup (NMDHCPClient *self)
+timeout_cleanup (NMDhcpClient *self)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
 
 	if (priv->timeout_id) {
 		g_source_remove (priv->timeout_id);
@@ -126,9 +237,9 @@ timeout_cleanup (NMDHCPClient *self)
 }
 
 static void
-watch_cleanup (NMDHCPClient *self)
+watch_cleanup (NMDhcpClient *self)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
 
 	if (priv->watch_id) {
 		g_source_remove (priv->watch_id);
@@ -137,171 +248,158 @@ watch_cleanup (NMDHCPClient *self)
 }
 
 void
-nm_dhcp_client_stop_pid (GPid pid, const char *iface)
+nm_dhcp_client_stop_pid (pid_t pid, const char *iface)
 {
-	int i = 5;  /* roughly 0.5 seconds */
-
-	g_return_if_fail (pid > 0);
-
-	/* Tell it to quit; maybe it wants to send out a RELEASE message */
-	kill (pid, SIGTERM);
-
-	while (i-- > 0) {
-		gint child_status;
-		int ret;
+	char *name = iface ? g_strdup_printf ("dhcp-client-%s", iface) : NULL;
 
-		ret = waitpid (pid, &child_status, WNOHANG);
-		if (ret > 0)
-			break;
+	g_return_if_fail (pid > 1);
 
-		if (ret == -1) {
-			/* Child already exited */
-			if (errno == ECHILD) {
-				/* Was it really our child and it exited? */
-				if (kill (pid, 0) < 0 && errno == ESRCH)
-					break;
-			} else {
-				/* Took too long; shoot it in the head */
-				i = 0;
-				break;
-			}
-		}
-		g_usleep (G_USEC_PER_SEC / 10);
-	}
-
-	if (i <= 0) {
-		if (iface) {
-			nm_log_warn (LOGD_DHCP, "(%s): DHCP client pid %d didn't exit, will kill it.",
-			             iface, pid);
-		}
-		kill (pid, SIGKILL);
-
-		nm_log_dbg (LOGD_DHCP, "waiting for DHCP client pid %d to exit", pid);
-		waitpid (pid, NULL, 0);
-		nm_log_dbg (LOGD_DHCP, "DHCP client pid %d cleaned up", pid);
-	}
+	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)
+stop (NMDhcpClient *self, gboolean release, const GByteArray *duid)
 {
-	NMDHCPClientPrivate *priv;
+	NMDhcpClientPrivate *priv;
 
 	g_return_if_fail (NM_IS_DHCP_CLIENT (self));
 
 	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	g_return_if_fail (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);
 
+	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;
 }
 
-static gboolean
-daemon_timeout (gpointer user_data)
+void
+nm_dhcp_client_set_state (NMDhcpClient *self,
+                          NMDhcpState new_state,
+                          GObject *ip_config,
+                          GHashTable *options)
 {
-	NMDHCPClient *self = NM_DHCP_CLIENT (user_data);
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
 
-	if (priv->ipv6) {
-		nm_log_warn (LOGD_DHCP6, "(%s): DHCPv6 request timed out.", priv->iface);
+	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 {
-		nm_log_warn (LOGD_DHCP4, "(%s): DHCPv4 request timed out.", priv->iface);
+		g_assert (ip_config == NULL);
+		g_assert (options == NULL);
 	}
-	g_signal_emit (G_OBJECT (self), signals[SIGNAL_TIMEOUT], 0);
-	return FALSE;
-}
 
-static gboolean
-signal_remove (gpointer user_data)
-{
-	NMDHCPClient *self = NM_DHCP_CLIENT (user_data);
+	/* 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;
 
-	NM_DHCP_CLIENT_GET_PRIVATE (self)->remove_id = 0;
-	g_signal_emit (G_OBJECT (self), signals[SIGNAL_REMOVE], 0);
-	return FALSE;
+	nm_log_info (priv->ipv6 ? LOGD_DHCP6 : LOGD_DHCP4,
+	             "(%s): DHCPv%c state changed %s -> %s",
+	             priv->iface,
+	             priv->ipv6 ? '6' : '4',
+	             state_to_string (priv->state),
+	             state_to_string (new_state));
+
+	priv->state = new_state;
+	g_signal_emit (G_OBJECT (self),
+	               signals[SIGNAL_STATE_CHANGED], 0,
+	               new_state,
+	               ip_config,
+	               options);
 }
 
-static void
-dhcp_client_set_state (NMDHCPClient *self,
-                       NMDHCPState state,
-                       gboolean emit_state,
-                       gboolean remove_now)
+static gboolean
+daemon_timeout (gpointer user_data)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpClient *self = NM_DHCP_CLIENT (user_data);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
 
-	priv->state = state;
-
-	if (emit_state)
-		g_signal_emit (G_OBJECT (self), signals[SIGNAL_STATE_CHANGED], 0, priv->state);
-
-	if (state == DHC_END || state == DHC_ABEND) {
-		/* Start the remove signal timer */
-		if (remove_now) {
-			g_signal_emit (G_OBJECT (self), signals[SIGNAL_REMOVE], 0);
-		} else {
-			if (!priv->remove_id)
-				priv->remove_id = g_timeout_add_seconds (5, signal_remove, self);
-		}
-	}
+	priv->timeout_id = 0;
+	nm_log_warn (priv->ipv6 ? LOGD_DHCP6 : LOGD_DHCP4,
+	             "(%s): DHCPv%c request timed out.",
+	             priv->iface,
+	             priv->ipv6 ? '6' : '4');
+	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;
-
-	if (priv->ipv6) {
-		nm_log_info (LOGD_DHCP6, "(%s): DHCPv6 client pid %d exited with status %d",
-		             priv->iface, pid,
-		             WIFEXITED (status) ? WEXITSTATUS (status) : -1);
-	} else {
-		nm_log_info (LOGD_DHCP4, "(%s): DHCPv4 client pid %d exited with status %d",
-		             priv->iface, pid,
-		             WIFEXITED (status) ? WEXITSTATUS (status) : -1);
-	}
-
-	if (!WIFEXITED (status)) {
-		new_state = DHC_ABEND;
+	NMDhcpClient *self = NM_DHCP_CLIENT (user_data);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpState new_state;
+	guint64 log_domain;
+	guint ip_ver;
+
+	log_domain = priv->ipv6 ? LOGD_DHCP6 : LOGD_DHCP4;
+	ip_ver = priv->ipv6 ? 6 : 4;
+
+	if (WIFEXITED (status))
+		nm_log_info (log_domain, "(%s): DHCPv%d client pid %d exited with status %d",
+		             priv->iface, ip_ver, pid, WEXITSTATUS (status));
+	else if (WIFSIGNALED (status))
+		nm_log_info (log_domain, "(%s): DHCPv%d client pid %d killed by signal %d",
+		             priv->iface, ip_ver, pid, WTERMSIG (status));
+	else if (WIFSTOPPED(status))
+		nm_log_info (log_domain, "(%s): DHCPv%d client pid %d stopped by signal %d",
+		             priv->iface, ip_ver, pid, WSTOPSIG (status));
+	else if (WIFCONTINUED (status))
+		nm_log_info (log_domain, "(%s): DHCPv%d client pid %d resumed (by SIGCONT)",
+		             priv->iface, ip_ver, pid);
+	else
 		nm_log_warn (LOGD_DHCP, "DHCP client died abnormally");
-	} else
-		new_state = DHC_END;
 
-	watch_cleanup (self);
-	timeout_cleanup (self);
-	priv->dead = TRUE;
+	if (!WIFEXITED (status))
+		new_state = NM_DHCP_STATE_FAIL;
+	else
+		new_state = NM_DHCP_STATE_DONE;
+
+	priv->pid = -1;
 
-	dhcp_client_set_state (self, new_state, TRUE, FALSE);
+	nm_dhcp_client_set_state (self, new_state, NULL, NULL);
 }
 
-static void
-start_monitor (NMDHCPClient *self)
+void
+nm_dhcp_client_watch_child (NMDhcpClient *self, pid_t pid)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
 
-	g_return_if_fail (priv->pid > 0);
+	g_return_if_fail (priv->pid == -1);
+	priv->pid = pid;
 
 	/* 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,
 	                                          daemon_timeout,
 	                                          self);
-	priv->watch_id = g_child_watch_add (priv->pid,
-	                                    (GChildWatchFunc) daemon_watch_cb,
-	                                    self);
+	g_assert (priv->watch_id == 0);
+	priv->watch_id = g_child_watch_add (pid, daemon_watch_cb, self);
 }
 
 gboolean
-nm_dhcp_client_start_ip4 (NMDHCPClient *self,
+nm_dhcp_client_start_ip4 (NMDhcpClient *self,
                           const char *dhcp_client_id,
-                          GByteArray *dhcp_anycast_addr,
-                          const char *hostname)
+                          const char *dhcp_anycast_addr,
+                          const char *hostname,
+                          const char *last_ip4_address)
 {
-	NMDHCPClientPrivate *priv;
+	NMDhcpClientPrivate *priv;
 
 	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), FALSE);
 
@@ -313,11 +411,12 @@ nm_dhcp_client_start_ip4 (NMDHCPClient *self,
 	nm_log_info (LOGD_DHCP, "Activation (%s) Beginning DHCPv4 transaction (timeout in %d seconds)",
 	             priv->iface, priv->timeout);
 
-	priv->pid = NM_DHCP_CLIENT_GET_CLASS (self)->ip4_start (self, dhcp_client_id, dhcp_anycast_addr, hostname);
-	if (priv->pid)
-		start_monitor (self);
+	nm_dhcp_client_set_client_id (self, dhcp_client_id ? nm_dhcp_utils_client_id_string_to_bytes (dhcp_client_id) : NULL);
 
-	return priv->pid ? TRUE : FALSE;
+	g_clear_pointer (&priv->hostname, g_free);
+	priv->hostname = g_strdup (hostname);
+
+	return NM_DHCP_CLIENT_GET_CLASS (self)->ip4_start (self, dhcp_anycast_addr, last_ip4_address);
 }
 
 /* uuid_parse does not work for machine-id, so we use our own converter */
@@ -406,38 +505,21 @@ generate_duid_from_machine_id (void)
 	return duid;
 }
 
-static char *
-escape_duid (const GByteArray *duid)
-{
-	guint32 i = 0;
-	GString *s;
-
-	g_return_val_if_fail (duid != NULL, NULL);
-
-	s = g_string_sized_new (40);
-	while (i < duid->len) {
-		if (s->len)
-			g_string_append_c (s, ':');
-		g_string_append_printf (s, "%02x", duid->data[i++]);
-	}
-	return g_string_free (s, FALSE);
-}
-
 static GByteArray *
-get_duid (NMDHCPClient *self)
+get_duid (NMDhcpClient *self)
 {
 	static GByteArray *duid = NULL;
 	GByteArray *copy = NULL;
-	char *escaped;
+	char *str;
 
 	if (G_UNLIKELY (duid == NULL)) {
 		duid = generate_duid_from_machine_id ();
 		g_assert (duid);
 
 		if (nm_logging_enabled (LOGL_DEBUG, LOGD_DHCP6)) {
-			escaped = escape_duid (duid);
-			nm_log_dbg (LOGD_DHCP6, "Generated DUID %s", escaped);
-			g_free (escaped);
+			str = nm_dhcp_utils_duid_to_string (duid);
+			nm_log_dbg (LOGD_DHCP6, "Generated DUID %s", str);
+			g_free (str);
 		}
 	}
 
@@ -450,13 +532,14 @@ get_duid (NMDHCPClient *self)
 }
 
 gboolean
-nm_dhcp_client_start_ip6 (NMDHCPClient *self,
-                          GByteArray *dhcp_anycast_addr,
+nm_dhcp_client_start_ip6 (NMDhcpClient *self,
+                          const char *dhcp_anycast_addr,
                           const char *hostname,
-                          gboolean info_only)
+                          gboolean info_only,
+                          NMSettingIP6ConfigPrivacy privacy)
 {
-	NMDHCPClientPrivate *priv;
-	char *escaped;
+	NMDhcpClientPrivate *priv;
+	char *str;
 
 	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), FALSE);
 
@@ -472,25 +555,24 @@ nm_dhcp_client_start_ip6 (NMDHCPClient *self,
 		priv->duid = NM_DHCP_CLIENT_GET_CLASS (self)->get_duid (self);
 
 	if (nm_logging_enabled (LOGL_DEBUG, LOGD_DHCP)) {
-		escaped = escape_duid (priv->duid);
-		nm_log_dbg (LOGD_DHCP, "(%s): DHCPv6 DUID is '%s'", priv->iface, escaped);
-		g_free (escaped);
+		str = nm_dhcp_utils_duid_to_string (priv->duid);
+		nm_log_dbg (LOGD_DHCP, "(%s): DHCPv6 DUID is '%s'", priv->iface, str);
+		g_free (str);
 	}
 
+	g_clear_pointer (&priv->hostname, g_free);
+	priv->hostname = g_strdup (hostname);
+
 	priv->info_only = info_only;
 
 	nm_log_info (LOGD_DHCP, "Activation (%s) Beginning DHCPv6 transaction (timeout in %d seconds)",
 	             priv->iface, priv->timeout);
 
-	priv->pid = NM_DHCP_CLIENT_GET_CLASS (self)->ip6_start (self,
-	                                                        dhcp_anycast_addr,
-	                                                        hostname,
-	                                                        info_only,
-	                                                        priv->duid);
-	if (priv->pid > 0)
-		start_monitor (self);
-
-	return priv->pid ? TRUE : FALSE;
+	return NM_DHCP_CLIENT_GET_CLASS (self)->ip6_start (self,
+	                                                   dhcp_anycast_addr,
+	                                                   info_only,
+	                                                   privacy,
+	                                                   priv->duid);
 }
 
 void
@@ -506,11 +588,14 @@ nm_dhcp_client_stop_existing (const char *pid_file, const char *binary_name)
 	errno = 0;
 	tmp = strtol (pid_contents, NULL, 10);
 	if ((errno == 0) && (tmp > 1)) {
+		guint64 start_time;
 		const char *exe;
 
 		/* Ensure the process is a DHCP client */
+		start_time = nm_utils_get_start_time_for_pid (tmp);
 		proc_path = g_strdup_printf ("/proc/%ld/cmdline", tmp);
-		if (g_file_get_contents (proc_path, &proc_contents, NULL, NULL)) {
+		if (   start_time
+		    && g_file_get_contents (proc_path, &proc_contents, NULL, NULL)) {
 			exe = strrchr (proc_contents, '/');
 			if (exe)
 				exe++;
@@ -518,7 +603,8 @@ nm_dhcp_client_stop_existing (const char *pid_file, const char *binary_name)
 				exe = proc_contents;
 
 			if (!strcmp (exe, binary_name))
-				nm_dhcp_client_stop_pid ((GPid) tmp, NULL);
+				nm_utils_kill_process_sync (tmp, start_time, SIGTERM, LOGD_DHCP,
+				                            "dhcp-client", 1000 / 2, 1000 / 20);
 		}
 	}
 
@@ -531,103 +617,30 @@ nm_dhcp_client_stop_existing (const char *pid_file, const char *binary_name)
 }
 
 void
-nm_dhcp_client_stop (NMDHCPClient *self, gboolean release)
+nm_dhcp_client_stop (NMDhcpClient *self, gboolean release)
 {
-	NMDHCPClientPrivate *priv;
+	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 */
-	if (!priv->dead) {
-		NM_DHCP_CLIENT_GET_CLASS (self)->stop (self, release, priv->duid);
-		priv->dead = TRUE;
-
+	old_pid = priv->pid;
+	NM_DHCP_CLIENT_GET_CLASS (self)->stop (self, release, priv->duid);
+	if (old_pid > 0) {
 		nm_log_info (LOGD_DHCP, "(%s): canceled DHCP transaction, DHCP client pid %d",
-		             priv->iface, priv->pid);
-	}
-
-	/* And clean stuff up */
-
-	priv->pid = -1;
-	dhcp_client_set_state (self, DHC_END, FALSE, TRUE);
-
-	g_hash_table_remove_all (priv->options);
+		             priv->iface, old_pid);
+	} else
+		nm_log_info (LOGD_DHCP, "(%s): canceled DHCP transaction", priv->iface);
+	g_assert (priv->pid == -1);
 
-	timeout_cleanup (self);
-	watch_cleanup (self);
+	nm_dhcp_client_set_state (self, NM_DHCP_STATE_DONE, NULL, NULL);
 }
 
 /********************************************/
 
-static gboolean
-state_is_bound (guint32 state)
-{
-	if (   (state == DHC_BOUND4)
-	    || (state == DHC_BOUND6)
-	    || (state == DHC_RENEW4)
-	    || (state == DHC_RENEW6)
-	    || (state == DHC_REBOOT)
-	    || (state == DHC_REBIND4)
-	    || (state == DHC_REBIND6)
-	    || (state == DHC_IPV4LL))
-		return TRUE;
-
-	return FALSE;
-}
-
-static const char *state_table[] = {
-	[DHC_NBI]             = "nbi",
-	[DHC_PREINIT]         = "preinit",
-	[DHC_PREINIT6]        = "preinit6",
-	[DHC_BOUND4]          = "bound",
-	[DHC_BOUND6]          = "bound6",
-	[DHC_IPV4LL]          = "ipv4ll",
-	[DHC_RENEW4]          = "renew",
-	[DHC_RENEW6]          = "renew6",
-	[DHC_REBOOT]          = "reboot",
-	[DHC_REBIND4]         = "rebind",
-	[DHC_REBIND6]         = "rebind6",
-	[DHC_DEPREF6]         = "depref6",
-	[DHC_STOP]            = "stop",
-	[DHC_STOP6]           = "stop6",
-	[DHC_MEDIUM]          = "medium",
-	[DHC_TIMEOUT]         = "timeout",
-	[DHC_FAIL]            = "fail",
-	[DHC_EXPIRE]          = "expire",
-	[DHC_EXPIRE6]         = "expire6",
-	[DHC_RELEASE]         = "release",
-	[DHC_RELEASE6]        = "release6",
-	[DHC_START]           = "start",
-	[DHC_ABEND]           = "abend",
-	[DHC_END]             = "end",
-};
-
-static const char *
-state_to_string (NMDHCPState state)
-{
-	if (state >= 0 && state < G_N_ELEMENTS (state_table))
-		return state_table[state];
-	return NULL;
-}
-
-static NMDHCPState
-string_to_state (const char *name)
-{
-	int i;
-
-	if (name) {
-		for (i = 0; i < G_N_ELEMENTS (state_table); i++) {
-			const char *n = state_table[i];
-
-			if (n && !strcasecmp (name, n))
-				return i;
-		}
-	}
-	return 255;
-}
-
 static char *
 garray_to_string (GArray *array, const char *key)
 {
@@ -661,848 +674,133 @@ garray_to_string (GArray *array, const char *key)
 	return converted;
 }
 
+#define OLD_TAG "old_"
+#define NEW_TAG "new_"
+
 static void
-copy_option (gpointer key,
-             gpointer value,
+copy_option (const char * key,
+             GValue *value,
              gpointer user_data)
 {
 	GHashTable *hash = user_data;
-	const char *str_key = (const char *) key;
 	char *str_value = NULL;
+	const char **p;
+	static const char *ignored_keys[] = {
+		"interface",
+		"pid",
+		"reason",
+		"dhcp_message_type",
+		NULL
+	};
 
-	if (G_VALUE_TYPE (value) != DBUS_TYPE_G_UCHAR_ARRAY) {
-		nm_log_warn (LOGD_DHCP, "unexpected key %s value type was not "
-		             "DBUS_TYPE_G_UCHAR_ARRAY",
-		             str_key);
+	if (!G_VALUE_HOLDS (value, DBUS_TYPE_G_UCHAR_ARRAY)) {
+		nm_log_warn (LOGD_DHCP, "key %s value type was not DBUS_TYPE_G_UCHAR_ARRAY", key);
 		return;
 	}
 
-	str_value = garray_to_string ((GArray *) g_value_get_boxed (value), str_key);
-	if (str_value)
-		g_hash_table_insert (hash, g_strdup (str_key), str_value);
-}
-
-void
-nm_dhcp_client_new_options (NMDHCPClient *self,
-                            GHashTable *options,
-                            const char *reason)
-{
-	NMDHCPClientPrivate *priv;
-	guint32 old_state;
-	guint32 new_state;
-
-	g_return_if_fail (NM_IS_DHCP_CLIENT (self));
-	g_return_if_fail (options != NULL);
-	g_return_if_fail (reason != NULL);
+	if (g_str_has_prefix (key, OLD_TAG))
+		return;
 
-	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	old_state = priv->state;
-	new_state = string_to_state (reason);
-
-	/* Clear old and save new DHCP options */
-	g_hash_table_remove_all (priv->options);
-	g_hash_table_foreach (options, copy_option, priv->options);
-
-	if (old_state == new_state) {
-		/* dhclient will stay in the same state (or, really, provide the same
-		 * reason) for operations like RENEW and REBIND.  We need to ensure
-		 * that triggers various DHCP lease change code, so we need to pass
-		 * along same-state transitions for these states.
-		 */
-		if (   new_state != DHC_BOUND4
-		    && new_state != DHC_RENEW4
-		    && new_state != DHC_REBIND4
-		    && new_state != DHC_BOUND6
-		    && new_state != DHC_RENEW6
-		    && new_state != DHC_REBIND6)
+	/* Filter out stuff that's not actually new DHCP options */
+	for (p = ignored_keys; *p; p++) {
+		if (!strcmp (*p, key))
 			return;
 	}
 
-	/* Handle changed device state */
-	if (state_is_bound (new_state)) {
-		/* Cancel the timeout if the DHCP client is now bound */
-		timeout_cleanup (self);
-	}
-
-	if (priv->ipv6) {
-		nm_log_info (LOGD_DHCP6, "(%s): DHCPv6 state changed %s -> %s",
-		            priv->iface,
-		            state_to_string (old_state),
-		            state_to_string (new_state));
-	} else {
-		nm_log_info (LOGD_DHCP4, "(%s): DHCPv4 state changed %s -> %s",
-		            priv->iface,
-		            state_to_string (old_state),
-		            state_to_string (new_state));
-	}
+	if (g_str_has_prefix (key, NEW_TAG))
+		key += STRLEN (NEW_TAG);
+	if (!key[0])
+		return;
 
-	dhcp_client_set_state (self, new_state, TRUE, FALSE);
+	str_value = garray_to_string ((GArray *) g_value_get_boxed (value), key);
+	if (str_value)
+		g_hash_table_insert (hash, g_strdup (key), str_value);
 }
 
-#define NEW_TAG "new_"
-#define OLD_TAG "old_"
-
 gboolean
-nm_dhcp_client_foreach_option (NMDHCPClient *self,
-                               GHFunc func,
-                               gpointer user_data)
-{
-	NMDHCPClientPrivate *priv;
-	GHashTableIter iter;
-	gpointer iterkey, itervalue;
+nm_dhcp_client_handle_event (gpointer unused,
+                             const char *iface,
+                             gint pid,
+                             GHashTable *options,
+                             const char *reason,
+                             NMDhcpClient *self)
+{
+	NMDhcpClientPrivate *priv;
+	guint32 old_state;
+	guint32 new_state;
+	GHashTable *str_options = NULL;
+	GObject *ip_config = NULL;
 
 	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), FALSE);
-	g_return_val_if_fail (func != NULL, FALSE);
-
-	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-
-	if (!state_is_bound (priv->state)) {
-		if (priv->ipv6) {
-			nm_log_warn (LOGD_DHCP6, "(%s): DHCPv6 client didn't bind to a lease.", priv->iface);
-		} else {
-			nm_log_warn (LOGD_DHCP4, "(%s): DHCPv4 client didn't bind to a lease.", priv->iface);
-		}
-	}
-
-	g_hash_table_iter_init (&iter, priv->options);
-	while (g_hash_table_iter_next (&iter, &iterkey, &itervalue)) {
-		const char *key = iterkey, *value = itervalue;
-		const char **p;
-		static const char *filter_options[] = {
-			"interface", "pid", "reason", "dhcp_message_type", NULL
-		};
-		gboolean ignore = FALSE;
-
-		/* Filter out stuff that's not actually new DHCP options */
-		for (p = filter_options; *p; p++) {
-			if (!strcmp (*p, key) || !strncmp (key, OLD_TAG, strlen (OLD_TAG))) {
-				ignore = TRUE;
-				break;
-			}
-		}
-
-		if (!ignore) {
-			const char *tmp_key = key;
-
-			/* Remove the "new_" prefix that dhclient passes back */
-			if (!strncmp (key, NEW_TAG, strlen (NEW_TAG)))
-				tmp_key = key + strlen (NEW_TAG);
-
-			func ((gpointer) tmp_key, (gpointer) value, user_data);
-		}
-	}
-	return TRUE;
-}
-
-/********************************************/
-
-static gboolean
-ip4_process_dhcpcd_rfc3442_routes (NMDHCPClient *self,
-                                   const char *str,
-                                   NMIP4Config *ip4_config,
-                                   guint32 *gwaddr)
-{
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	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) {
-		nm_log_warn (LOGD_DHCP4, "  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 == EINVAL) || (errno == ERANGE)) {
-				nm_log_warn (LOGD_DHCP4, "DHCP provided invalid classless static route cidr: '%s'", slash + 1);
-				continue;
-			}
-		}
-		if (inet_pton (AF_INET, *r, &rt_addr) <= 0) {
-			nm_log_warn (LOGD_DHCP4, "DHCP provided invalid classless static route address: '%s'", *r);
-			continue;
-		}
-		if (inet_pton (AF_INET, *(r + 1), &rt_route) <= 0) {
-			nm_log_warn (LOGD_DHCP4, "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 {
-			nm_log_info (LOGD_DHCP4, "  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.source = NM_PLATFORM_SOURCE_DHCP;
-			route.metric = priv->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;
-		}
-		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 (NMDHCPClient *self,
-                                     const char *str,
-                                     NMIP4Config *ip4_config,
-                                     guint32 *gwaddr)
-{
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	char **octets, **o;
-	gboolean have_routes = FALSE;
-	NMPlatformIP4Route route;
-	gboolean success;
-
-	o = octets = g_strsplit_set (str, " .", 0);
-	if (g_strv_length (octets) < 5) {
-		nm_log_warn (LOGD_DHCP4, "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) {
-			nm_log_warn (LOGD_DHCP4, "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.source = NM_PLATFORM_SOURCE_DHCP;
-			route.metric = priv->priority;
-			nm_ip4_config_add_route (ip4_config, &route);
-
-			nm_log_info (LOGD_DHCP4, "  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 (NMDHCPClient *self,
-                              GHashTable *options,
-                              NMIP4Config *ip4_config,
-                              guint32 *gwaddr)
-{
-	const char *str, *p;
-
+	g_return_val_if_fail (iface != NULL, FALSE);
+	g_return_val_if_fail (pid > 0, FALSE);
 	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, "new_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, "new_rfc3442_classless_static_routes");
-
-	/* Microsoft version; same as rfc3442 but with a different option # (249) */
-	if (!str)
-		str = g_hash_table_lookup (options, "new_ms_classless_static_routes");
-
-	if (!str || !strlen (str))
-		return FALSE;
-
-	p = str;
-	while (*p) {
-		if (!g_ascii_isdigit (*p) && (*p != ' ') && (*p != '.') && (*p != '/')) {
-			nm_log_warn (LOGD_DHCP4, "ignoring invalid classless static routes '%s'", str);
-			return FALSE;
-		}
-		p++;
-	};
-
-	if (strchr (str, '/')) {
-		/* dhcpcd format */
-		return ip4_process_dhcpcd_rfc3442_routes (self, str, ip4_config, gwaddr);
-	}
-
-	return ip4_process_dhclient_rfc3442_routes (self, str, ip4_config, gwaddr);
-}
-
-static void
-process_classful_routes (NMDHCPClient *self, GHashTable *options, NMIP4Config *ip4_config)
-{
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	const char *str;
-	char **searches, **s;
-
-	str = g_hash_table_lookup (options, "new_static_routes");
-	if (!str)
-		return;
-
-	searches = g_strsplit (str, " ", 0);
-	if ((g_strv_length (searches) % 2)) {
-		nm_log_info (LOGD_DHCP, "  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) {
-			nm_log_warn (LOGD_DHCP, "DHCP provided invalid static route address: '%s'", *s);
-			continue;
-		}
-		if (inet_pton (AF_INET, *(s + 1), &rt_route) <= 0) {
-			nm_log_warn (LOGD_DHCP, "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.source = NM_PLATFORM_SOURCE_DHCP;
-		route.metric = priv->priority;
-
-		nm_ip4_config_add_route (ip4_config, &route);
-		nm_log_info (LOGD_DHCP, "  static route %s",
-		                        nm_platform_ip4_route_to_string (&route));
-	}
-
-out:
-	g_strfreev (searches);
-}
-
-static void
-process_domain_search (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, '\\')) {
-		nm_log_warn (LOGD_DHCP, "  invalid domain search: '%s'", unescaped);
-		goto out;
-	}
-
-	searches = g_strsplit (unescaped, " ", 0);
-	for (s = searches; *s; s++) {
-		if (strlen (*s)) {
-			nm_log_info (LOGD_DHCP, "  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);
-}
-
-/* Given a table of DHCP options from the client, convert into an IP4Config */
-static NMIP4Config *
-ip4_options_to_config (NMDHCPClient *self)
-{
-	NMDHCPClientPrivate *priv;
-	NMIP4Config *ip4_config = NULL;
-	guint32 tmp_addr;
-	NMPlatformIP4Address address;
-	char *str = NULL;
-	guint32 gwaddr = 0, plen = 0;
-
-	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL);
+	g_return_val_if_fail (reason != NULL, FALSE);
 
 	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	g_return_val_if_fail (priv->options != NULL, NULL);
-
-	ip4_config = nm_ip4_config_new ();
-	memset (&address, 0, sizeof (address));
-	address.timestamp = nm_utils_get_monotonic_timestamp_s ();
 
-	str = g_hash_table_lookup (priv->options, "new_ip_address");
-	if (str && (inet_pton (AF_INET, str, &tmp_addr) > 0)) {
-		address.address = tmp_addr;
-		nm_log_info (LOGD_DHCP4, "  address %s", str);
-	} else
-		goto error;
-
-	str = g_hash_table_lookup (priv->options, "new_subnet_mask");
-	if (str && (inet_pton (AF_INET, str, &tmp_addr) > 0)) {
-		plen = nm_utils_ip4_netmask_to_prefix (tmp_addr);
-		nm_log_info (LOGD_DHCP4, "  plen %d (%s)", plen, str);
-	} else {
-		/* Get default netmask for the IP according to appropriate class. */
-		plen = nm_utils_ip4_get_default_prefix (address.address);
-		nm_log_info (LOGD_DHCP4, "  plen %d (default)", plen);
-	}
-	address.plen = plen;
-
-	/* Routes: if the server returns classless static routes, we MUST ignore
-	 * the 'static_routes' option.
-	 */
-	if (!ip4_process_classless_routes (self, priv->options, ip4_config, &gwaddr))
-		process_classful_routes (self, priv->options, ip4_config);
-
-	if (gwaddr) {
-		nm_log_info (LOGD_DHCP4, "  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 (priv->options, "new_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);
-					nm_log_info (LOGD_DHCP4, "  gateway %s", *s);
-					break;
-				} else
-					nm_log_warn (LOGD_DHCP4, "ignoring invalid gateway '%s'", *s);
-			}
-			g_strfreev (routers);
-		}
-	}
+	if (g_strcmp0 (priv->iface, iface) != 0)
+		return FALSE;
+	if (priv->pid != pid)
+		return FALSE;
 
-	/*
-	 * 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 (priv->options, "new_dhcp_server_identifier");
-	if (str) {
-		if (inet_pton (AF_INET, str, &tmp_addr) > 0) {
-			NMPlatformIP4Route route;
-			guint32 mask = nm_utils_ip4_prefix_to_netmask (address.plen);
-
-			nm_log_info (LOGD_DHCP4, "  server identifier %s", str);
-			if ((tmp_addr & mask) != (address.address & mask)) {
-				/* DHCP server not on assigned subnet, route needed */
-				memset (&route, 0, sizeof (route));
-				route.network = tmp_addr;
-				route.plen = 32;
-				/* this will be a device route if gwaddr is 0 */
-				route.gateway = gwaddr;
-				route.source = NM_PLATFORM_SOURCE_DHCP;
-				route.metric = priv->priority;
-				nm_ip4_config_add_route (ip4_config, &route);
-				nm_log_dbg (LOGD_IP, "adding route for server identifier: %s",
-				                      nm_platform_ip4_route_to_string (&route));
+	old_state = priv->state;
+	new_state = reason_to_state (priv->iface, reason);
+	nm_log_dbg (LOGD_DHCP, "(%s): DHCP reason '%s' -> state '%s'",
+	            iface, reason, state_to_string (new_state));
+
+	if (new_state == NM_DHCP_STATE_BOUND) {
+		/* Copy options */
+		str_options = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+		g_hash_table_foreach (options, (GHFunc) copy_option, str_options);
+
+		/* Create the IP config */
+		g_warn_if_fail (g_hash_table_size (str_options));
+		if (g_hash_table_size (str_options)) {
+			if (priv->ipv6) {
+				ip_config = (GObject *) nm_dhcp_utils_ip6_config_from_options (priv->iface,
+				                                                               str_options,
+				                                                               priv->priority,
+				                                                               priv->info_only);
+			} else {
+				ip_config = (GObject *) nm_dhcp_utils_ip4_config_from_options (priv->iface,
+				                                                               str_options,
+				                                                               priv->priority);
 			}
+			g_warn_if_fail (ip_config != NULL);
 		}
-		else
-			nm_log_warn (LOGD_DHCP4, "ignoring invalid server identifier '%s'", str);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_dhcp_lease_time");
-	if (str) {
-		address.lifetime = address.preferred = strtoul (str, NULL, 10);
-		nm_log_info (LOGD_DHCP4, "  lease time %d", address.lifetime);
-	}
-
-	address.source = NM_PLATFORM_SOURCE_DHCP;
-	nm_ip4_config_add_address (ip4_config, &address);
-
-	str = g_hash_table_lookup (priv->options, "new_host_name");
-	if (str)
-		nm_log_info (LOGD_DHCP4, "  hostname '%s'", str);
-
-	str = g_hash_table_lookup (priv->options, "new_domain_name_servers");
-	if (str) {
-		char **searches = g_strsplit (str, " ", 0);
-		char **s;
-
-		for (s = searches; *s; s++) {
-			if (inet_pton (AF_INET, *s, &tmp_addr) > 0) {
-				nm_ip4_config_add_nameserver (ip4_config, tmp_addr);
-				nm_log_info (LOGD_DHCP4, "  nameserver '%s'", *s);
-			} else
-				nm_log_warn (LOGD_DHCP4, "ignoring invalid nameserver '%s'", *s);
-		}
-		g_strfreev (searches);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_domain_name");
-	if (str) {
-		char **domains = g_strsplit (str, " ", 0);
-		char **s;
-
-		for (s = domains; *s; s++) {
-			nm_log_info (LOGD_DHCP4, "  domain name '%s'", *s);
-			nm_ip4_config_add_domain (ip4_config, *s);
-		}
-		g_strfreev (domains);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_domain_search");
-	if (str)
-		process_domain_search (str, ip4_add_domain_search, ip4_config);
-
-	str = g_hash_table_lookup (priv->options, "new_netbios_name_servers");
-	if (str) {
-		char **searches = g_strsplit (str, " ", 0);
-		char **s;
-
-		for (s = searches; *s; s++) {
-			if (inet_pton (AF_INET, *s, &tmp_addr) > 0) {
-				nm_ip4_config_add_wins (ip4_config, tmp_addr);
-				nm_log_info (LOGD_DHCP4, "  wins '%s'", *s);
-			} else
-				nm_log_warn (LOGD_DHCP4, "ignoring invalid WINS server '%s'", *s);
-		}
-		g_strfreev (searches);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_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);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_nis_domain");
-	if (str) {
-		nm_log_info (LOGD_DHCP4, "  NIS domain '%s'", str);
-		nm_ip4_config_set_nis_domain (ip4_config, str);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_nis_servers");
-	if (str) {
-		char **searches = g_strsplit (str, " ", 0);
-		char **s;
-
-		for (s = searches; *s; s++) {
-			if (inet_pton (AF_INET, *s, &tmp_addr) > 0) {
-				nm_ip4_config_add_nis_server (ip4_config, tmp_addr);
-				nm_log_info (LOGD_DHCP4, "  nis '%s'", *s);
-			} else
-				nm_log_warn (LOGD_DHCP4, "ignoring invalid NIS server '%s'", *s);
-		}
-		g_strfreev (searches);
-	}
-
-	return ip4_config;
-
-error:
-	g_object_unref (ip4_config);
-	return NULL;
-}
-
-NMIP4Config *
-nm_dhcp_client_get_ip4_config (NMDHCPClient *self, gboolean test)
-{
-	NMDHCPClientPrivate *priv;
-
-	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL);
-
-	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-
-	if (test && !state_is_bound (priv->state)) {
-		nm_log_warn (LOGD_DHCP4, "(%s): DHCPv4 client didn't bind to a lease.", priv->iface);
-		return NULL;
-	}
-
-	if (!g_hash_table_size (priv->options)) {
-		/* We never got a response from the DHCP client */
-		return NULL;
-	}
-
-	return ip4_options_to_config (self);
-}
-
-/********************************************/
-
-static void
-ip6_add_domain_search (gpointer data, gpointer user_data)
-{
-	nm_ip6_config_add_search (NM_IP6_CONFIG (user_data), (const char *) data);
-}
-
-/* Given a table of DHCP options from the client, convert into an IP6Config */
-static NMIP6Config *
-ip6_options_to_config (NMDHCPClient *self)
-{
-	NMDHCPClientPrivate *priv;
-	NMIP6Config *ip6_config = NULL;
-	struct in6_addr tmp_addr;
-	NMPlatformIP6Address address;
-	char *str = NULL;
-	GHashTableIter iter;
-	gpointer key, value;
-
-	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL);
-
-	memset (&address, 0, sizeof (address));
-	address.plen = 128;
-	address.timestamp = nm_utils_get_monotonic_timestamp_s ();
-
-	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-	g_return_val_if_fail (priv->options != NULL, NULL);
-
-	g_hash_table_iter_init (&iter, priv->options);
-	while (g_hash_table_iter_next (&iter, &key, &value)) {
-		nm_log_dbg (LOGD_DHCP6, "(%s): option '%s'=>'%s'",
-		            priv->iface, (const char *) key, (const char *) value);
-	}
-
-	ip6_config = nm_ip6_config_new ();
-
-	str = g_hash_table_lookup (priv->options, "new_max_life");
-	if (str) {
-		address.lifetime = strtoul (str, NULL, 10);
-		nm_log_info (LOGD_DHCP6, "  valid_lft %d", address.lifetime);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_preferred_life");
-	if (str) {
-		address.preferred = strtoul (str, NULL, 10);
-		nm_log_info (LOGD_DHCP6, "  preferred_lft %d", address.preferred);
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_ip6_address");
-	if (str) {
-		if (!inet_pton (AF_INET6, str, &tmp_addr)) {
-			nm_log_warn (LOGD_DHCP6, "(%s): DHCP returned invalid address '%s'",
-			             priv->iface, str);
-			goto error;
-		}
-
-		address.address = tmp_addr;
-		address.source = NM_PLATFORM_SOURCE_DHCP;
-		nm_ip6_config_add_address (ip6_config, &address);
-		nm_log_info (LOGD_DHCP6, "  address %s", str);
-	} else if (priv->info_only == FALSE) {
-		/* No address in Managed mode is a hard error */
-		goto error;
-	}
-
-	str = g_hash_table_lookup (priv->options, "new_host_name");
-	if (str)
-		nm_log_info (LOGD_DHCP6, "  hostname '%s'", str);
-
-	str = g_hash_table_lookup (priv->options, "new_dhcp6_name_servers");
-	if (str) {
-		char **searches = g_strsplit (str, " ", 0);
-		char **s;
-
-		for (s = searches; *s; s++) {
-			if (inet_pton (AF_INET6, *s, &tmp_addr) > 0) {
-				nm_ip6_config_add_nameserver (ip6_config, &tmp_addr);
-				nm_log_info (LOGD_DHCP6, "  nameserver '%s'", *s);
-			} else
-				nm_log_warn (LOGD_DHCP6, "ignoring invalid nameserver '%s'", *s);
-		}
-		g_strfreev (searches);
 	}
 
-	str = g_hash_table_lookup (priv->options, "new_dhcp6_domain_search");
-	if (str)
-		process_domain_search (str, ip6_add_domain_search, ip6_config);
+	nm_dhcp_client_set_state (self, new_state, ip_config, str_options);
 
-	return ip6_config;
+	if (str_options)
+		g_hash_table_destroy (str_options);
+	g_clear_object (&ip_config);
 
-error:
-	g_object_unref (ip6_config);
-	return NULL;
-}
-
-NMIP6Config *
-nm_dhcp_client_get_ip6_config (NMDHCPClient *self, gboolean test)
-{
-	NMDHCPClientPrivate *priv;
-
-	g_return_val_if_fail (NM_IS_DHCP_CLIENT (self), NULL);
-
-	priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-
-	if (test && !state_is_bound (priv->state)) {
-		nm_log_warn (LOGD_DHCP6, "(%s): DHCPv6 client didn't bind to a lease.", priv->iface);
-		return NULL;
-	}
-
-	if (!g_hash_table_size (priv->options)) {
-		/* We never got a response from the DHCP client */
-		return NULL;
-	}
-
-	return ip6_options_to_config (self);
+	return TRUE;
 }
 
 /********************************************/
 
 static void
-nm_dhcp_client_init (NMDHCPClient *self)
+nm_dhcp_client_init (NMDhcpClient *self)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
-
-	priv->options = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-	priv->pid = -1;
+	NM_DHCP_CLIENT_GET_PRIVATE (self)->pid = -1;
 }
 
 static void
 get_property (GObject *object, guint prop_id,
 			  GValue *value, GParamSpec *pspec)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (object);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (object);
 
 	switch (prop_id) {
 	case PROP_IFACE:
 		g_value_set_string (value, priv->iface);
 		break;
+	case PROP_IFINDEX:
+		g_value_set_int (value, priv->ifindex);
+		break;
 	case PROP_HWADDR:
 		g_value_set_boxed (value, priv->hwaddr);
 		break;
@@ -1528,13 +826,18 @@ static void
 set_property (GObject *object, guint prop_id,
 			  const GValue *value, GParamSpec *pspec)
 {
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (object);
+	NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (object);
  
 	switch (prop_id) {
 	case PROP_IFACE:
 		/* construct-only */
 		priv->iface = g_strdup (g_value_get_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);
@@ -1563,23 +866,17 @@ set_property (GObject *object, guint prop_id,
 static void
 dispose (GObject *object)
 {
-	NMDHCPClient *self = NM_DHCP_CLIENT (object);
-	NMDHCPClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE (self);
+	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.
 	 */
 
-	if (priv->remove_id) {
-		g_source_remove (priv->remove_id);
-		priv->remove_id = 0;
-	}
+	watch_cleanup (self);
+	timeout_cleanup (self);
 
-	if (priv->options) {
-		g_hash_table_destroy (priv->options);
-		priv->options = NULL;
-	}
 	g_clear_pointer (&priv->iface, g_free);
 
 	if (priv->hwaddr) {
@@ -1596,11 +893,11 @@ dispose (GObject *object)
 }
 
 static void
-nm_dhcp_client_class_init (NMDHCPClientClass *client_class)
+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));
+	g_type_class_add_private (client_class, sizeof (NMDhcpClientPrivate));
 
 	/* virtual methods */
 	object_class->dispose = dispose;
@@ -1612,78 +909,59 @@ nm_dhcp_client_class_init (NMDHCPClientClass *client_class)
 
 	g_object_class_install_property
 		(object_class, PROP_IFACE,
-		 g_param_spec_string (NM_DHCP_CLIENT_INTERFACE,
-		                      "iface",
-		                      "Interface",
+		 g_param_spec_string (NM_DHCP_CLIENT_INTERFACE, "", "",
 		                      NULL,
-		                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                      G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
+		(object_class, PROP_IFINDEX,
+		 g_param_spec_int (NM_DHCP_CLIENT_IFINDEX, "", "",
+		                   -1, G_MAXINT, -1,
+		                   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
 
 	g_object_class_install_property
 		(object_class, PROP_HWADDR,
-		 g_param_spec_boxed (NM_DHCP_CLIENT_HWADDR,
-		                     "hwaddr",
-		                     "hardware address",
+		 g_param_spec_boxed (NM_DHCP_CLIENT_HWADDR, "", "",
 		                     G_TYPE_BYTE_ARRAY,
-		                     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		                     G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                     G_PARAM_STATIC_STRINGS));
 
 	g_object_class_install_property
 		(object_class, PROP_IPV6,
-		 g_param_spec_boolean (NM_DHCP_CLIENT_IPV6,
-		                       "ipv6",
-		                       "IPv6",
+		 g_param_spec_boolean (NM_DHCP_CLIENT_IPV6, "", "",
 		                       FALSE,
-		                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                       G_PARAM_STATIC_STRINGS));
 
 	g_object_class_install_property
 		(object_class, PROP_UUID,
-		 g_param_spec_string (NM_DHCP_CLIENT_UUID,
-		                      "uuid",
-		                      "UUID",
+		 g_param_spec_string (NM_DHCP_CLIENT_UUID, "", "",
 		                      NULL,
-		                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		                      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                      G_PARAM_STATIC_STRINGS));
 
 	g_object_class_install_property
 		(object_class, PROP_PRIORITY,
-		 g_param_spec_uint (NM_DHCP_CLIENT_PRIORITY,
-		                    "priority",
-		                    "Priority",
-		                    0, G_MAXUINT, 0,
-		                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		 g_param_spec_uint (NM_DHCP_CLIENT_PRIORITY, "", "",
+		                    0, G_MAXUINT32, 0,
+		                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                    G_PARAM_STATIC_STRINGS));
 
 	g_object_class_install_property
 		(object_class, PROP_TIMEOUT,
-		 g_param_spec_uint (NM_DHCP_CLIENT_TIMEOUT,
-		                    "timeout",
-		                    "Timeout",
+		 g_param_spec_uint (NM_DHCP_CLIENT_TIMEOUT, "", "",
 		                    0, G_MAXUINT, 45,
-		                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+		                    G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
+		                    G_PARAM_STATIC_STRINGS));
 
 	/* signals */
 	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,
-					  g_cclosure_marshal_VOID__UINT,
-					  G_TYPE_NONE, 1, G_TYPE_UINT);
-
-	signals[SIGNAL_TIMEOUT] =
-		g_signal_new (NM_DHCP_CLIENT_SIGNAL_TIMEOUT,
-					  G_OBJECT_CLASS_TYPE (object_class),
-					  G_SIGNAL_RUN_FIRST,
-					  G_STRUCT_OFFSET (NMDHCPClientClass, timeout),
-					  NULL, NULL,
-					  g_cclosure_marshal_VOID__VOID,
-					  G_TYPE_NONE, 0);
-
-	signals[SIGNAL_REMOVE] =
-		g_signal_new (NM_DHCP_CLIENT_SIGNAL_REMOVE,
-					  G_OBJECT_CLASS_TYPE (object_class),
-					  G_SIGNAL_RUN_FIRST,
-					  G_STRUCT_OFFSET (NMDHCPClientClass, remove),
-					  NULL, NULL,
-					  g_cclosure_marshal_VOID__VOID,
-					  G_TYPE_NONE, 0);
+					  G_STRUCT_OFFSET (NMDhcpClientClass, state_changed),
+					  NULL, NULL, NULL,
+					  G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_OBJECT, G_TYPE_HASH_TABLE);
 }