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".
This commit is contained in:
Beniamino Galvani
2024-12-17 10:17:09 +01:00
parent 28668f8698
commit 4dee109b8d
3 changed files with 497 additions and 0 deletions

View File

@@ -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: * nm_setting_ovs_other_config_check_key:
* @key: (nullable): the key to check * @key: (nullable): the key to check

View File

@@ -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_key(const char *key, GError **error);
gboolean nm_setting_ovs_other_config_check_val(const char *val, GError **error); gboolean nm_setting_ovs_other_config_check_val(const char *val, GError **error);

View File

@@ -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 static void
test_dhcp_iaid_hexstr(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_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_connection_path", test_connection_path);
g_test_add_func("/core/general/test_dnsname", test_dnsname); 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); g_test_add_func("/core/general/test_dhcp_iaid_hexstr", test_dhcp_iaid_hexstr);
return g_test_run(); return g_test_run();