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