From 4dee109b8ddc26037b30b68645227ef0e7c13e4c Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Tue, 17 Dec 2024 10:17:09 +0100 Subject: [PATCH] libnm-core: add new functions for DNS parsing Introduce new functions to parse and normalize name servers. Their name contains "dns_uri" because they also support a URI-like syntax as: "dns+tls://192.0.2.0:553#example.org". --- .../nm-libnm-core-utils.c | 293 ++++++++++++++++++ .../nm-libnm-core-utils.h | 23 ++ src/libnm-core-impl/tests/test-general.c | 181 +++++++++++ 3 files changed, 497 insertions(+) diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.c b/src/libnm-core-aux-intern/nm-libnm-core-utils.c index 3d619a3c1b..e13a8381ff 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.c +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.c @@ -810,6 +810,299 @@ nm_utils_dnsname_normalize(int addr_family, const char *dns, char **out_free) /*****************************************************************************/ +/* + * nm_dns_uri_parse: + * @addr_family: the address family, or AF_UNSPEC to autodetect it + * @str: the name server URI string to parse + * @dns: the name server descriptor to fill, or %NULL + * + * Parses the given name server URI string. Each name server is represented + * by the following grammar: + * + * NAMESERVER := { PLAIN | TLS_URI | UDP_URI } + * PLAIN := { ipv4address | ipv6address } [ '#' SERVERNAME ] + * TLS_URI := 'dns+tls://' URI_ADDRESS [ ':' PORT ] [ '#' SERVERNAME ] + * UDP_URI := 'dns+udp://' URI_ADDRESS [ ':' PORT ] + * URI_ADDRESS := { ipv4address | '[' ipv6address [ '%' ifname ] ']' } + * + * Examples: + * + * 192.0.2.0 + * 192.0.2.0#example.com + * 2001:db8::1 + * dns+tls://192.0.2.0 + * dns+tls://[2001:db8::1] + * dns+tls://192.0.2.0:53#example.com + * dns+udp://[fe80::1%enp1s0] + * + * Note that on return, the lifetime of the members in the @dns struct is + * the same as the input string @str. + * + * Returns: %TRUE on success, %FALSE on failure + */ +gboolean +nm_dns_uri_parse(int addr_family, const char *str, NMDnsServer *dns) +{ + NMDnsServer dns_stack; + gs_free char *addr_port_heap = NULL; + gs_free char *addr_heap = NULL; + const char *addr_port; + const char *addr; + const char *name; + const char *port; + + nm_assert_addr_family_or_unspec(addr_family); + + if (!dns) + dns = &dns_stack; + + if (!str) + return FALSE; + + *dns = (NMDnsServer) { + .port = -1, + }; + + if (NM_STR_HAS_PREFIX(str, "dns+tls://")) { + dns->scheme = NM_DNS_URI_SCHEME_TLS; + str += NM_STRLEN("dns+tls://"); + } else if (NM_STR_HAS_PREFIX(str, "dns+udp://")) { + dns->scheme = NM_DNS_URI_SCHEME_UDP; + str += NM_STRLEN("dns+udp://"); + } else { + name = strchr(str, '#'); + if (name) { + str = nm_strndup_a(200, str, name - str, &addr_heap); + name++; + } + + if (name && name[0] == '\0') { + /* empty DoT server name is not allowed */ + return FALSE; + } + + if (!nm_inet_parse_bin(addr_family, str, &dns->addr_family, &dns->addr)) + return FALSE; + + dns->servername = name; + dns->scheme = NM_DNS_URI_SCHEME_NONE; + + return TRUE; + } + + addr_port = str; + name = strrchr(addr_port, '#'); + if (name) { + addr_port = nm_strndup_a(100, addr_port, name - addr_port, &addr_port_heap); + name++; + if (*name == '\0') { + /* empty DoT server name not allowed */ + return FALSE; + } + dns->servername = name; + } + + if (addr_family != AF_INET && *addr_port == '[') { + const char *end; + char *perc; + + addr_family = AF_INET6; + addr_port++; + end = strchr(addr_port, ']'); + if (!end) + return FALSE; + addr = nm_strndup_a(100, addr_port, end - addr_port, &addr_heap); + + /* IPv6 link-local scope-id */ + perc = strchr(addr, '%'); + if (perc) { + *perc = '\0'; + if (g_strlcpy(dns->interface, perc + 1, sizeof(dns->interface)) + >= sizeof(dns->interface)) + return FALSE; + } + + /* port */ + end++; + if (*end == ':') { + end++; + dns->port = _nm_utils_ascii_str_to_int64(end, 10, 0, 65535, G_MAXINT32); + if (dns->port == G_MAXINT32) + return FALSE; + } + } else if (addr_family != AF_INET6) { + /* square brackets are mandatory for IPv6, so it must be IPv4 */ + + addr_family = AF_INET; + addr = addr_port; + + /* port */ + port = strchr(addr_port, ':'); + if (port) { + addr = nm_strndup_a(100, addr_port, port - addr_port, &addr_heap); + port++; + dns->port = _nm_utils_ascii_str_to_int64(port, 10, 0, 65535, G_MAXINT32); + if (dns->port == G_MAXINT32) + return FALSE; + } + } else { + return FALSE; + } + + if (!nm_inet_parse_bin(addr_family, addr, &dns->addr_family, &dns->addr)) + return FALSE; + + if (dns->scheme != NM_DNS_URI_SCHEME_TLS && dns->servername) + return FALSE; + + /* For now, allow the interface only for IPv6 link-local addresses */ + if (dns->interface[0] + && (dns->addr_family != AF_INET6 || !IN6_IS_ADDR_LINKLOCAL(&dns->addr.addr6))) + return FALSE; + + return TRUE; +} + +/* @nm_dns_uri_parse_plain: + * @addr_family: the address family, or AF_UNSPEC to autodetect it + * @str: the name server URI string + * @out_addrstr: the buffer to fill with the address string on return, + * or %NULL. Must be of size at least NM_INET_ADDRSTRLEN. + * @out_addr: the %NMIPAddr struct to fill on return, or %NULL + * + * Returns whether the string contains a "plain" (DNS over UDP on port 53) + * name server. In such case, it fills the arguments with the address + * of the name server. + * + * Returns: %TRUE on success, %FALSE if the string can't be parsed or + * if it's not a plain name server. + */ +gboolean +nm_dns_uri_parse_plain(int addr_family, const char *str, char *out_addrstr, NMIPAddr *out_addr) +{ + NMDnsServer dns; + + if (!nm_dns_uri_parse(addr_family, str, &dns)) + return FALSE; + + switch (dns.scheme) { + case NM_DNS_URI_SCHEME_TLS: + return FALSE; + case NM_DNS_URI_SCHEME_NONE: + NM_SET_OUT(out_addr, dns.addr); + if (out_addrstr) { + nm_inet_ntop(dns.addr_family, &dns.addr, out_addrstr); + } + return TRUE; + case NM_DNS_URI_SCHEME_UDP: + if (dns.port != -1 && dns.port != 53) + return FALSE; + if (dns.interface[0]) + return FALSE; + NM_SET_OUT(out_addr, dns.addr); + if (out_addrstr) { + nm_inet_ntop(dns.addr_family, &dns.addr, out_addrstr); + } + return TRUE; + case NM_DNS_URI_SCHEME_UNKNOWN: + default: + return FALSE; + } +} + +/* @nm_dns_uri_normalize: + * @addr_family: the address family, or AF_UNSPEC to autodetect it + * @str: the name server URI string + * @out_free: the newly-allocated string to set on return, or %NULL + * + * Returns the "normal" representation for the given name server URI. + * Note that a plain name server (DNS over UDP on port 53) is always + * represented in the "legacy" (non-URI) form. + * + * Returns: the normalized DNS URI + */ +const char * +nm_dns_uri_normalize(int addr_family, const char *str, char **out_free) +{ + NMDnsServer dns; + char addrstr[NM_INET_ADDRSTRLEN]; + char portstr[32]; + char *ret; + gsize len; + + nm_assert_addr_family_or_unspec(addr_family); + nm_assert(str); + nm_assert(out_free && !*out_free); + + if (!nm_dns_uri_parse(addr_family, str, &dns)) + return NULL; + + nm_inet_ntop(dns.addr_family, &dns.addr, addrstr); + + if (dns.port != -1) { + nm_assert(dns.port >= 0 && dns.port <= 65535); + g_snprintf(portstr, sizeof(portstr), "%d", dns.port); + } + + switch (dns.scheme) { + case NM_DNS_URI_SCHEME_NONE: + len = strlen(addrstr); + /* In the vast majority of cases, the name is in fact normalized. Check + * whether it is, and don't duplicate the string. */ + if (strncmp(str, addrstr, len) == 0) { + if (dns.servername) { + if (str[len] == '#' && nm_streq(&str[len + 1], dns.servername)) + return str; + } else { + if (str[len] == '\0') + return str; + } + } + + if (!dns.servername) + ret = g_strdup(addrstr); + else + ret = g_strconcat(addrstr, "#", dns.servername, NULL); + break; + case NM_DNS_URI_SCHEME_UDP: + if (dns.interface[0] || dns.port != -1) { + ret = g_strdup_printf("dns+udp://%s%s%s%s%s%s%s", + dns.addr_family == AF_INET6 ? "[" : "", + addrstr, + dns.interface[0] ? "%" : "", + dns.interface[0] ? dns.interface : "", + dns.addr_family == AF_INET6 ? "]" : "", + dns.port != -1 ? ":" : "", + dns.port != -1 ? portstr : ""); + break; + } + ret = g_strdup_printf("%s%s%s", addrstr, dns.servername ? "#" : "", dns.servername ?: ""); + break; + case NM_DNS_URI_SCHEME_TLS: + ret = g_strdup_printf("dns+tls://%s%s%s%s%s%s%s%s%s", + dns.addr_family == AF_INET6 ? "[" : "", + addrstr, + dns.interface[0] ? "%%" : "", + dns.interface[0] ? dns.interface : "", + dns.addr_family == AF_INET6 ? "]" : "", + dns.port != -1 ? ":" : "", + dns.port != -1 ? portstr : "", + dns.servername ? "#" : "", + dns.servername ?: ""); + break; + case NM_DNS_URI_SCHEME_UNKNOWN: + default: + nm_assert_not_reached(); + ret = NULL; + } + + *out_free = ret; + + return ret; +} + +/*****************************************************************************/ + /** * nm_setting_ovs_other_config_check_key: * @key: (nullable): the key to check diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.h b/src/libnm-core-aux-intern/nm-libnm-core-utils.h index b296ca898a..18c42bf787 100644 --- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h +++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h @@ -341,6 +341,29 @@ const char *nm_utils_dnsname_normalize(int addr_family, const char *dns, char ** /*****************************************************************************/ +typedef enum { + NM_DNS_URI_SCHEME_UNKNOWN, + NM_DNS_URI_SCHEME_NONE, + NM_DNS_URI_SCHEME_UDP, + NM_DNS_URI_SCHEME_TLS, +} NMDnsUriScheme; + +typedef struct { + NMIPAddr addr; + const char *servername; + char interface[NM_IFNAMSIZ]; + NMDnsUriScheme scheme; + int addr_family; + int port; +} NMDnsServer; + +gboolean nm_dns_uri_parse(int addr_family, const char *str, NMDnsServer *out_dns); +gboolean +nm_dns_uri_parse_plain(int addr_family, const char *str, char *out_addrstr, NMIPAddr *out_addr); +const char *nm_dns_uri_normalize(int addr_family, const char *str, char **out_free); + +/*****************************************************************************/ + gboolean nm_setting_ovs_other_config_check_key(const char *key, GError **error); gboolean nm_setting_ovs_other_config_check_val(const char *val, GError **error); diff --git a/src/libnm-core-impl/tests/test-general.c b/src/libnm-core-impl/tests/test-general.c index c08789a0d5..7e104cee4b 100644 --- a/src/libnm-core-impl/tests/test-general.c +++ b/src/libnm-core-impl/tests/test-general.c @@ -11571,6 +11571,184 @@ test_dnsname(void) /*****************************************************************************/ +static void +t_dns_0(const char *str) +{ + NMDnsServer server = {}; + gboolean ret; + + ret = nm_dns_uri_parse(AF_UNSPEC, str, &server); + + g_assert(!ret); +} + +static void +dns_uri_parse_ok(const char *str, + int addr_family, + NMDnsUriScheme scheme, + const char *addr, + int port, + const char *sname, + const char *ifname) +{ + NMDnsServer dns = {}; + char addrstr[NM_INET_ADDRSTRLEN]; + gboolean ret; + + for (int i = 0; i < 2; i++) { + gboolean af_unspec = i; + + ret = nm_dns_uri_parse(af_unspec ? AF_UNSPEC : addr_family, str, &dns); + g_assert(ret); + + g_assert_cmpint(addr_family, ==, dns.addr_family); + g_assert_cmpint(port, ==, dns.port); + g_assert_cmpstr(sname, ==, dns.servername); + g_assert_cmpstr(ifname ?: "", ==, dns.interface); + + nm_inet_ntop(dns.addr_family, &dns.addr, addrstr); + g_assert_cmpstr(addrstr, ==, addr); + + /* Parse with the wrong address family must fail */ + ret = nm_dns_uri_parse(addr_family == AF_INET ? AF_INET6 : AF_INET, str, &dns); + g_assert(!ret); + } +} + +#define t_dns_1(str, af, scheme, addr, port, sname, ifname) \ + dns_uri_parse_ok((str), \ + (AF_##af), \ + (NM_DNS_URI_SCHEME_##scheme), \ + (addr), \ + (port), \ + (sname), \ + (ifname)) + +static void +test_dns_uri_parse(void) +{ + /* clang-format off */ + t_dns_1("dns+tls://8.8.8.8", INET, TLS, "8.8.8.8", -1, NULL, NULL); + t_dns_1("dns+tls://8.8.8.8", INET, TLS, "8.8.8.8", -1, NULL, NULL); + t_dns_1("dns+tls://1.2.3.4#name", INET, TLS, "1.2.3.4", -1, "name", NULL); + t_dns_1("dns+tls://1.2.3.4#a.b.c", INET, TLS, "1.2.3.4", -1, "a.b.c", NULL); + t_dns_1("dns+tls://1.2.3.4:53", INET, TLS, "1.2.3.4", 53, NULL, NULL); + t_dns_1("dns+tls://1.2.3.4:53#foobar", INET, TLS, "1.2.3.4", 53, "foobar", NULL); + t_dns_1("dns+tls://192.168.120.250:99", INET, TLS, "192.168.120.250", 99, NULL, NULL); + t_dns_1("dns+udp://8.8.8.8:65535", INET, UDP, "8.8.8.8", 65535, NULL, NULL); + + t_dns_1("dns+udp://[fd01::1]", INET6, UDP, "fd01::1", -1, NULL, NULL); + t_dns_1("dns+tls://[fd01::2]:5353", INET6, UDP, "fd01::2", 5353, NULL, NULL); + t_dns_1("dns+tls://[::1]#name", INET6, UDP, "::1", -1, "name", NULL); + t_dns_1("dns+tls://[::2]:65535#name", INET6, UDP, "::2", 65535, "name", NULL); + t_dns_1("dns+udp://[::ffff:1.2.3.4]", INET6, UDP, "::ffff:1.2.3.4", -1, NULL, NULL); + t_dns_1("dns+tls://[fe80::1%eth0]", INET6, UDP, "fe80::1", -1, NULL, "eth0"); + t_dns_1("dns+tls://[fe80::2%en1]:53#a", INET6, UDP, "fe80::2", 53, "a", "en1"); + t_dns_1("dns+tls://[fe80::1%en3456789012345]", INET6, UDP, "fe80::1", -1, NULL, "en3456789012345"); + + t_dns_1("1.2.3.4", INET, NONE, "1.2.3.4", -1, NULL, NULL); + t_dns_1("1.2.3.4#foo", INET, NONE, "1.2.3.4", -1, "foo", NULL); + t_dns_1("1::#x", INET6, NONE, "1::", -1, "x", NULL); + t_dns_1("1::0#x", INET6, NONE, "1::", -1, "x", NULL); + t_dns_1("192.168.0.1", INET, NONE, "192.168.0.1", -1, NULL, NULL); + t_dns_1("192.168.0.1#tst.com", INET, NONE, "192.168.0.1", -1, "tst.com", NULL); + t_dns_1("fe80::18", INET6, NONE, "fe80::18", -1, NULL, NULL); + t_dns_1("fe80::18#foo.com", INET6, NONE, "fe80::18", -1, "foo.com", NULL); + /* clang-format on */ + + t_dns_0("http://8.8.8.8"); /* unsupported schema */ + t_dns_0("dns+udp://1.2.3.4#name"); /* servername not supported for plain UDP */ + t_dns_0("dns+tls://1.2.3"); /* invalid address */ + t_dns_0("dns+tls://fd01::1"); /* IPv6 requires brackets */ + t_dns_0("dns+tls://[fd13:a:aaaa]"); /* invalid address */ + t_dns_0("dns+tls://1.2.3.4:1:1"); /* invalid syntax */ + t_dns_0("dns+tls://1.2.3.4#name#name"); /* invalid syntax */ + t_dns_0("dns+tls://1.2.3.4%eth0"); /* interface only allowed for IPv6 */ + t_dns_0("dns+tls://[2001::1%eth0]"); /* interface only allowed for IPv6 link-local */ + t_dns_0("dns+tls://[fe80::1%en34567890123456]"); /* interface name too long */ + t_dns_0("1.2.3.4#"); + t_dns_0("1::0#"); + t_dns_0("192.168.0.1:53"); + t_dns_0("192.168.0.1:53#example.com"); + t_dns_0("fe80::18%19"); + t_dns_0("fe80::18%lo"); + t_dns_0("[fe80::18]:53"); + t_dns_0("[fe80::18]:53%19"); + t_dns_0("[fe80::18]:53%lo"); + t_dns_0("fe80::18%19#hoge.com"); + t_dns_0("[fe80::18]:53#hoge.com"); + t_dns_0("[fe80::18]:53%19"); + t_dns_0("[fe80::18]:53%19#hoge.com"); + t_dns_0("[fe80::18]:53%lo"); + t_dns_0("[fe80::18]:53%lo#hoge.com"); +} + +static void +test_dns_uri_parse_plain(void) +{ + struct { + const char *input; + int input_af; + gboolean result; + const char *addrstr; + } values[] = { + {"1.2.3.4", AF_INET, TRUE, "1.2.3.4"}, + {"1.2.3.4", AF_INET6, FALSE, NULL}, + {"1.2.3.4", AF_UNSPEC, TRUE, "1.2.3.4"}, + {"1234:5555:ffff:dddd::4321", AF_INET, FALSE, NULL}, + {"1234:5555:ffff:dddd::4321", AF_INET6, TRUE, "1234:5555:ffff:dddd::4321"}, + {"1234:5555:ffff:dddd::4321", AF_UNSPEC, TRUE, "1234:5555:ffff:dddd::4321"}, + {"192.0.2.1#example.com", AF_INET, TRUE, "192.0.2.1"}, + {"192.0.2.1#example.com", AF_UNSPEC, TRUE, "192.0.2.1"}, + {"192.0.2.1#example.com", AF_INET6, FALSE, NULL}, + {"dns+tls://1.2.3.4", AF_INET, FALSE, NULL}, + {"dns+tls://[fd01::1]", AF_INET, FALSE, NULL}, + {"dns+udp://1.2.3.4:53", AF_INET, TRUE, "1.2.3.4"}, + {"dns+udp://1.2.3.4:54", AF_INET, FALSE, NULL}, + {"dns+udp://[fd01::1]", AF_INET6, TRUE, "fd01::1"}, + {"dns+udp://[fd01::1]:53", AF_INET6, TRUE, "fd01::1"}, + {"dns+udp://[fd01::1]:60000", AF_INET, FALSE, NULL}, + }; + guint i; + + for (i = 0; i < G_N_ELEMENTS(values); i++) { + char addrstr[NM_INET_ADDRSTRLEN]; + gboolean result; + NMIPAddr addr; + + result = nm_dns_uri_parse_plain(values[i].input_af, values[i].input, addrstr, &addr); + g_assert_cmpint(result, ==, values[i].result); + if (result) { + char buf[NM_INET_ADDRSTRLEN]; + + nm_inet_ntop(strchr(addrstr, ':') ? AF_INET6 : AF_INET, addr.addr_ptr, buf); + g_assert_cmpstr(buf, ==, addrstr); + g_assert_cmpstr(addrstr, ==, values[i].addrstr); + } + } +} + +static void +t_dns_uri_normalize(const char *input, const char *expected) +{ + const char *str; + gs_free char *str_free = NULL; + + str = nm_dns_uri_normalize(AF_UNSPEC, input, &str_free); + g_assert_cmpstr(str, ==, expected); +} + +static void +test_dns_uri_normalize(void) +{ + t_dns_uri_normalize("8.8.8.8", "8.8.8.8"); + t_dns_uri_normalize("dns+tls://[2001:0:0::1234]:999#name", "dns+tls://[2001::1234]:999#name"); + t_dns_uri_normalize("dns+udp://[0::1]:0123", "dns+udp://[::1]:123"); + t_dns_uri_normalize("8.8.8.888", NULL); +} + +/*****************************************************************************/ + static void test_dhcp_iaid_hexstr(void) { @@ -11946,6 +12124,9 @@ main(int argc, char **argv) g_test_add_func("/core/general/test_direct_string_is_refstr", test_direct_string_is_refstr); g_test_add_func("/core/general/test_connection_path", test_connection_path); g_test_add_func("/core/general/test_dnsname", test_dnsname); + g_test_add_func("/core/general/test_dns_uri_parse", test_dns_uri_parse); + g_test_add_func("/core/general/test_dns_uri_get_legacy", test_dns_uri_parse_plain); + g_test_add_func("/core/general/test_dns_uri_normalize", test_dns_uri_normalize); g_test_add_func("/core/general/test_dhcp_iaid_hexstr", test_dhcp_iaid_hexstr); return g_test_run();