NetworkManager/src/core/dhcp/nm-dhcp-dhclient-utils.c
Thomas Haller 58287cbcc0 core: rework IP configuration in NetworkManager using layer 3 configuration
Completely rework IP configuration in the daemon. Use NML3Cfg as layer 3
manager for the IP configuration of an interface. Use NML3ConfigData as
pieces of configuration that the various components collect and
configure. NMDevice is managing most of the IP configuration at a higher
level, that is, it starts DHCP and other IP methods. Rework the state
handling there.

This is a huge rework of how NetworkManager daemon handles IP
configuration. Some fallout is to be expected.

It appears the patch deletes many lines of code. That is not accurate, because
you also have to count the files `src/core/nm-l3*`, which were unused previously.

Co-authored-by: Beniamino Galvani <bgalvani@redhat.com>
2021-11-18 16:21:29 +01:00

714 lines
23 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2011 Red Hat, Inc.
*/
#include "src/core/nm-default-daemon.h"
#include "nm-dhcp-dhclient-utils.h"
#include <ctype.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <linux/if_ether.h>
#include "libnm-glib-aux/nm-dedup-multi.h"
#include "nm-dhcp-utils.h"
#include "nm-utils.h"
#include "libnm-platform/nm-platform.h"
#include "NetworkManagerUtils.h"
#define TIMEOUT_TAG "timeout "
#define RETRY_TAG "retry "
#define CLIENTID_TAG "send dhcp-client-identifier"
#define HOSTNAME4_TAG "send host-name"
#define HOSTNAME4_FORMAT HOSTNAME4_TAG " \"%s\"; # added by NetworkManager"
#define FQDN_TAG_PREFIX "send fqdn."
#define FQDN_TAG FQDN_TAG_PREFIX "fqdn"
#define FQDN_FORMAT FQDN_TAG " \"%s\"; # added by NetworkManager"
#define ALSOREQ_TAG "also request "
#define REQ_TAG "request "
#define MUDURLv4_DEF "option mudurl code 161 = text;\n"
#define MUDURLv4_FMT "send mudurl \"%s\";\n"
#define MUDURLv6_DEF "option dhcp6.mudurl code 112 = text;\n"
#define MUDURLv6_FMT "send dhcp6.mudurl \"%s\";\n"
static void
add_request(GPtrArray *array, const char *item)
{
guint i;
for (i = 0; i < array->len; i++) {
if (nm_streq(array->pdata[i], item))
return;
}
g_ptr_array_add(array, g_strdup(item));
}
static gboolean
grab_request_options(GPtrArray *store, const char *line)
{
gs_free const char **line_v = NULL;
gsize i;
/* Grab each 'request' or 'also request' option and save for later */
line_v = nm_strsplit_set(line, "\t ,");
for (i = 0; line_v && line_v[i]; i++) {
const char *ss = nm_str_skip_leading_spaces(line_v[i]);
gsize l;
gboolean end = FALSE;
if (!ss[0])
continue;
if (ss[0] == ';') {
/* all done */
return TRUE;
}
if (!g_ascii_isalnum(ss[0]))
continue;
l = strlen(ss);
while (l > 0 && g_ascii_isspace(ss[l - 1])) {
((char *) ss)[l - 1] = '\0';
l--;
}
if (l > 0 && ss[l - 1] == ';') {
/* Remove the EOL marker */
((char *) ss)[l - 1] = '\0';
end = TRUE;
}
if (ss[0])
add_request(store, ss);
if (end)
return TRUE;
}
return FALSE;
}
static void
add_ip4_config(GString * str,
GBytes * client_id,
const char * hostname,
gboolean use_fqdn,
NMDhcpHostnameFlags hostname_flags)
{
if (client_id) {
const char *p;
gsize l;
guint i;
p = g_bytes_get_data(client_id, &l);
nm_assert(p);
/* Allow type 0 (non-hardware address) to be represented as a string
* as long as all the characters are printable.
*/
for (i = 1; (p[0] == 0) && i < l; i++) {
if (!g_ascii_isprint(p[i]) || p[i] == '\\' || p[i] == '"')
break;
}
g_string_append(str, CLIENTID_TAG " ");
if (i < l) {
/* Unprintable; convert to a hex string */
for (i = 0; i < l; i++) {
if (i > 0)
g_string_append_c(str, ':');
g_string_append_printf(str, "%02x", (guint8) p[i]);
}
} else {
/* Printable; just add to the line with type 0 */
g_string_append_c(str, '"');
g_string_append(str, "\\x00");
g_string_append_len(str, p + 1, l - 1);
g_string_append_c(str, '"');
}
g_string_append(str, "; # added by NetworkManager\n");
}
if (hostname) {
if (use_fqdn) {
g_string_append_printf(str, FQDN_FORMAT "\n", hostname);
g_string_append_printf(str,
FQDN_TAG_PREFIX "encoded %s;\n",
(hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) ? "on"
: "off");
g_string_append_printf(
str,
FQDN_TAG_PREFIX "server-update %s;\n",
(hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE) ? "on" : "off");
g_string_append_printf(str,
FQDN_TAG_PREFIX "no-client-update %s;\n",
(hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE) ? "on"
: "off");
} else
g_string_append_printf(str, HOSTNAME4_FORMAT "\n", hostname);
}
g_string_append_c(str, '\n');
/* Define options for classless static routes */
g_string_append(
str,
"option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n");
g_string_append(str,
"option ms-classless-static-routes code 249 = array of unsigned integer 8;\n");
/* Web Proxy Auto-Discovery option (bgo #368423) */
g_string_append(str, "option wpad code 252 = string;\n");
g_string_append_c(str, '\n');
}
static void
add_hostname6(GString *str, const char *hostname, NMDhcpHostnameFlags hostname_flags)
{
if (hostname) {
g_string_append_printf(str, FQDN_FORMAT "\n", hostname);
if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_SERV_UPDATE)
g_string_append(str, FQDN_TAG_PREFIX "server-update on;\n");
if (hostname_flags & NM_DHCP_HOSTNAME_FLAG_FQDN_NO_UPDATE)
g_string_append(str, FQDN_TAG_PREFIX "no-client-update on;\n");
g_string_append_c(str, '\n');
}
}
static void
add_mud_url_config(GString *str, const char *mud_url, int addr_family)
{
if (mud_url) {
if (addr_family == AF_INET) {
g_string_append(str, MUDURLv4_DEF);
g_string_append_printf(str, MUDURLv4_FMT, mud_url);
} else {
g_string_append(str, MUDURLv6_DEF);
g_string_append_printf(str, MUDURLv6_FMT, mud_url);
}
}
}
static GBytes *
read_client_id(const char *str)
{
gs_free char *s = NULL;
char * p;
int i = 0, j = 0;
nm_assert(!strncmp(str, CLIENTID_TAG, NM_STRLEN(CLIENTID_TAG)));
str += NM_STRLEN(CLIENTID_TAG);
if (!g_ascii_isspace(*str))
return NULL;
while (g_ascii_isspace(*str))
str++;
if (*str == '"') {
/* Parse string literal with escape sequences */
s = g_strdup(str + 1);
p = strrchr(s, '"');
if (p)
*p = '\0';
else
return NULL;
if (!s[0])
return NULL;
while (s[i]) {
if (s[i] == '\\' && s[i + 1] == 'x' && g_ascii_isxdigit(s[i + 2])
&& g_ascii_isxdigit(s[i + 3])) {
s[j++] = (g_ascii_xdigit_value(s[i + 2]) << 4) + g_ascii_xdigit_value(s[i + 3]);
i += 4;
continue;
}
if (s[i] == '\\' && s[i + 1] >= '0' && s[i + 1] <= '7' && s[1 + 2] >= '0'
&& s[i + 2] <= '7' && s[1 + 3] >= '0' && s[i + 3] <= '7') {
s[j++] = ((s[i + 1] - '0') << 6) + ((s[i + 2] - '0') << 3) + (s[i + 3] - '0');
i += 4;
continue;
}
s[j++] = s[i++];
}
return g_bytes_new_take(g_steal_pointer(&s), j);
}
/* Otherwise, try to read a hexadecimal sequence */
s = g_strdup(str);
g_strchomp(s);
if (s[strlen(s) - 1] == ';')
s[strlen(s) - 1] = '\0';
return nm_utils_hexstr2bin(s);
}
static gboolean
read_interface(const char *line, char *interface, guint size)
{
gs_free char *dup = g_strdup(line + NM_STRLEN("interface"));
char * ptr = dup, *end;
while (g_ascii_isspace(*ptr))
ptr++;
if (*ptr == '"') {
ptr++;
end = strchr(ptr, '"');
if (!end)
return FALSE;
*end = '\0';
} else {
end = strchr(ptr, ' ');
if (!end)
end = strchr(ptr, '{');
if (!end)
return FALSE;
*end = '\0';
}
if (ptr[0] == '\0' || strlen(ptr) + 1 > size)
return FALSE;
g_snprintf(interface, size, "%s", ptr);
return TRUE;
}
char *
nm_dhcp_dhclient_create_config(const char * interface,
int addr_family,
GBytes * client_id,
const char * anycast_address,
const char * hostname,
guint32 timeout,
gboolean use_fqdn,
NMDhcpHostnameFlags hostname_flags,
const char * mud_url,
const char *const * reject_servers,
const char * orig_path,
const char * orig_contents,
GBytes ** out_new_client_id)
{
nm_auto_free_gstring GString *new_contents = NULL;
gs_unref_ptrarray GPtrArray *fqdn_opts = NULL;
gs_unref_ptrarray GPtrArray *reqs = NULL;
gboolean reset_reqlist = FALSE;
int i;
g_return_val_if_fail(!anycast_address || nm_utils_hwaddr_valid(anycast_address, ETH_ALEN),
NULL);
g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), NULL);
g_return_val_if_fail(!reject_servers || addr_family == AF_INET, NULL);
nm_assert(!out_new_client_id || !*out_new_client_id);
new_contents = g_string_new(_("# Created by NetworkManager\n"));
reqs = g_ptr_array_new_full(5, g_free);
if (orig_contents) {
gs_free const char **lines = NULL;
gsize line_i;
nm_auto_free_gstring GString *blocks_stack = NULL;
guint blocks_skip = 0;
gboolean in_alsoreq = FALSE;
gboolean in_req = FALSE;
char intf[IFNAMSIZ];
blocks_stack = g_string_new(NULL);
g_string_append_printf(new_contents, _("# Merged from %s\n\n"), orig_path);
intf[0] = '\0';
lines = nm_strsplit_set(orig_contents, "\n\r");
for (line_i = 0; lines && lines[line_i]; line_i++) {
const char *line = nm_str_skip_leading_spaces(lines[line_i]);
const char *p;
if (line[0] == '\0')
continue;
g_strchomp((char *) line);
p = line;
if (in_req) {
/* pass */
} else if (strchr(p, '{')) {
if (NM_STR_HAS_PREFIX(p, "lease") || NM_STR_HAS_PREFIX(p, "alias")
|| NM_STR_HAS_PREFIX(p, "interface") || NM_STR_HAS_PREFIX(p, "pseudo")) {
/* skip over these blocks, except 'interface' when it
* matches the current interface */
blocks_skip++;
g_string_append_c(blocks_stack, 'b');
if (!intf[0] && NM_STR_HAS_PREFIX(p, "interface")) {
if (read_interface(p, intf, sizeof(intf)))
continue;
}
} else {
/* allow other blocks (conditionals) */
if (!strchr(p, '}')) /* '} else {' */
g_string_append_c(blocks_stack, 'c');
}
} else if (strchr(p, '}')) {
if (blocks_stack->len > 0) {
if (blocks_stack->str[blocks_stack->len - 1] == 'b') {
g_string_truncate(blocks_stack, blocks_stack->len - 1);
nm_assert(blocks_skip > 0);
blocks_skip--;
intf[0] = '\0';
continue;
}
g_string_truncate(blocks_stack, blocks_stack->len - 1);
}
}
if (blocks_skip > 0 && !intf[0])
continue;
if (intf[0] && !nm_streq(intf, interface))
continue;
/* Some timing parameters in dhclient should not be imported (timeout, retry).
* The retry parameter will be simply not used as we will exit on first failure.
* The timeout one instead may affect NetworkManager behavior: if the timeout
* elapses before dhcp-timeout dhclient will report failure and cause NM to
* fail the dhcp process before dhcp-timeout. So, always skip importing timeout
* as we will need to add one greater than dhcp-timeout.
*/
if (!strncmp(p, TIMEOUT_TAG, strlen(TIMEOUT_TAG))
|| !strncmp(p, RETRY_TAG, strlen(RETRY_TAG)))
continue;
if (!strncmp(p, CLIENTID_TAG, strlen(CLIENTID_TAG))) {
/* Override config file "dhcp-client-id" and use one from the connection */
if (client_id)
continue;
/* Otherwise, capture and return the existing client id */
if (out_new_client_id)
nm_clear_pointer(out_new_client_id, g_bytes_unref);
NM_SET_OUT(out_new_client_id, read_client_id(p));
}
/* Override config file hostname and use one from the connection */
if (hostname) {
if (strncmp(p, HOSTNAME4_TAG, strlen(HOSTNAME4_TAG)) == 0)
continue;
if (strncmp(p, FQDN_TAG, strlen(FQDN_TAG)) == 0)
continue;
}
/* To let user's FQDN options (except "fqdn.fqdn") override the
* default ones set by NM, add them later
*/
if (!strncmp(p, FQDN_TAG_PREFIX, NM_STRLEN(FQDN_TAG_PREFIX))) {
if (!fqdn_opts)
fqdn_opts = g_ptr_array_new_full(5, g_free);
g_ptr_array_add(fqdn_opts, g_strdup(p + NM_STRLEN(FQDN_TAG_PREFIX)));
continue;
}
/* Ignore 'script' since we pass our own */
if (g_str_has_prefix(p, "script "))
continue;
/* Check for "request" */
if (!strncmp(p, REQ_TAG, strlen(REQ_TAG))) {
in_req = TRUE;
p += strlen(REQ_TAG);
g_ptr_array_set_size(reqs, 0);
reset_reqlist = TRUE;
}
/* Save all request options for later use */
if (in_req) {
in_req = !grab_request_options(reqs, p);
continue;
}
/* Check for "also require" */
if (!strncmp(p, ALSOREQ_TAG, strlen(ALSOREQ_TAG))) {
in_alsoreq = TRUE;
p += strlen(ALSOREQ_TAG);
}
if (in_alsoreq) {
in_alsoreq = !grab_request_options(reqs, p);
continue;
}
/* Existing configuration line is OK, add it to new configuration */
g_string_append(new_contents, line);
g_string_append_c(new_contents, '\n');
}
} else
g_string_append_c(new_contents, '\n');
/* ensure dhclient timeout is greater than dhcp-timeout: as dhclient timeout default value is
* 60 seconds, we need this only if dhcp-timeout is greater than 60.
*/
if (timeout >= 60) {
timeout = timeout < G_MAXINT32 ? timeout + 1 : G_MAXINT32;
g_string_append_printf(new_contents, "timeout %u;\n", timeout);
}
add_mud_url_config(new_contents, mud_url, addr_family);
if (reject_servers && reject_servers[0]) {
g_string_append(new_contents, "reject ");
for (i = 0; reject_servers[i]; i++) {
if (i != 0)
g_string_append(new_contents, ", ");
g_string_append(new_contents, reject_servers[i]);
}
g_string_append(new_contents, ";\n");
}
if (addr_family == AF_INET) {
add_ip4_config(new_contents, client_id, hostname, use_fqdn, hostname_flags);
add_request(reqs, "rfc3442-classless-static-routes");
add_request(reqs, "ms-classless-static-routes");
add_request(reqs, "static-routes");
add_request(reqs, "wpad");
add_request(reqs, "ntp-servers");
add_request(reqs, "root-path");
} else {
add_hostname6(new_contents, hostname, hostname_flags);
add_request(reqs, "dhcp6.name-servers");
add_request(reqs, "dhcp6.domain-search");
/* FIXME: internal client does not support requesting client-id option. Does this even work? */
add_request(reqs, "dhcp6.client-id");
}
if (reset_reqlist)
g_string_append(new_contents, "request; # override dhclient defaults\n");
/* And add it to the dhclient configuration */
for (i = 0; i < reqs->len; i++)
g_string_append_printf(new_contents, "also request %s;\n", (char *) reqs->pdata[i]);
if (fqdn_opts) {
for (i = 0; i < fqdn_opts->len; i++) {
const char *t = fqdn_opts->pdata[i];
if (i == 0)
g_string_append_printf(new_contents, "\n# FQDN options from %s\n", orig_path);
g_string_append_printf(new_contents, FQDN_TAG_PREFIX "%s\n", t);
}
}
g_string_append_c(new_contents, '\n');
if (anycast_address) {
g_string_append_printf(new_contents,
"interface \"%s\" {\n"
" initial-interval 1; \n"
" anycast-mac ethernet %s;\n"
"}\n",
interface,
anycast_address);
}
return g_string_free(g_steal_pointer(&new_contents), FALSE);
}
/* Roughly follow what dhclient's quotify_buf() and pretty_escape() functions do */
char *
nm_dhcp_dhclient_escape_duid(GBytes *duid)
{
char * escaped;
const guint8 *s, *s0;
gsize len;
char * d;
g_return_val_if_fail(duid, NULL);
s0 = g_bytes_get_data(duid, &len);
s = s0;
d = escaped = g_malloc((len * 4) + 1);
while (s < (s0 + len)) {
if (!g_ascii_isprint(*s)) {
*d++ = '\\';
*d++ = '0' + ((*s >> 6) & 0x7);
*d++ = '0' + ((*s >> 3) & 0x7);
*d++ = '0' + (*s++ & 0x7);
} else if (*s == '"' || *s == '\'' || *s == '$' || *s == '`' || *s == '\\' || *s == '|'
|| *s == '&') {
*d++ = '\\';
*d++ = *s++;
} else
*d++ = *s++;
}
*d++ = '\0';
return escaped;
}
static gboolean
isoctal(const guint8 *p)
{
return (p[0] >= '0' && p[0] <= '3' && p[1] >= '0' && p[1] <= '7' && p[2] >= '0' && p[2] <= '7');
}
GBytes *
nm_dhcp_dhclient_unescape_duid(const char *duid)
{
GByteArray * unescaped;
const guint8 *p = (const guint8 *) duid;
guint i, len;
guint8 octal;
/* FIXME: it's wrong to have an "unescape-duid" function. dhclient
* defines a file format with escaping. So we need a general unescape
* function that can handle dhclient syntax. */
len = strlen(duid);
unescaped = g_byte_array_sized_new(len);
for (i = 0; i < len; i++) {
if (p[i] == '\\') {
i++;
if (isdigit(p[i])) {
/* Octal escape sequence */
if (i + 2 >= len || !isoctal(p + i))
goto error;
octal = ((p[i] - '0') << 6) + ((p[i + 1] - '0') << 3) + (p[i + 2] - '0');
g_byte_array_append(unescaped, &octal, 1);
i += 2;
} else {
/* FIXME: don't warn on untrusted data. Either signal an error, or accept
* it silently. */
/* One of ", ', $, `, \, |, or & */
g_warn_if_fail(p[i] == '"' || p[i] == '\'' || p[i] == '$' || p[i] == '`'
|| p[i] == '\\' || p[i] == '|' || p[i] == '&');
g_byte_array_append(unescaped, &p[i], 1);
}
} else
g_byte_array_append(unescaped, &p[i], 1);
}
return g_byte_array_free_to_bytes(unescaped);
error:
g_byte_array_free(unescaped, TRUE);
return NULL;
}
#define DUID_PREFIX "default-duid \""
/* Beware: @error may be unset even if the function returns %NULL. */
GBytes *
nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
{
gs_free char * contents = NULL;
gs_free const char **contents_v = NULL;
gsize i;
if (!g_file_test(leasefile, G_FILE_TEST_EXISTS))
return NULL;
if (!g_file_get_contents(leasefile, &contents, NULL, error))
return NULL;
contents_v = nm_strsplit_set(contents, "\n\r");
for (i = 0; contents_v && contents_v[i]; i++) {
const char *p = nm_str_skip_leading_spaces(contents_v[i]);
GBytes * duid;
if (!NM_STR_HAS_PREFIX(p, DUID_PREFIX))
continue;
p += NM_STRLEN(DUID_PREFIX);
g_strchomp((char *) p);
if (!NM_STR_HAS_SUFFIX(p, "\";"))
continue;
((char *) p)[strlen(p) - 2] = '\0';
duid = nm_dhcp_dhclient_unescape_duid(p);
if (duid)
return duid;
}
return NULL;
}
gboolean
nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error)
{
gs_free char * escaped_duid = NULL;
gs_free const char **lines = NULL;
nm_auto_free_gstring GString *s = NULL;
const char *const * iter;
gsize len = 0;
g_return_val_if_fail(leasefile != NULL, FALSE);
if (!duid) {
nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing duid");
g_return_val_if_reached(FALSE);
}
escaped_duid = nm_dhcp_dhclient_escape_duid(duid);
nm_assert(escaped_duid);
if (g_file_test(leasefile, G_FILE_TEST_EXISTS)) {
gs_free char *contents = NULL;
if (!g_file_get_contents(leasefile, &contents, &len, error)) {
g_prefix_error(error, "failed to read lease file %s: ", leasefile);
return FALSE;
}
lines = nm_strsplit_set_with_empty(contents, "\n\r");
}
s = g_string_sized_new(len + 50);
g_string_append_printf(s, DUID_PREFIX "%s\";\n", escaped_duid);
/* Preserve existing leasefile contents */
if (lines) {
for (iter = lines; *iter; iter++) {
const char *str = *iter;
const char *l;
/* If we find an uncommented DUID in the file, check if
* equal to the one we are going to write: if so, no need
* to update the lease file, otherwise skip the old DUID.
*/
l = nm_str_skip_leading_spaces(str);
if (g_str_has_prefix(l, DUID_PREFIX)) {
gs_strfreev char **split = NULL;
split = g_strsplit(l, "\"", -1);
if (split[0] && nm_streq0(split[1], escaped_duid))
return TRUE;
continue;
}
if (str)
g_string_append(s, str);
/* avoid to add an extra '\n' at the end of file */
if ((iter[1]) != NULL)
g_string_append_c(s, '\n');
}
}
if (!g_file_set_contents(leasefile, s->str, -1, error)) {
g_prefix_error(error, "failed to set DUID in lease file %s: ", leasefile);
return FALSE;
}
return TRUE;
}