/* 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 #include #include #include #include "nm-glib-aux/nm-dedup-multi.h" #include "nm-dhcp-utils.h" #include "nm-ip4-config.h" #include "nm-utils.h" #include "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_utils_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; 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_addr, 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_addr || nm_utils_hwaddr_valid(anycast_addr, 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_utils_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_addr) { g_string_append_printf(new_contents, "interface \"%s\" {\n" " initial-interval 1; \n" " anycast-mac ethernet %s;\n" "}\n", interface, anycast_addr); } return g_string_free(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_utils_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_utils_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; }