diff options
Diffstat (limited to 'src/core/dhcp/nm-dhcp-nettools.c')
| -rw-r--r-- | src/core/dhcp/nm-dhcp-nettools.c | 1247 |
1 files changed, 1247 insertions, 0 deletions
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c new file mode 100644 index 00000000..116e1bdb --- /dev/null +++ b/src/core/dhcp/nm-dhcp-nettools.c @@ -0,0 +1,1247 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2014 - 2019 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.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-glib-aux/nm-dedup-multi.h" +#include "nm-std-aux/unaligned.h" +#include "nm-glib-aux/nm-str-buf.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); +} + +static inline int +_client_lease_query(NDhcp4ClientLease *lease, + uint8_t option, + const uint8_t ** datap, + size_t * n_datap) +{ + return n_dhcp4_client_lease_query(lease, option, (guint8 **) datap, n_datap); +} + +/*****************************************************************************/ + +#define DHCP_MAX_FQDN_LENGTH 255 + +/*****************************************************************************/ + +static gboolean +lease_option_consume_route(const uint8_t **datap, + size_t * n_datap, + gboolean classless, + in_addr_t * destp, + uint8_t * plenp, + in_addr_t * gatewayp) +{ + in_addr_t dest; + in_addr_t gateway; + const uint8_t *data = *datap; + size_t n_data = *n_datap; + uint8_t plen; + + if (classless) { + uint8_t bytes; + + if (!nm_dhcp_lease_data_consume(&data, &n_data, &plen, sizeof(plen))) + return FALSE; + + if (plen > 32) + return FALSE; + + bytes = plen == 0 ? 0 : ((plen - 1) / 8) + 1; + + dest = 0; + if (!nm_dhcp_lease_data_consume(&data, &n_data, &dest, bytes)) + return FALSE; + } else { + if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &dest)) + return FALSE; + + plen = _nm_utils_ip4_get_default_prefix0(dest); + if (plen == 0) + return FALSE; + } + + dest = nm_utils_ip4_address_clear_host_address(dest, plen); + + if (!nm_dhcp_lease_data_consume_in_addr(&data, &n_data, &gateway)) + return FALSE; + + *destp = dest; + *plenp = plen; + *gatewayp = gateway; + *datap = data; + *n_datap = n_data; + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +lease_parse_address(NDhcp4ClientLease *lease, + NMIP4Config * ip4_config, + GHashTable * options, + GError ** error) +{ + struct in_addr a_address; + in_addr_t a_netmask; + struct in_addr a_next_server; + guint32 a_plen; + guint64 nettools_lifetime; + guint32 a_lifetime; + guint32 a_timestamp; + guint64 a_expiry; + const guint8 * l_data; + gsize l_data_len; + int r; + + 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); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &l_data, &l_data_len); + if (r != 0 || !nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &a_netmask)) { + nm_utils_error_set_literal(error, + NM_UTILS_ERROR_UNKNOWN, + "could not get netmask from lease"); + return FALSE; + } + + a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask); + + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, + a_address.s_addr); + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_SUBNET_MASK, + a_netmask); + + nm_dhcp_option_add_option_u64(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME, + (guint64) a_lifetime); + + if (a_expiry != G_MAXUINT64) { + nm_dhcp_option_add_option_u64(options, AF_INET, 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_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, + a_next_server.s_addr); + } + + 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_address_list(NDhcp4ClientLease * lease, + NMIP4Config * ip4_config, + NMDhcpOptionDhcp4Options option, + GHashTable * options, + NMStrBuf * sbuf) +{ + const guint8 *l_data; + gsize l_data_len; + int r; + + r = _client_lease_query(lease, option, &l_data, &l_data_len); + if (r != 0) + return; + + if (l_data_len == 0 || l_data_len % 4 != 0) + return; + + nm_str_buf_reset(sbuf); + + for (; l_data_len > 0; l_data_len -= 4, l_data += 4) { + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + const in_addr_t addr = unaligned_read_ne32(l_data); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(addr, addr_str)); + + switch (option) { + case NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER: + if (addr == 0 || nm_ip4_addr_is_localhost(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); + break; + case NM_DHCP_OPTION_DHCP4_NIS_SERVERS: + nm_ip4_config_add_nis_server(ip4_config, addr); + break; + case NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER: + nm_ip4_config_add_wins(ip4_config, addr); + break; + case NM_DHCP_OPTION_DHCP4_NTP_SERVER: + break; + default: + nm_assert_not_reached(); + } + } + + nm_dhcp_option_add_option(options, AF_INET, option, nm_str_buf_get_str(sbuf)); +} + +static void +lease_parse_routes(NDhcp4ClientLease *lease, + NMIP4Config * ip4_config, + GHashTable * options, + guint32 route_table, + guint32 route_metric, + NMStrBuf * sbuf) +{ + char dest_str[NM_UTILS_INET_ADDRSTRLEN]; + char gateway_str[NM_UTILS_INET_ADDRSTRLEN]; + in_addr_t dest; + in_addr_t gateway; + uint8_t plen; + guint32 m; + gboolean has_router_from_classless = FALSE; + gboolean has_classless = FALSE; + guint32 default_route_metric = route_metric; + const guint8 *l_data; + gsize l_data_len; + int r; + + r = _client_lease_query(lease, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + &l_data, + &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + has_classless = TRUE; + + while (lease_option_consume_route(&l_data, &l_data_len, TRUE, &dest, &plen, &gateway)) { + _nm_utils_inet4_ntop(dest, dest_str); + _nm_utils_inet4_ntop(gateway, gateway_str); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append_printf(sbuf, "%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, + .plen = plen, + .gateway = gateway, + .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, + AF_INET, + NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE, + nm_str_buf_get_str(sbuf)); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, &l_data, &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + while (lease_option_consume_route(&l_data, &l_data_len, FALSE, &dest, &plen, &gateway)) { + _nm_utils_inet4_ntop(dest, dest_str); + _nm_utils_inet4_ntop(gateway, gateway_str); + + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append_printf(sbuf, "%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, + .plen = plen, + .gateway = gateway, + .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, + AF_INET, + NM_DHCP_OPTION_DHCP4_STATIC_ROUTE, + nm_str_buf_get_str(sbuf)); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROUTER, &l_data, &l_data_len); + if (r == 0) { + nm_str_buf_reset(sbuf); + + while (nm_dhcp_lease_data_consume_in_addr(&l_data, &l_data_len, &gateway)) { + nm_str_buf_append_required_delimiter(sbuf, ' '); + nm_str_buf_append(sbuf, _nm_utils_inet4_ntop(gateway, gateway_str)); + + if (gateway == 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, + .table_coerced = nm_platform_route_table_coerce(route_table), + .metric = m, + }), + NULL); + } + + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_ROUTER, + nm_str_buf_get_str(sbuf)); + } +} + +static void +lease_parse_search_domains(NDhcp4ClientLease *lease, NMIP4Config *ip4_config, GHashTable *options) +{ + gs_strfreev char **domains = NULL; + const guint8 * l_data; + gsize l_data_len; + guint i; + int r; + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, &l_data, &l_data_len); + if (r != 0) + return; + + domains = nm_dhcp_lease_data_parse_search_list(l_data, l_data_len); + + if (!domains || !domains[0]) + return; + + for (i = 0; domains[i]; i++) + nm_ip4_config_add_search(ip4_config, domains[i]); + + nm_dhcp_option_take_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, + g_strjoinv(" ", domains)); +} + +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; + const guint8 *l_data; + gsize l_data_len; + 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 = _client_lease_query(lease, i, &l_data, &l_data_len); + if (r) + continue; + + option_string = nm_utils_bin2hexstr_full(l_data, l_data_len, ':', FALSE, NULL); + nm_dhcp_option_take_option(options, AF_INET, 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) +{ + nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(0, FALSE); + gs_unref_object NMIP4Config *ip4_config = NULL; + gs_unref_hashtable GHashTable *options = NULL; + const guint8 * l_data; + gsize l_data_len; + const char * v_str; + guint16 v_u16; + gboolean v_bool; + in_addr_t v_inaddr; + struct in_addr v_inaddr_s; + int r; + + 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; + + r = n_dhcp4_client_lease_get_server_identifier(lease, &v_inaddr_s); + if (r == 0) { + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_SERVER_ID, + v_inaddr_s.s_addr); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BROADCAST, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &v_inaddr)) { + nm_dhcp_option_add_option_in_addr(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_BROADCAST, + v_inaddr); + } + + lease_parse_routes(lease, ip4_config, options, route_table, route_metric, &sbuf); + + lease_parse_address_list(lease, + ip4_config, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, + options, + &sbuf); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + gs_free const char **domains = NULL; + + nm_str_buf_reset(&sbuf); + nm_str_buf_append_len0(&sbuf, (const char *) l_data, l_data_len); + + /* Multiple domains sometimes stuffed into option 15 "Domain Name". */ + domains = nm_utils_strsplit_set(nm_str_buf_get_str(&sbuf), " "); + + nm_str_buf_reset(&sbuf); + if (domains) { + gsize i; + + for (i = 0; domains[i]; i++) { + gs_free char *s = NULL; + + s = nm_dhcp_lease_data_parse_domain_validate(domains[i]); + if (!s) + continue; + + nm_str_buf_append_required_delimiter(&sbuf, ' '); + nm_str_buf_append(&sbuf, s); + nm_ip4_config_add_domain(ip4_config, s); + } + } + + if (sbuf.len > 0) { + nm_dhcp_option_add_option(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, + nm_str_buf_get_str(&sbuf)); + } + } + + lease_parse_search_domains(lease, ip4_config, options); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_mtu(l_data, l_data_len, &v_u16)) { + nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, v_u16); + nm_ip4_config_set_mtu(ip4_config, v_u16, NM_IP_CONFIG_SOURCE_DHCP); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC, &l_data, &l_data_len); + v_bool = + (r == 0) && memmem(l_data, l_data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")); + nm_ip4_config_set_metered(ip4_config, v_bool); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_HOST_NAME, &l_data, &l_data_len); + if (r == 0) { + gs_free char *s = NULL; + + if (nm_dhcp_lease_data_parse_domain(l_data, l_data_len, &s)) { + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s); + } + } + + lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf); + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROOT_PATH, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + /* https://tools.ietf.org/html/rfc2132#section-3.19 + * + * The path is formatted as a character string consisting of + * characters from the NVT ASCII character set. + * + * We still accept any character set and backslash escape it! */ + if (l_data_len == 0) { + /* "Its minimum length is 1." */ + } else { + nm_dhcp_option_add_option_utf8safe_escape(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_ROOT_PATH, + l_data, + l_data_len); + } + } + + r = _client_lease_query(lease, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, + &l_data, + &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + /* https://tools.ietf.org/html/draft-ietf-wrec-wpad-01#section-4.4.1 + * + * We reject NUL characters inside the string (except trailing NULs). + * Otherwise, we allow any encoding and backslash-escape the result to + * UTF-8. */ + nm_dhcp_option_add_option_utf8safe_escape(options, + AF_INET, + NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY, + l_data, + l_data_len); + } + + r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, &l_data, &l_data_len); + if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) { + gs_free char *to_free = NULL; + + /* https://tools.ietf.org/html/rfc2132#section-8.1 */ + + v_str = nm_utils_buf_utf8safe_escape((char *) l_data, l_data_len, 0, &to_free); + + nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, v_str); + nm_ip4_config_set_nis_domain(ip4_config, v_str); + } + + lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf); + + lease_parse_address_list(lease, + ip4_config, + NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, + options, + &sbuf); + + 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_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE); + char addr_str[NM_UTILS_INET_ADDRSTRLEN]; + gs_free_error GError *error = NULL; + + nm_assert(lease); + nm_assert(lease_file); + + n_dhcp4_client_lease_get_yiaddr(lease, &a_address); + if (a_address.s_addr == INADDR_ANY) + return; + + nm_str_buf_append(&sbuf, "# This is private data. Do not parse.\n"); + nm_str_buf_append_printf(&sbuf, + "ADDRESS=%s\n", + _nm_utils_inet4_ntop(a_address.s_addr, addr_str)); + + if (!g_file_set_contents(lease_file, nm_str_buf_get_str_unsafe(&sbuf), sbuf.len, &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, AF_INET); + 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 void +dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event) +{ + NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self); + struct in_addr server_id; + char addr_str[INET_ADDRSTRLEN]; + int r; + + _LOGT("client event %d", event->event); + + switch (event->event) { + case N_DHCP4_CLIENT_EVENT_OFFER: + r = n_dhcp4_client_lease_get_server_identifier(event->offer.lease, &server_id); + if (r) { + _LOGW("selecting lease failed: %d", r); + return; + } + + if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) { + _LOGD("server-id %s is in the reject-list, ignoring", + nm_utils_inet_ntop(AF_INET, &server_id, addr_str)); + return; + } + + r = n_dhcp4_client_lease_select(event->offer.lease); + if (r) { + _LOGW("selecting lease failed: %d", r); + return; + } + 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; + } +} + +static gboolean +dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data) +{ + NMDhcpNettools * self = user_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 being 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; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options); 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, +}; |