Blob Blame History Raw
// SPDX-License-Identifier: LGPL-2.1+
/*
 * Copyright (C) 2014 - 2019 Red Hat, Inc.
 */

#include "nm-default.h"

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <net/if_arp.h>

#include "nm-sd-adapt-shared.h"
#include "hostname-util.h"

#include "nm-glib-aux/nm-dedup-multi.h"
#include "nm-std-aux/unaligned.h"

#include "nm-utils.h"
#include "nm-config.h"
#include "nm-dhcp-utils.h"
#include "nm-dhcp-options.h"
#include "nm-core-utils.h"
#include "NetworkManagerUtils.h"
#include "platform/nm-platform.h"
#include "nm-dhcp-client-logging.h"
#include "n-dhcp4/src/n-dhcp4.h"
#include "systemd/nm-sd-utils-shared.h"
#include "systemd/nm-sd-utils-dhcp.h"

/*****************************************************************************/

#define NM_TYPE_DHCP_NETTOOLS            (nm_dhcp_nettools_get_type ())
#define NM_DHCP_NETTOOLS(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettools))
#define NM_DHCP_NETTOOLS_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettoolsClass))
#define NM_IS_DHCP_NETTOOLS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_DHCP_NETTOOLS))
#define NM_IS_DHCP_NETTOOLS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_DHCP_NETTOOLS))
#define NM_DHCP_NETTOOLS_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_DHCP_NETTOOLS, NMDhcpNettoolsClass))

typedef struct _NMDhcpNettools NMDhcpNettools;
typedef struct _NMDhcpNettoolsClass NMDhcpNettoolsClass;

/*****************************************************************************/

typedef struct {
	NDhcp4Client *client;
	NDhcp4ClientProbe *probe;
	NDhcp4ClientLease *lease;
	GSource *event_source;
	char *lease_file;
} NMDhcpNettoolsPrivate;

struct _NMDhcpNettools {
	NMDhcpClient parent;
	NMDhcpNettoolsPrivate _priv;
};

struct _NMDhcpNettoolsClass {
	NMDhcpClientClass parent;
};

G_DEFINE_TYPE (NMDhcpNettools, nm_dhcp_nettools, NM_TYPE_DHCP_CLIENT)

#define NM_DHCP_NETTOOLS_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMDhcpNettools, NM_IS_DHCP_NETTOOLS)

/*****************************************************************************/

static void
set_error_nettools (GError **error, int r, const char *message)
{
	/* the error code returned from n_dhcp4_* API is either a negative
	 * errno, or a positive internal error code. Generate different messages
	 * for these. */
	if (r < 0)
		nm_utils_error_set_errno (error, r, "%s: %s", message);
	else
		nm_utils_error_set (error, r, "%s (code %d)", message, r);
}

/*****************************************************************************/

#define DHCP_MAX_FQDN_LENGTH 255

enum {
	NM_IN_ADDR_CLASS_A,
	NM_IN_ADDR_CLASS_B,
	NM_IN_ADDR_CLASS_C,
	NM_IN_ADDR_CLASS_INVALID,
};

static int
in_addr_class (struct in_addr addr)
{
	switch (ntohl (addr.s_addr) >> 24) {
	case   0 ... 127:
		return NM_IN_ADDR_CLASS_A;
	case 128 ... 191:
		return NM_IN_ADDR_CLASS_B;
	case 192 ... 223:
		return NM_IN_ADDR_CLASS_C;
	default:
		return NM_IN_ADDR_CLASS_INVALID;
	}
}

static gboolean
lease_option_consume (void *out,
                      size_t n_out,
                      uint8_t **datap,
                      size_t *n_datap)
{
	if (*n_datap < n_out)
		return FALSE;

	memcpy (out, *datap, n_out);
	*datap += n_out;
	*n_datap -= n_out;
	return TRUE;
}

static gboolean
lease_option_next_in_addr (struct in_addr *addrp,
                           uint8_t **datap,
                           size_t *n_datap)
{
	return lease_option_consume (addrp, sizeof (struct in_addr), datap, n_datap);
}

static gboolean
lease_option_next_route (struct in_addr *destp,
                         uint8_t *plenp,
                         struct in_addr *gatewayp,
                         gboolean classless,
                         uint8_t **datap,
                         size_t *n_datap)
{
	struct in_addr dest = {}, gateway;
	uint8_t *data = *datap;
	size_t n_data = *n_datap;
	uint8_t plen;
	uint8_t bytes;

	if (classless) {
		if (!lease_option_consume (&plen, sizeof (plen), &data, &n_data))
			return FALSE;

		if (plen > 32)
			return FALSE;

		bytes = plen == 0 ? 0 : ((plen - 1) / 8) + 1;

		if (!lease_option_consume (&dest, bytes, &data, &n_data))
			return FALSE;
	} else {
		if (!lease_option_next_in_addr (&dest, &data, &n_data))
			return FALSE;

		switch (in_addr_class (dest)) {
		case NM_IN_ADDR_CLASS_A:
			plen = 8;
			break;
		case NM_IN_ADDR_CLASS_B:
			plen = 16;
			break;
		case NM_IN_ADDR_CLASS_C:
			plen = 24;
			break;
		case NM_IN_ADDR_CLASS_INVALID:
			return FALSE;
		}
	}

	dest.s_addr = nm_utils_ip4_address_clear_host_address (dest.s_addr, plen);

	if (!lease_option_next_in_addr (&gateway, &data, &n_data))
		return FALSE;

	*destp = dest;
	*plenp = plen;
	*gatewayp = gateway;
	*datap = data;
	*n_datap = n_data;
	return TRUE;
}

static gboolean
lease_option_print_label (GString *str, size_t n_label, uint8_t **datap, size_t *n_datap)
{
	for (size_t i = 0; i < n_label; ++i) {
		uint8_t c;

		if (!lease_option_consume(&c, sizeof (c), datap, n_datap))
			return FALSE;

		switch (c) {
                case 'a' ... 'z':
                case 'A' ... 'Z':
		case '0' ... '9':
                case '-':
		case '_':
			g_string_append_c(str, c);
			break;
		case '.':
		case '\\':
			g_string_append_printf(str, "\\%c", c);
			break;
		default:
			g_string_append_printf(str, "\\%3d", c);
		}
	}

	return TRUE;
}

static gboolean
lease_option_print_domain_name (GString *str, uint8_t *cache, size_t *n_cachep, uint8_t **datap, size_t *n_datap)
{
	uint8_t *domain;
	size_t n_domain, n_cache = *n_cachep;
	uint8_t **domainp = datap;
	size_t *n_domainp = n_datap;
	gboolean first = TRUE;
	uint8_t c;

	/*
	 * We are given two adjacent memory regions. The @cache contains alreday parsed
	 * domain names, and the @datap contains the remaining data to parse.
	 *
	 * A domain name is formed from a sequence of labels. Each label start with
	 * a length byte, where the two most significant bits are unset. A zero-length
	 * label indicates the end of the domain name.
	 *
	 * Alternatively, a label can be followed by an offset (indicated by the two
	 * most significant bits being set in the next byte that is read). The offset
	 * is an offset into the cache, where the next label of the domain name can
	 * be found.
	 *
	 * Note, that each time a jump to an offset is performed, the size of the
	 * cache shrinks, so this is guaranteed to terminate.
	 */
	if (cache + n_cache != *datap)
		return FALSE;

	for (;;) {
		if (!lease_option_consume(&c, sizeof (c), domainp, n_domainp))
			return FALSE;

		switch (c & 0xC0) {
		case 0x00: /* label length */
		{
			size_t n_label = c;

			if (n_label == 0) {
				/*
				 * We reached the final label of the domain name. Adjust
				 * the cache to include the consumed data, and return.
				 */
				*n_cachep = *datap - cache;
				return TRUE;
			}

			if (!first)
				g_string_append_c(str, '.');
			else
				first = FALSE;

			if (!lease_option_print_label (str, n_label, domainp, n_domainp))
				return FALSE;

			break;
		}
		case 0xC0: /* back pointer */
		{
			size_t offset = (c & 0x3F) << 16;

			/*
			 * The offset is given as two bytes (in big endian), where the
			 * two high bits are masked out.
			 */

			if (!lease_option_consume (&c, sizeof (c), domainp, n_domainp))
				return FALSE;

			offset += c;

			if (offset >= n_cache)
				return FALSE;

			domain = cache + offset;
			n_domain = n_cache - offset;
			n_cache = offset;

			domainp = &domain;
			n_domainp = &n_domain;

			break;
		}
		default:
			return FALSE;
		}
	}
}

static gboolean
lease_get_in_addr (NDhcp4ClientLease *lease,
                   guint8 option,
                   struct in_addr *addrp) {
	struct in_addr addr;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, option, &data, &n_data);
	if (r)
		return FALSE;

	if (!lease_option_next_in_addr (&addr, &data, &n_data))
		return FALSE;

	if (n_data != 0)
		return FALSE;

	*addrp = addr;
	return TRUE;
}

static gboolean
lease_get_u16 (NDhcp4ClientLease *lease,
               uint8_t option,
               uint16_t *u16p)
{
	uint8_t *data;
	size_t n_data;
	uint16_t be16;
	int r;

	r = n_dhcp4_client_lease_query (lease, option, &data, &n_data);
	if (r)
		return FALSE;

	if (n_data != sizeof (be16))
		return FALSE;

	memcpy (&be16, data, sizeof (be16));

	*u16p = ntohs(be16);
	return TRUE;
}

static gboolean
lease_parse_address (NDhcp4ClientLease *lease,
                     NMIP4Config *ip4_config,
                     GHashTable *options,
                     GError **error)
{
	char addr_str[NM_UTILS_INET_ADDRSTRLEN];
	struct in_addr a_address;
	struct in_addr a_netmask;
	struct in_addr a_next_server;
	guint32 a_plen;
	guint64 nettools_lifetime;
	guint32 a_lifetime;
	guint32 a_timestamp;
	guint64 a_expiry;

	n_dhcp4_client_lease_get_yiaddr (lease, &a_address);
	if (a_address.s_addr == INADDR_ANY) {
		nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, "could not get address from lease");
		return FALSE;
	}

	n_dhcp4_client_lease_get_lifetime (lease, &nettools_lifetime);

	if (nettools_lifetime == G_MAXUINT64) {
		a_timestamp = 0;
		a_lifetime = NM_PLATFORM_LIFETIME_PERMANENT;
		a_expiry = G_MAXUINT64;
	} else {
		guint64 nettools_basetime;
		guint64 lifetime;
		gint64 ts;

		n_dhcp4_client_lease_get_basetime (lease, &nettools_basetime);

		/* usually we shouldn't assert against external libraries like n-dhcp4.
		 * Here we still do it... it seems safe enough. */
		nm_assert (nettools_basetime > 0);
		nm_assert (nettools_lifetime >= nettools_basetime);
		nm_assert (((nettools_lifetime - nettools_basetime) % NM_UTILS_NSEC_PER_SEC) == 0);
		nm_assert ((nettools_lifetime - nettools_basetime) / NM_UTILS_NSEC_PER_SEC <= G_MAXUINT32);

		if (nettools_lifetime <= nettools_basetime) {
			/* A lease time of 0 is allowed on some dhcp servers, so, let's accept it. */
			lifetime = 0;
		} else {
			lifetime = nettools_lifetime - nettools_basetime;

			/* we "ceil" the value to the next second. In practice, we don't expect any sub-second values
			 * from n-dhcp4 anyway, so this should have no effect. */
			lifetime += NM_UTILS_NSEC_PER_SEC - 1;
		}

		ts = nm_utils_monotonic_timestamp_from_boottime (nettools_basetime, 1);

		/* the timestamp must be positive, because we only started nettools DHCP client
		 * after obtaining the first monotonic timestamp. Hence, the lease must have been
		 * received afterwards. */
		nm_assert (ts >= NM_UTILS_NSEC_PER_SEC);

		a_timestamp = ts / NM_UTILS_NSEC_PER_SEC;
		a_lifetime = NM_MIN (lifetime / NM_UTILS_NSEC_PER_SEC, NM_PLATFORM_LIFETIME_PERMANENT - 1);
		a_expiry = time (NULL) + ((lifetime - (nm_utils_clock_gettime_nsec (CLOCK_BOOTTIME) - nettools_basetime)) / NM_UTILS_NSEC_PER_SEC);
	}

	if (!lease_get_in_addr (lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &a_netmask)) {
		nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, "could not get netmask from lease");
		return FALSE;
	}

	_nm_utils_inet4_ntop (a_address.s_addr, addr_str);
	a_plen = nm_utils_ip4_netmask_to_prefix (a_netmask.s_addr);

	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS,
	                           addr_str);
	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_SUBNET_MASK,
	                           _nm_utils_inet4_ntop (a_netmask.s_addr, addr_str));

	nm_dhcp_option_add_option_u64 (options,
	                               _nm_dhcp_option_dhcp4_options,
	                               NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME,
	                               (guint64) a_lifetime);

	if (a_expiry != G_MAXUINT64) {
		nm_dhcp_option_add_option_u64 (options,
		                               _nm_dhcp_option_dhcp4_options,
		                               NM_DHCP_OPTION_DHCP4_NM_EXPIRY,
		                               a_expiry);
	}


	n_dhcp4_client_lease_get_siaddr (lease, &a_next_server);
	if (a_next_server.s_addr != INADDR_ANY) {
		_nm_utils_inet4_ntop (a_next_server.s_addr, addr_str);
		nm_dhcp_option_add_option (options,
		                           _nm_dhcp_option_dhcp4_options,
		                           NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER,
		                           addr_str);
	}

	nm_ip4_config_add_address (ip4_config,
	                           &((const NMPlatformIP4Address) {
	                                   .address      = a_address.s_addr,
	                                   .peer_address = a_address.s_addr,
	                                   .plen         = a_plen,
	                                   .addr_source  = NM_IP_CONFIG_SOURCE_DHCP,
	                                   .timestamp    = a_timestamp,
	                                   .lifetime     = a_lifetime,
	                                   .preferred    = a_lifetime,
	                           }));

	return TRUE;
}

static void
lease_parse_domain_name_servers (NDhcp4ClientLease *lease,
                                 NMIP4Config *ip4_config,
                                 GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	char addr_str[NM_UTILS_INET_ADDRSTRLEN];
	struct in_addr addr;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, &data, &n_data);
	if (r)
		return;

	nm_gstring_prepare (&str);

	while (lease_option_next_in_addr (&addr, &data, &n_data)) {

		_nm_utils_inet4_ntop (addr.s_addr, addr_str);
		g_string_append (nm_gstring_add_space_delimiter (str), addr_str);

		if (   addr.s_addr == 0
		    || nm_ip4_addr_is_localhost (addr.s_addr)) {
			/* Skip localhost addresses, like also networkd does.
			 * See https://github.com/systemd/systemd/issues/4524. */
			continue;
		}
		nm_ip4_config_add_nameserver (ip4_config, addr.s_addr);
	}

	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
	                           str->str);
}

static void
lease_parse_routes (NDhcp4ClientLease *lease,
                    NMIP4Config *ip4_config,
                    GHashTable *options,
                    guint32 route_table,
                    guint32 route_metric)
{
	nm_auto_free_gstring GString *str = NULL;
	char dest_str[NM_UTILS_INET_ADDRSTRLEN];
	char gateway_str[NM_UTILS_INET_ADDRSTRLEN];
	const char *s;
	struct in_addr dest, gateway;
	uint8_t plen;
	guint32 m;
	gboolean has_router_from_classless = FALSE, has_classless = FALSE;
	guint32 default_route_metric = route_metric;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, &data, &n_data);
	if (!r) {
		nm_gstring_prepare (&str);

		has_classless = TRUE;

		while (lease_option_next_route (&dest, &plen, &gateway, TRUE, &data, &n_data)) {

			_nm_utils_inet4_ntop (dest.s_addr, dest_str);
			_nm_utils_inet4_ntop (gateway.s_addr, gateway_str);

			g_string_append_printf (nm_gstring_add_space_delimiter (str),
			                        "%s/%d %s",
			                        dest_str,
			                        (int) plen,
			                        gateway_str);

			if (plen == 0) {
				/* if there are multiple default routes, we add them with differing
				 * metrics. */
				m = default_route_metric;
				if (default_route_metric < G_MAXUINT32)
					default_route_metric++;

				has_router_from_classless = TRUE;
			} else {
				m = route_metric;
                        }

			nm_ip4_config_add_route (ip4_config,
			                         &((const NMPlatformIP4Route) {
			                             .network       = dest.s_addr,
			                             .plen          = plen,
			                             .gateway       = gateway.s_addr,
			                             .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
			                             .metric        = m,
			                             .table_coerced = nm_platform_route_table_coerce (route_table),
			                         }),
			                         NULL);
		}
		nm_dhcp_option_add_option (options,
		                           _nm_dhcp_option_dhcp4_options,
		                           NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE,
		                           str->str);
	}

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, &data, &n_data);
	if (!r) {
		nm_gstring_prepare (&str);

		while (lease_option_next_route (&dest, &plen, &gateway, FALSE, &data, &n_data)) {

			_nm_utils_inet4_ntop (dest.s_addr, dest_str);
			_nm_utils_inet4_ntop (gateway.s_addr, gateway_str);

			g_string_append_printf (nm_gstring_add_space_delimiter (str),
			                        "%s/%d %s",
			                        dest_str,
			                        (int) plen,
			                        gateway_str);

			if (has_classless) {
				/* RFC 3443: if the DHCP server returns both a Classless Static Routes
				 * option and a Static Routes option, the DHCP client MUST ignore the
				 * Static Routes option. */
				continue;
			}

			if (plen == 0) {
				/* for option 33 (static route), RFC 2132 says:
				 *
				 * The default route (0.0.0.0) is an illegal destination for a static
				 * route. */
				continue;
			}

			nm_ip4_config_add_route (ip4_config,
			                         &((const NMPlatformIP4Route) {
			                             .network       = dest.s_addr,
			                             .plen          = plen,
			                             .gateway       = gateway.s_addr,
			                             .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
			                             .metric        = route_metric,
			                             .table_coerced = nm_platform_route_table_coerce (route_table),
			                         }),
			                         NULL);
		}
		nm_dhcp_option_add_option (options,
		                           _nm_dhcp_option_dhcp4_options,
		                           NM_DHCP_OPTION_DHCP4_STATIC_ROUTE,
		                           str->str);
	}

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_ROUTER, &data, &n_data);
	if (!r) {
		nm_gstring_prepare (&str);

		while (lease_option_next_in_addr (&gateway, &data, &n_data)) {
			s = _nm_utils_inet4_ntop (gateway.s_addr, gateway_str);
			g_string_append (nm_gstring_add_space_delimiter (str), s);

			if (gateway.s_addr == 0) {
				/* silently skip 0.0.0.0 */
				continue;
			}

			if (has_router_from_classless) {
				/* If the DHCP server returns both a Classless Static Routes option and a
				 * Router option, the DHCP client MUST ignore the Router option [RFC 3442].
				 *
				 * Be more lenient and ignore the Router option only if Classless Static
				 * Routes contain a default gateway (as other DHCP backends do).
				 */
				continue;
			}

			/* if there are multiple default routes, we add them with differing
			 * metrics. */
			m = default_route_metric;
			if (default_route_metric < G_MAXUINT32)
				default_route_metric++;

			nm_ip4_config_add_route (ip4_config,
			                         &((const NMPlatformIP4Route) {
			                                 .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
			                                 .gateway       = gateway.s_addr,
			                                 .table_coerced = nm_platform_route_table_coerce (route_table),
			                                 .metric        = m,
			                         }),
			                         NULL);
		}
		nm_dhcp_option_add_option (options,
		                           _nm_dhcp_option_dhcp4_options,
		                           NM_DHCP_OPTION_DHCP4_ROUTER,
		                           str->str);
	}
}

static void
lease_parse_mtu (NDhcp4ClientLease *lease,
                 NMIP4Config *ip4_config,
                 GHashTable *options)
{
	uint16_t mtu;

	if (!lease_get_u16 (lease, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, &mtu))
		return;

	if (mtu < 68)
		return;

	nm_dhcp_option_add_option_u64 (options,
	                               _nm_dhcp_option_dhcp4_options,
	                               NM_DHCP_OPTION_DHCP4_INTERFACE_MTU,
	                               mtu);
	nm_ip4_config_set_mtu (ip4_config, mtu, NM_IP_CONFIG_SOURCE_DHCP);
}

static void
lease_parse_metered (NDhcp4ClientLease *lease,
                     NMIP4Config *ip4_config,
                     GHashTable *options)
{
	gboolean metered = FALSE;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC, &data, &n_data);
	if (r)
		metered = FALSE;
	else
		metered = !!memmem (data, n_data, "ANDROID_METERED", NM_STRLEN ("ANDROID_METERED"));

	/* TODO: expose the vendor specific option when present */
	nm_ip4_config_set_metered (ip4_config, metered);
}

static void
lease_parse_ntps (NDhcp4ClientLease *lease,
                  GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	char addr_str[NM_UTILS_INET_ADDRSTRLEN];
	struct in_addr addr;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_NTP_SERVER, &data, &n_data);
	if (r)
		return;

	nm_gstring_prepare (&str);

	while (lease_option_next_in_addr (&addr, &data, &n_data)) {
		_nm_utils_inet4_ntop (addr.s_addr, addr_str);
		g_string_append (nm_gstring_add_space_delimiter (str), addr_str);
	}

	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_NTP_SERVER,
	                           str->str);
}

static void
lease_parse_hostname (NDhcp4ClientLease *lease,
                      GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_HOST_NAME, &data, &n_data);
	if (r)
		return;

	str = g_string_new_len ((char *)data, n_data);

	if (is_localhost(str->str))
		return;

	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_HOST_NAME,
	                           str->str);
}

static void
lease_parse_domainname (NDhcp4ClientLease *lease,
                        NMIP4Config *ip4_config,
                        GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	gs_strfreev char **domains = NULL;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, &data, &n_data);
	if (r)
		return;

	str = g_string_new_len ((char *)data, n_data);

	/* Multiple domains sometimes stuffed into option 15 "Domain Name". */
	domains = g_strsplit (str->str, " ", 0);
	nm_gstring_prepare (&str);

	for (char **d = domains; *d; d++) {
		if (is_localhost(*d))
			return;

		g_string_append (nm_gstring_add_space_delimiter (str), *d);
		nm_ip4_config_add_domain (ip4_config, *d);
	}
	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_DOMAIN_NAME,
	                           str->str);
}

char **
nm_dhcp_parse_search_list (guint8 *data, size_t n_data)
{
	GPtrArray *array = NULL;
	guint8 *cache = data;
	size_t n_cache = 0;

	for (;;) {
		nm_auto_free_gstring GString *domain = NULL;

		nm_gstring_prepare (&domain);

		if (!lease_option_print_domain_name (domain, cache, &n_cache, &data, &n_data))
			break;

		if (!array)
			array = g_ptr_array_new ();

		g_ptr_array_add (array, g_string_free (domain, FALSE));
		domain = NULL;
	}

	if (array) {
		g_ptr_array_add (array, NULL);
		return (char **) g_ptr_array_free (array, FALSE);
	} else
		return NULL;
}

static void
lease_parse_search_domains (NDhcp4ClientLease *lease,
                            NMIP4Config *ip4_config,
                            GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	uint8_t *data;
	size_t n_data;
	gs_strfreev char **domains = NULL;
	guint i;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, &data, &n_data);
	if (r)
		return;

	domains = nm_dhcp_parse_search_list (data, n_data);
	nm_gstring_prepare (&str);

	for (i = 0; domains && domains[i]; i++) {
		g_string_append (nm_gstring_add_space_delimiter (str), domains[i]);
		nm_ip4_config_add_search (ip4_config, domains[i]);
	}
	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST,
	                           str->str);
}

static void
lease_parse_root_path (NDhcp4ClientLease *lease,
                       GHashTable *options)
{
	nm_auto_free_gstring GString *str = NULL;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_ROOT_PATH, &data, &n_data);
	if (r)
		return;

	str = g_string_new_len ((char *)data, n_data);
	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_ROOT_PATH,
	                           str->str);
}

static void
lease_parse_wpad (NDhcp4ClientLease *lease,
                  GHashTable *options)
{
	gs_free char *wpad = NULL;
	uint8_t *data;
	size_t n_data;
	int r;

	r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, &data, &n_data);
	if (r)
		return;

	nm_utils_buf_utf8safe_escape ((char *)data, n_data, 0, &wpad);
	if (wpad == NULL)
		wpad = g_strndup ((char *)data, n_data);

	nm_dhcp_option_add_option (options,
	                           _nm_dhcp_option_dhcp4_options,
	                           NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
	                           wpad);
}

static void
lease_parse_private_options (NDhcp4ClientLease *lease,
                             GHashTable *options)
{
	int i;

	for (i = NM_DHCP_OPTION_DHCP4_PRIVATE_224; i <= NM_DHCP_OPTION_DHCP4_PRIVATE_254; i++) {
		gs_free char *option_string = NULL;
		guint8 *data;
		gsize n_data;
		int r;

		/* We manage private options 249 (private classless static route) and 252 (wpad) in a special
		 * way, so skip them as we here just manage all (the other) private options as raw data */
		if (NM_IN_SET (i, NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE,
		                  NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY))
			continue;

		r = n_dhcp4_client_lease_query (lease, i, &data, &n_data);
		if (r)
			continue;

		option_string = nm_utils_bin2hexstr_full (data, n_data, ':', FALSE, NULL);
		nm_dhcp_option_take_option (options,
		                            _nm_dhcp_option_dhcp4_options,
		                            i,
		                            g_steal_pointer (&option_string));
	}
}

static NMIP4Config *
lease_to_ip4_config (NMDedupMultiIndex *multi_idx,
                     const char *iface,
                     int ifindex,
                     NDhcp4ClientLease *lease,
                     guint32 route_table,
                     guint32 route_metric,
                     GHashTable **out_options,
                     GError **error)
{
	gs_unref_object NMIP4Config *ip4_config = NULL;
	gs_unref_hashtable GHashTable *options = NULL;

	g_return_val_if_fail (lease != NULL, NULL);

	ip4_config = nm_ip4_config_new (multi_idx, ifindex);
	options = nm_dhcp_option_create_options_dict ();

	if (!lease_parse_address (lease, ip4_config, options, error))
		return NULL;

	lease_parse_routes (lease, ip4_config, options, route_table, route_metric);
	lease_parse_domain_name_servers (lease, ip4_config, options);
	lease_parse_domainname (lease, ip4_config, options);
	lease_parse_search_domains (lease, ip4_config, options);
	lease_parse_mtu (lease, ip4_config, options);
	lease_parse_metered (lease, ip4_config, options);

	lease_parse_hostname (lease, options);
	lease_parse_ntps (lease, options);
	lease_parse_root_path (lease, options);
	lease_parse_wpad (lease, options);
	lease_parse_private_options (lease, options);

	NM_SET_OUT (out_options, g_steal_pointer (&options));
	return g_steal_pointer (&ip4_config);
}

/*****************************************************************************/

static void
lease_save (NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_file)
{
	struct in_addr a_address;
	nm_auto_free_gstring GString *new_contents = NULL;
	char sbuf[NM_UTILS_INET_ADDRSTRLEN];
	gs_free_error GError *error = NULL;

	nm_assert (lease);
	nm_assert (lease_file);

	new_contents = g_string_new ("# This is private data. Do not parse.\n");

	n_dhcp4_client_lease_get_yiaddr (lease, &a_address);
	if (a_address.s_addr == INADDR_ANY)
		return;

	g_string_append_printf (new_contents,
	                        "ADDRESS=%s\n", _nm_utils_inet4_ntop (a_address.s_addr, sbuf));

	if (!g_file_set_contents (lease_file,
	                          new_contents->str,
	                          -1,
	                          &error))
		_LOGW ("error saving lease to %s: %s", lease_file, error->message);
}

static void
bound4_handle (NMDhcpNettools *self, NDhcp4ClientLease *lease, gboolean extended)
{
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	const char *iface = nm_dhcp_client_get_iface (NM_DHCP_CLIENT (self));
	gs_unref_object NMIP4Config *ip4_config = NULL;
	gs_unref_hashtable GHashTable *options = NULL;
	GError *error = NULL;

	_LOGT ("lease available (%s)", extended ? "extended" : "new");

	ip4_config = lease_to_ip4_config (nm_dhcp_client_get_multi_idx (NM_DHCP_CLIENT (self)),
	                                  iface,
	                                  nm_dhcp_client_get_ifindex (NM_DHCP_CLIENT (self)),
	                                  lease,
	                                  nm_dhcp_client_get_route_table (NM_DHCP_CLIENT (self)),
	                                  nm_dhcp_client_get_route_metric (NM_DHCP_CLIENT (self)),
	                                  &options,
	                                  &error);
	if (!ip4_config) {
		_LOGW ("%s", error->message);
		g_clear_error (&error);
		nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL);
		return;
	}

	nm_dhcp_option_add_requests_to_options (options, _nm_dhcp_option_dhcp4_options);
	lease_save (self, lease, priv->lease_file);

	nm_dhcp_client_set_state (NM_DHCP_CLIENT (self),
	                          extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND,
	                          NM_IP_CONFIG_CAST (ip4_config),
	                          options);
}

static gboolean
dhcp4_event_handle (NMDhcpNettools *self,
                    NDhcp4ClientEvent *event)
{
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	int r;

	_LOGT ("client event %d", event->event);

	switch (event->event) {
	case N_DHCP4_CLIENT_EVENT_OFFER:
		/* always accept the first lease */
		r = n_dhcp4_client_lease_select (event->offer.lease);
		if (r)
			_LOGW ("selecting lease failed: %d", r);
		break;
	case N_DHCP4_CLIENT_EVENT_RETRACTED:
	case N_DHCP4_CLIENT_EVENT_EXPIRED:
		nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_EXPIRE, NULL, NULL);
		break;
	case N_DHCP4_CLIENT_EVENT_CANCELLED:
		nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL);
		break;
	case N_DHCP4_CLIENT_EVENT_GRANTED:
		priv->lease = n_dhcp4_client_lease_ref (event->granted.lease);
		bound4_handle (self, event->granted.lease, FALSE);
		break;
	case N_DHCP4_CLIENT_EVENT_EXTENDED:
		bound4_handle (self, event->extended.lease, TRUE);
		break;
	case N_DHCP4_CLIENT_EVENT_DOWN:
		/* ignore down events, they are purely informational */
		break;
	case N_DHCP4_CLIENT_EVENT_LOG: {
			NMLogLevel nm_level;

			nm_level = nm_log_level_from_syslog (event->log.level);
			if (nm_logging_enabled (nm_level, LOGD_DHCP4)) {
				nm_log (nm_level, LOGD_DHCP4, NULL , NULL,
				        "dhcp4 (%s): %s",
				        nm_dhcp_client_get_iface (NM_DHCP_CLIENT (self)),
				        event->log.message);
			}
		}
		break;
	default:
		_LOGW ("unhandled DHCP event %d", event->event);
		break;
	}

	return TRUE;
}

static gboolean
dhcp4_event_cb (int fd,
                GIOCondition condition,
                gpointer data)
{
	NMDhcpNettools *self = data;
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	NDhcp4ClientEvent *event;
	int r;

	r = n_dhcp4_client_dispatch (priv->client);
	if (r < 0) {
		/* FIXME: if any operation (e.g. send()) fails during the
		 * dispatch, n-dhcp4 returns an error without arming timers
		 * or progressing state, so the only reasonable thing to do
		 * is to move to failed state so that the client will be
		 * restarted. Ideally n-dhcp4 should retry failed operations
		 * a predefined number of times (possibly infinite).
		 */
		_LOGE ("error %d dispatching events", r);
		nm_clear_g_source_inst (&priv->event_source);
		nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_FAIL, NULL, NULL);
		return G_SOURCE_REMOVE;
	}

	while (!n_dhcp4_client_pop_event (priv->client, &event) && event) {
		dhcp4_event_handle (self, event);
	}

	return G_SOURCE_CONTINUE;
}

static gboolean
nettools_create (NMDhcpNettools *self,
                 const char *dhcp_anycast_addr,
                 GError **error)
{
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	nm_auto (n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
	nm_auto (n_dhcp4_client_unrefp) NDhcp4Client *client = NULL;
	GBytes *hwaddr;
	GBytes *bcast_hwaddr;
	const uint8_t *hwaddr_arr;
	const uint8_t *bcast_hwaddr_arr;
	gsize hwaddr_len;
	gsize bcast_hwaddr_len;
	GBytes *client_id;
	gs_unref_bytes GBytes *client_id_new = NULL;
	const uint8_t *client_id_arr;
	size_t client_id_len;
	int r, fd, arp_type, transport;

	g_return_val_if_fail (!priv->client, FALSE);

	hwaddr = nm_dhcp_client_get_hw_addr (NM_DHCP_CLIENT (self));
	if (   !hwaddr
	    || !(hwaddr_arr = g_bytes_get_data (hwaddr, &hwaddr_len))
	    || (arp_type = nm_utils_arp_type_detect_from_hwaddrlen (hwaddr_len)) < 0) {
		nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address");
		return FALSE;
	}

	bcast_hwaddr = nm_dhcp_client_get_broadcast_hw_addr (NM_DHCP_CLIENT (self));
	bcast_hwaddr_arr = g_bytes_get_data (bcast_hwaddr, &bcast_hwaddr_len);

	switch (arp_type) {
	case ARPHRD_ETHER:
		transport = N_DHCP4_TRANSPORT_ETHERNET;
		break;
	case ARPHRD_INFINIBAND:
		transport = N_DHCP4_TRANSPORT_INFINIBAND;
		break;
	default:
		nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, "unsupported ARP type");
		return FALSE;
	}

	/* Note that we always set a client-id. In particular for infiniband that is necessary,
	 * see https://tools.ietf.org/html/rfc4390#section-2.1 . */
	client_id = nm_dhcp_client_get_client_id (NM_DHCP_CLIENT (self));
	if (!client_id) {
		client_id_new = nm_utils_dhcp_client_id_mac (arp_type, hwaddr_arr, hwaddr_len);
		client_id = client_id_new;
	}

	if (   !(client_id_arr = g_bytes_get_data (client_id, &client_id_len))
	    || client_id_len < 2) {

		/* invalid client-ids are not expected. */
		nm_assert_not_reached ();

		nm_utils_error_set_literal (error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id");
		return FALSE;
	}

	r = n_dhcp4_client_config_new (&config);
	if (r) {
		set_error_nettools (error, r, "failed to create client-config");
		return FALSE;
	}

	n_dhcp4_client_config_set_ifindex (config, nm_dhcp_client_get_ifindex (NM_DHCP_CLIENT (self)));
	n_dhcp4_client_config_set_transport (config, transport);
	n_dhcp4_client_config_set_mac (config, hwaddr_arr, hwaddr_len);
	n_dhcp4_client_config_set_broadcast_mac (config, bcast_hwaddr_arr, bcast_hwaddr_len);
	r = n_dhcp4_client_config_set_client_id (config,
	                                         client_id_arr,
	                                         NM_MIN (client_id_len, 1 + _NM_SD_MAX_CLIENT_ID_LEN));
	if (r) {
		set_error_nettools (error, r, "failed to set client-id");
		return FALSE;
	}

	r = n_dhcp4_client_new (&client, config);
	if (r) {
		set_error_nettools (error, r, "failed to create client");
		return FALSE;
	}

	priv->client = client;
	client = NULL;

	n_dhcp4_client_set_log_level (priv->client, nm_log_level_to_syslog (nm_logging_get_level (LOGD_DHCP4)));

	n_dhcp4_client_get_fd (priv->client, &fd);

	priv->event_source = nm_g_unix_fd_source_new (fd,
	                                              G_IO_IN,
	                                              G_PRIORITY_DEFAULT,
	                                              dhcp4_event_cb,
	                                              self,
	                                              NULL);
	g_source_attach (priv->event_source, NULL);

	return TRUE;
}

static gboolean
_accept (NMDhcpClient *client,
         GError **error)
{
	NMDhcpNettools *self = NM_DHCP_NETTOOLS (client);
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	int r;

	g_return_val_if_fail (priv->lease, FALSE);

	_LOGT ("accept");

	r = n_dhcp4_client_lease_accept (priv->lease);
	if (r) {
		set_error_nettools (error, r, "failed to accept lease");
		return FALSE;
	}

	priv->lease = n_dhcp4_client_lease_unref (priv->lease);

	return TRUE;
}

static gboolean
decline (NMDhcpClient *client,
         const char *error_message,
         GError **error)
{
	NMDhcpNettools *self = NM_DHCP_NETTOOLS (client);
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	int r;

	g_return_val_if_fail (priv->lease, FALSE);

	_LOGT ("dhcp4-client: decline");

	r = n_dhcp4_client_lease_decline (priv->lease, error_message);
	if (r) {
		set_error_nettools (error, r, "failed to decline lease");
		return FALSE;
	}

	priv->lease = n_dhcp4_client_lease_unref (priv->lease);

	return TRUE;
}

static guint8
fqdn_flags_to_wire (NMDhcpHostnameFlags flags)
{
	guint r = 0;

	/* RFC 4702 section 2.1 */
	if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE)
		r |= (1 << 0);
	if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED)
		r |= (1 << 2);
	if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE)
		r |= (1 << 3);

	return r;
}

static gboolean
ip4_start (NMDhcpClient *client,
           const char *dhcp_anycast_addr,
           const char *last_ip4_address,
           GError **error)
{
	nm_auto (n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
	NMDhcpNettools *self = NM_DHCP_NETTOOLS (client);
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);
	gs_free char *lease_file = NULL;
	struct in_addr last_addr = { 0 };
	const char *hostname;
	const char *mud_url;
	GBytes *vendor_class_identifier;
	int r, i;

	g_return_val_if_fail (!priv->probe, FALSE);

	if (!nettools_create (self, dhcp_anycast_addr, error))
		return FALSE;

	r = n_dhcp4_client_probe_config_new (&config);
	if (r) {
		set_error_nettools (error, r, "failed to create dhcp-client-probe-config");
		return FALSE;
	}

	/*
	 * FIXME:
	 * Select, or configure, a reasonable start delay, to protect poor servers beeing flooded.
	 */
	n_dhcp4_client_probe_config_set_start_delay (config, 1);

	nm_dhcp_utils_get_leasefile_path (AF_INET,
	                                  "internal",
	                                  nm_dhcp_client_get_iface (client),
	                                  nm_dhcp_client_get_uuid (client),
	                                  &lease_file);

	if (last_ip4_address)
		inet_pton (AF_INET, last_ip4_address, &last_addr);
	else {
		/*
		 * TODO: we stick to the systemd-networkd lease file format. Quite easy for now to
		 * just use the functions in systemd code. Anyway, as in the end we just use the
		 * ip address from all the options found in the lease, write a function that parses
		 * the lease file just for the assigned address and returns it in &last_address.
		 * Then drop reference to systemd-networkd structures and functions.
		 */
		nm_auto (sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;

		dhcp_lease_load (&lease, lease_file);
		if (lease)
			sd_dhcp_lease_get_address (lease, &last_addr);
	}

	if (last_addr.s_addr) {
		n_dhcp4_client_probe_config_set_requested_ip (config, last_addr);
		n_dhcp4_client_probe_config_set_init_reboot (config, TRUE);
	}

	/* Add requested options */
	for (i = 0; _nm_dhcp_option_dhcp4_options[i].name; i++) {
		if (_nm_dhcp_option_dhcp4_options[i].include) {
			nm_assert (_nm_dhcp_option_dhcp4_options[i].option_num <= 255);
			n_dhcp4_client_probe_config_request_option (config,
			                                            _nm_dhcp_option_dhcp4_options[i].option_num);
		}
	}

	mud_url = nm_dhcp_client_get_mud_url (client);
	if (mud_url) {
		r = n_dhcp4_client_probe_config_append_option (config,
		                                               NM_DHCP_OPTION_DHCP4_MUD_URL,
		                                               mud_url,
		                                               strlen (mud_url));
		if (r) {
			set_error_nettools (error, r, "failed to set MUD URL");
			return FALSE;
		}
	}
	hostname = nm_dhcp_client_get_hostname (client);
	if (hostname) {
		if (nm_dhcp_client_get_use_fqdn (client)) {
			uint8_t buffer[255];
			NMDhcpHostnameFlags flags;
			size_t fqdn_len;

			flags = nm_dhcp_client_get_hostname_flags (client);
			buffer[0] = fqdn_flags_to_wire (flags);
			buffer[1] = 0;   /* RCODE1 (deprecated) */
			buffer[2] = 0;   /* RCODE2 (deprecated) */

			if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) {
				r = nm_sd_dns_name_to_wire_format (hostname,
				                                   buffer + 3,
				                                   sizeof (buffer) - 3,
				                                   FALSE);
				if (r <= 0) {
					if (r < 0)
						nm_utils_error_set_errno (error, r, "failed to convert DHCP FQDN: %s");
					else
						nm_utils_error_set (error, r, "failed to convert DHCP FQDN");
					return FALSE;
				}
				fqdn_len = r;
			} else {
				fqdn_len = strlen (hostname);
				if (fqdn_len > sizeof (buffer) - 3) {
					nm_utils_error_set (error, r, "failed to set DHCP FQDN: name too long");
					return FALSE;
				}
				memcpy (buffer + 3, hostname, fqdn_len);
			}

			r = n_dhcp4_client_probe_config_append_option (config,
			                                               NM_DHCP_OPTION_DHCP4_CLIENT_FQDN,
			                                               buffer,
			                                               3 + fqdn_len);
			if (r) {
				set_error_nettools (error, r, "failed to set DHCP FQDN");
				return FALSE;
			}
		} else {
			r = n_dhcp4_client_probe_config_append_option (config,
			                                               NM_DHCP_OPTION_DHCP4_HOST_NAME,
			                                               hostname,
			                                               strlen (hostname));
			if (r) {
				set_error_nettools (error, r, "failed to set DHCP hostname");
				return FALSE;
			}
		}
	}

	vendor_class_identifier = nm_dhcp_client_get_vendor_class_identifier (client);
	if (vendor_class_identifier) {
		const void *option_data;
		gsize option_size;

		option_data = g_bytes_get_data (vendor_class_identifier, &option_size);
		nm_assert (option_data);
		nm_assert (option_size <= 255);

		r = n_dhcp4_client_probe_config_append_option (config,
		                                               NM_DHCP_OPTION_DHCP4_VENDOR_CLASS_IDENTIFIER,
		                                               option_data,
		                                               option_size);
		if (r) {
			set_error_nettools (error, r, "failed to set vendor class identifier");
			return FALSE;
		}
	}

	g_free (priv->lease_file);
	priv->lease_file = g_steal_pointer (&lease_file);

	r = n_dhcp4_client_probe (priv->client, &priv->probe, config);
	if (r) {
		set_error_nettools (error, r, "failed to start DHCP client");
		return FALSE;
	}

	_LOGT ("dhcp-client4: start %p", (gpointer) priv->client);

	nm_dhcp_client_start_timeout (client);
	return TRUE;
}

static void
stop (NMDhcpClient *client,
      gboolean release)
{
	NMDhcpNettools *self = NM_DHCP_NETTOOLS (client);
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (self);

	NM_DHCP_CLIENT_CLASS (nm_dhcp_nettools_parent_class)->stop (client, release);

	_LOGT ("dhcp-client4: stop %p",
	       (gpointer) priv->client);

	priv->probe = n_dhcp4_client_probe_free (priv->probe);
}

/*****************************************************************************/

static void
nm_dhcp_nettools_init (NMDhcpNettools *self)
{
}

static void
dispose (GObject *object)
{
	NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE (object);

	nm_clear_g_free (&priv->lease_file);
	nm_clear_g_source_inst (&priv->event_source);
	nm_clear_pointer (&priv->lease, n_dhcp4_client_lease_unref);
	nm_clear_pointer (&priv->probe, n_dhcp4_client_probe_free);
	nm_clear_pointer (&priv->client, n_dhcp4_client_unref);

	G_OBJECT_CLASS (nm_dhcp_nettools_parent_class)->dispose (object);
}

static void
nm_dhcp_nettools_class_init (NMDhcpNettoolsClass *class)
{
	NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS (class);
	GObjectClass *object_class = G_OBJECT_CLASS (class);

	object_class->dispose = dispose;

	client_class->ip4_start = ip4_start;
	client_class->accept = _accept;
	client_class->decline = decline;
	client_class->stop = stop;
}

const NMDhcpClientFactory _nm_dhcp_client_factory_nettools = {
	.name         = "nettools",
	.get_type     = nm_dhcp_nettools_get_type,
	.experimental = TRUE,
};