// SPDX-License-Identifier: LGPL-2.1+ /* * Copyright (C) 2014-2019 Red Hat, Inc. */ #include "nm-default.h" #include #include #include #include #include #include #include #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; GIOChannel *channel; guint event_id; 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) /*****************************************************************************/ #define DHCP_MAX_FQDN_LENGTH 255 enum { DHCP_FQDN_FLAG_S = (1 << 0), DHCP_FQDN_FLAG_O = (1 << 1), DHCP_FQDN_FLAG_E = (1 << 2), DHCP_FQDN_FLAG_N = (1 << 3), }; 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; if (classless) { if (!lease_option_consume (&plen, sizeof (plen), &data, &n_data)) return FALSE; if (plen > 32) return FALSE; if (!lease_option_consume (&dest, plen / 8, &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, '.'); 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]; const gint64 ts = nm_utils_get_monotonic_timestamp_ns (); const gint64 ts_clock_boottime = nm_utils_monotonic_timestamp_as_boottime (ts, 1); struct in_addr a_address; struct in_addr a_netmask; guint32 a_plen; guint64 nettools_lifetime; gint64 a_lifetime; gint64 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() never fails */ n_dhcp4_client_lease_get_lifetime (lease, &nettools_lifetime); /* FIXME: n_dhcp4_client_lease_get_lifetime() returns the time in nsec of CLOCK_BOOTTIME. * We want to retrieve the original lifetime value in seconds, so we approximate it in a_lifetime. * Use a nettools API to retrieve the original value as passed by the server. */ if (nettools_lifetime == G_MAXUINT64) { a_lifetime = NM_PLATFORM_LIFETIME_PERMANENT; a_expiry = NM_PLATFORM_LIFETIME_PERMANENT; } else { gint64 ts_time = time (NULL); a_lifetime = ((gint64) nettools_lifetime - ts_clock_boottime + (NM_UTILS_NS_PER_SECOND / 2)) / NM_UTILS_NS_PER_SECOND; /* A lease time of 0 is allowed on some dhcp servers, so, let's accept it. */ if (a_lifetime < 0) a_lifetime = 0; else if (a_lifetime > NM_PLATFORM_LIFETIME_PERMANENT) a_lifetime = NM_PLATFORM_LIFETIME_PERMANENT - 1; if (ts_time > NM_PLATFORM_LIFETIME_PERMANENT - a_lifetime) a_expiry = NM_PLATFORM_LIFETIME_PERMANENT - 1; else a_expiry = ts_time + a_lifetime; } 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); nm_dhcp_option_add_option_u64 (options, _nm_dhcp_option_dhcp4_options, NM_DHCP_OPTION_DHCP4_NM_EXPIRY, (guint64) a_expiry); 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 = ts / NM_UTILS_NS_PER_SECOND, .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); } static void lease_parse_search_domains (NDhcp4ClientLease *lease, NMIP4Config *ip4_config, GHashTable *options) { nm_auto_free_gstring GString *str = NULL; uint8_t *data, *cache; size_t n_data, n_cache = 0; int r; r = n_dhcp4_client_lease_query (lease, NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST, &data, &n_data); if (r) return; cache = data; nm_gstring_prepare (&str); 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; g_string_append (nm_gstring_add_space_delimiter (str), domain->str); nm_ip4_config_add_search (ip4_config, domain->str); } 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 (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]; 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)); g_file_set_contents (lease_file, new_contents->str, -1, NULL); } static void bound4_handle (NMDhcpNettools *self, NDhcp4ClientLease *lease) { 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"); 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 (lease, priv->lease_file); nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), 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_EXPIRED: nm_dhcp_client_set_state (NM_DHCP_CLIENT (self), NM_DHCP_STATE_EXPIRE, NULL, NULL); break; case N_DHCP4_CLIENT_EVENT_RETRACTED: 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); break; case N_DHCP4_CLIENT_EVENT_EXTENDED: bound4_handle (self, event->extended.lease); break; case N_DHCP4_CLIENT_EVENT_DOWN: /* ignore down events, they are purely informational */ break; default: _LOGW ("unhandled DHCP event %d", event->event); break; } return TRUE; } static gboolean dhcp4_event_cb (GIOChannel *source, 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) return G_SOURCE_CONTINUE; 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) { nm_utils_error_set_errno (error, r, "failed to create client-config: %s"); 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, client_id_len); if (r) { nm_utils_error_set_errno (error, r, "failed to set client-id: %s"); return FALSE; } r = n_dhcp4_client_new (&client, config); if (r) { nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN, "failed to create client: error %d", r); return FALSE; } priv->client = client; client = NULL; n_dhcp4_client_get_fd (priv->client, &fd); priv->channel = g_io_channel_unix_new (fd); priv->event_id = g_io_add_watch (priv->channel, G_IO_IN, dhcp4_event_cb, self); 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) { nm_utils_error_set_errno (error, r, "failed to accept lease: %s"); 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) { nm_utils_error_set_errno (error, r, "failed to decline lease: %s"); return FALSE; } priv->lease = n_dhcp4_client_lease_unref (priv->lease); return TRUE; } 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; 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) { nm_utils_error_set_errno (error, r, "failed to create dhcp-client-probe-config: %s"); 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); /* 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); } } hostname = nm_dhcp_client_get_hostname (client); if (hostname) { if (nm_dhcp_client_get_use_fqdn (client)) { uint8_t buffer[3 + DHCP_MAX_FQDN_LENGTH]; buffer[0] = DHCP_FQDN_FLAG_S | /* Request server to perform A RR DNS updates */ DHCP_FQDN_FLAG_E; /* Canonical wire format */ buffer[1] = 0; /* RCODE1 (deprecated) */ buffer[2] = 0; /* RCODE2 (deprecated) */ r = nm_sd_dns_name_to_wire_format (hostname, buffer + 3, sizeof (buffer) - 3, FALSE); if (r < 0) { nm_utils_error_set_errno (error, r, "failed to convert DHCP FQDN: %s"); return FALSE; } r = n_dhcp4_client_probe_config_append_option (config, NM_DHCP_OPTION_DHCP4_CLIENT_FQDN, buffer, 3 + r); if (r) { nm_utils_error_set_errno (error, r, "failed to set DHCP FQDN: %s"); return FALSE; } } else { r = n_dhcp4_client_probe_config_append_option (config, NM_DHCP_OPTION_DHCP4_HOST_NAME, hostname, strlen (hostname)); if (r) { nm_utils_error_set_errno (error, r, "failed to set DHCP hostname: %s"); 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) { nm_utils_error_set_errno (error, r, "failed to start DHCP client: %s"); 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 ((NMDhcpNettools *) object); nm_clear_pointer (&priv->lease_file, g_free); nm_clear_pointer (&priv->channel, g_io_channel_unref); nm_clear_g_source (&priv->event_id); 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, };