about summary refs log tree commit diff
path: root/src/libnm-core-aux-intern
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
committerMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
commit8bdf070ff046f482f6eb5e2b15ebc216f5d1e3da (patch)
treef706478d189d54c6532e8863d4b0d5ff5575af60 /src/libnm-core-aux-intern
parent818258cf34b83fbc754633295e1052d4752d7b15 (diff)
New upstream version 1.51.90 upstream/1.51.90
Diffstat (limited to 'src/libnm-core-aux-intern')
-rw-r--r--src/libnm-core-aux-intern/meson.build1
-rw-r--r--src/libnm-core-aux-intern/nm-libnm-core-utils.c462
-rw-r--r--src/libnm-core-aux-intern/nm-libnm-core-utils.h57
-rw-r--r--src/libnm-core-aux-intern/tests/meson.build34
-rw-r--r--src/libnm-core-aux-intern/tests/test-libnm-core-utils.c364
5 files changed, 825 insertions, 93 deletions
diff --git a/src/libnm-core-aux-intern/meson.build b/src/libnm-core-aux-intern/meson.build
index a58921e3..f413cde4 100644
--- a/src/libnm-core-aux-intern/meson.build
+++ b/src/libnm-core-aux-intern/meson.build
@@ -1,4 +1,5 @@
 # SPDX-License-Identifier: LGPL-2.1-or-later
+libnm_core_aux_intern_inc = include_directories('.')
 
 libnm_core_aux_intern = static_library(
   'nm-core-aux-intern',
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 b38accb2..5e7b90a4 100644
--- a/src/libnm-core-aux-intern/nm-libnm-core-utils.c
+++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.c
@@ -479,6 +479,151 @@ nm_utils_validate_dhcp_dscp(const char *dscp, GError **error)
 }
 
 gboolean
+nm_utils_validate_shared_dhcp_range(const char *shared_dhcp_range,
+                                    GPtrArray  *addresses,
+                                    GError    **error)
+{
+    char         *start_address_str;
+    char         *end_address_str;
+    NMIPAddress  *interface_address_with_prefix;
+    NMIPAddr      interface_address;
+    NMIPAddr      start_address;
+    NMIPAddr      end_address;
+    guint32       i;
+    guint32       mask;
+    guint32       prefix_length;
+    guint32       start_network;
+    guint32       end_network;
+    guint32       interface_network;
+    guint32       start_ip_length;
+    bool          range_is_in_interface_network;
+    gs_free char *to_free = NULL;
+
+    g_return_val_if_fail(!error || !(*error), FALSE);
+
+    if (!shared_dhcp_range) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("NULL DHCP range; it should be provided as <START_IP>,<END_IP>."));
+        return FALSE;
+    }
+
+    if (!*shared_dhcp_range) {
+        return TRUE;
+    }
+
+    if (!addresses) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Non-NULL range and NULL addresses detected."));
+        return FALSE;
+    }
+
+    end_address_str = strchr(shared_dhcp_range, ',');
+    if (!end_address_str) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Invalid DHCP range; it should be provided as <START_IP>,<END_IP>."));
+        return FALSE;
+    }
+
+    start_ip_length = end_address_str - shared_dhcp_range;
+    if (start_ip_length > 15) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Start IP has invalid length."));
+        return FALSE;
+    }
+
+    start_address_str = nm_strndup_a(200, shared_dhcp_range, start_ip_length, &to_free);
+    ++end_address_str; /* end address is pointing to ',', shift it to the actual address */
+
+    if (!nm_inet_parse_bin(AF_INET, start_address_str, NULL, &start_address)) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Start IP is invalid."));
+        return FALSE;
+    }
+
+    if (!nm_inet_parse_bin(AF_INET, end_address_str, NULL, &end_address)) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("End IP is invalid."));
+        return FALSE;
+    }
+
+    if (ntohl(start_address.addr4) > ntohl(end_address.addr4)) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Start IP should be lower than the end IP."));
+        return FALSE;
+    }
+
+    range_is_in_interface_network = FALSE;
+    for (i = 0; i < (*addresses).len; ++i) {
+        interface_address_with_prefix = (NMIPAddress *) addresses->pdata[i];
+        nm_inet_parse_bin(AF_INET,
+                          nm_ip_address_get_address(interface_address_with_prefix),
+                          NULL,
+                          &interface_address);
+        prefix_length = nm_ip_address_get_prefix(interface_address_with_prefix);
+        mask          = nm_utils_ip4_prefix_to_netmask(prefix_length);
+
+        interface_network = interface_address.addr4 & mask;
+        start_network     = start_address.addr4 & mask;
+        end_network       = end_address.addr4 & mask;
+
+        if (start_network == interface_network && end_network == interface_network) {
+            range_is_in_interface_network = TRUE;
+            break;
+        }
+    }
+
+    if (!range_is_in_interface_network) {
+        g_set_error_literal(
+            error,
+            NM_CONNECTION_ERROR,
+            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+            _("Requested range is not in any network configured on the interface."));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+nm_utils_validate_shared_dhcp_lease_time(int shared_dhcp_lease_time, GError **error)
+{
+    g_return_val_if_fail(!error || !(*error), FALSE);
+
+    if (shared_dhcp_lease_time == 0 || shared_dhcp_lease_time == G_MAXINT32) {
+        return TRUE;
+    }
+
+    if (shared_dhcp_lease_time < NM_MIN_FINITE_LEASE_TIME
+        || NM_MAX_FINITE_LEASE_TIME < shared_dhcp_lease_time) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("Invalid DHCP lease time value; it should be either default or a positive "
+                      "number between %u and %u or %s."),
+                    NM_MIN_FINITE_LEASE_TIME,
+                    NM_MAX_FINITE_LEASE_TIME,
+                    NM_INFINITE_LEASE_TIME);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
 nm_settings_connection_validate_permission_user(const char *item, gssize len)
 {
     gsize l;
@@ -555,112 +700,295 @@ nm_mptcp_flags_normalize(NMMptcpFlags flags)
 
 /*****************************************************************************/
 
+/*
+ * 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_utils_dnsname_parse(int                          addr_family,
-                       const char                  *dns,
-                       int                         *out_addr_family,
-                       gpointer /* (NMIPAddr **) */ out_addr,
-                       const char                 **out_servername)
+nm_dns_uri_parse(int addr_family, const char *str, NMDnsServer *dns)
 {
-    gs_free char *dns_heap = NULL;
-    const char   *s;
-    NMIPAddr      addr;
+    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);
-    nm_assert(!out_addr || out_addr_family || NM_IN_SET(addr_family, AF_INET, AF_INET6));
 
     if (!dns)
+        dns = &dns_stack;
+
+    if (!str)
         return FALSE;
 
-    s = strchr(dns, '#');
+    *dns = (NMDnsServer) {
+        .port = -1,
+    };
 
-    if (s) {
-        dns = nm_strndup_a(200, dns, s - dns, &dns_heap);
-        s++;
+    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 (s && s[0] == '\0') {
-        /* "ADDR#" empty DoT SNI name is not allowed. */
+    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, dns, &addr_family, out_addr ? &addr : NULL))
+    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;
 
-    NM_SET_OUT(out_addr_family, addr_family);
-    if (out_addr)
-        nm_ip_addr_set(addr_family, out_addr, &addr);
-    NM_SET_OUT(out_servername, s);
     return TRUE;
 }
 
-const char *
-nm_utils_dnsname_construct(int                                    addr_family,
-                           gconstpointer /* (const NMIPAddr *) */ addr,
-                           const char                            *server_name,
-                           char                                  *result,
-                           gsize                                  result_len)
+/* @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)
 {
-    char  sbuf[NM_INET_ADDRSTRLEN];
-    gsize l;
-    int   d;
+    NMDnsServer dns;
 
-    nm_assert_addr_family(addr_family);
-    nm_assert(addr);
-    nm_assert(!server_name || !nm_str_is_empty(server_name));
-
-    nm_inet_ntop(addr_family, addr, sbuf);
+    if (!nm_dns_uri_parse(addr_family, str, &dns))
+        return FALSE;
 
-    if (!server_name) {
-        l = g_strlcpy(result, sbuf, result_len);
-    } else {
-        d = g_snprintf(result, result_len, "%s#%s", sbuf, server_name);
-        nm_assert(d >= 0);
-        l = (gsize) d;
+    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;
     }
-
-    return l < result_len ? result : NULL;
 }
 
+/* @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_utils_dnsname_normalize(int addr_family, const char *dns, char **out_free)
+nm_dns_uri_normalize(int addr_family, const char *str, char **out_free)
 {
-    char        sbuf[NM_INET_ADDRSTRLEN];
-    const char *server_name;
-    char       *s;
-    NMIPAddr    a;
-    gsize       l;
+    NMDnsServer dns;
+    char        addrstr[NM_INET_ADDRSTRLEN];
+    char        portstr[32];
+    char       *ret;
+    gsize       len;
 
     nm_assert_addr_family_or_unspec(addr_family);
-    nm_assert(dns);
+    nm_assert(str);
     nm_assert(out_free && !*out_free);
 
-    if (!nm_utils_dnsname_parse(addr_family, dns, &addr_family, &a, &server_name))
+    if (!nm_dns_uri_parse(addr_family, str, &dns))
         return NULL;
 
-    nm_inet_ntop(addr_family, &a, sbuf);
+    nm_inet_ntop(dns.addr_family, &dns.addr, addrstr);
 
-    l = strlen(sbuf);
+    if (dns.port != -1) {
+        nm_assert(dns.port >= 0 && dns.port <= 65535);
+        g_snprintf(portstr, sizeof(portstr), "%d", dns.port);
+    }
 
-    /* In the vast majority of cases, the name is in fact normalized. Check
-     * whether it is, and don't duplicate the string. */
-    if (strncmp(dns, sbuf, l) == 0) {
-        if (server_name) {
-            if (dns[l] == '#' && nm_streq(&dns[l + 1], server_name))
-                return dns;
-        } else {
-            if (dns[l] == '\0')
-                return dns;
+    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;
     }
 
-    if (!server_name)
-        s = g_strdup(sbuf);
-    else
-        s = g_strconcat(sbuf, "#", server_name, NULL);
+    *out_free = ret;
 
-    *out_free = s;
-    return s;
+    return ret;
 }
 
 /*****************************************************************************/
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 454fe4b3..9d1637a2 100644
--- a/src/libnm-core-aux-intern/nm-libnm-core-utils.h
+++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.h
@@ -277,6 +277,17 @@ gboolean nm_utils_validate_dhcp_dscp(const char *dscp, GError **error);
 
 /*****************************************************************************/
 
+#define NM_MIN_FINITE_LEASE_TIME 120
+#define NM_MAX_FINITE_LEASE_TIME (3600 * 24 * 365)
+#define NM_INFINITE_LEASE_TIME   "infinity"
+
+gboolean nm_utils_validate_shared_dhcp_range(const char *shared_dhcp_range,
+                                             GPtrArray  *addresses,
+                                             GError    **error);
+gboolean nm_utils_validate_shared_dhcp_lease_time(int shared_dhcp_lease_time, GError **error);
+
+/*****************************************************************************/
+
 #define NM_SETTINGS_CONNECTION_PERMISSION_USER        "user"
 #define NM_SETTINGS_CONNECTION_PERMISSION_USER_PREFIX "user:"
 
@@ -301,32 +312,26 @@ NMMptcpFlags nm_mptcp_flags_normalize(NMMptcpFlags flags);
 
 /*****************************************************************************/
 
-gboolean nm_utils_dnsname_parse(int                          addr_family,
-                                const char                  *dns,
-                                int                         *out_addr_family,
-                                gpointer /* (NMIPAddr **) */ out_addr,
-                                const char                 **out_servername);
-
-#define nm_utils_dnsname_parse_assert(addr_family, dns, out_addr_family, out_addr, out_servername) \
-    ({                                                                                             \
-        gboolean _good;                                                                            \
-                                                                                                   \
-        _good = nm_utils_dnsname_parse((addr_family),                                              \
-                                       (dns),                                                      \
-                                       (out_addr_family),                                          \
-                                       (out_addr),                                                 \
-                                       (out_servername));                                          \
-        nm_assert(_good);                                                                          \
-        _good;                                                                                     \
-    })
-
-const char *nm_utils_dnsname_construct(int                                    addr_family,
-                                       gconstpointer /* (const NMIPAddr *) */ addr,
-                                       const char                            *server_name,
-                                       char                                  *result,
-                                       gsize                                  result_len);
-
-const char *nm_utils_dnsname_normalize(int addr_family, const char *dns, char **out_free);
+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);
 
 /*****************************************************************************/
 
diff --git a/src/libnm-core-aux-intern/tests/meson.build b/src/libnm-core-aux-intern/tests/meson.build
new file mode 100644
index 00000000..5f2670b9
--- /dev/null
+++ b/src/libnm-core-aux-intern/tests/meson.build
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-libnm-core-utils',
+  'test-libnm-core-utils.c',
+    include_directories: [
+      libnm_core_aux_intern_inc
+  ],
+  link_with: [
+    libnm_core_aux_intern,
+    libnm_core_impl,
+    libnm_base,
+    libnm_crypto,
+    libnm_systemd_shared,
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+  dependencies: [
+    libnm_client_public_dep,
+    libnm_core_public_dep,
+    uuid_dep,
+    glib_dep,
+    dl_dep,
+  ],
+)
+
+test(
+  'src/libnm-core-aux-intern/tests/test-libnm-core-utils',
+  exe,
+  args: test_args + [exe.full_path()],
+  timeout: default_test_timeout,
+)
diff --git a/src/libnm-core-aux-intern/tests/test-libnm-core-utils.c b/src/libnm-core-aux-intern/tests/test-libnm-core-utils.c
new file mode 100644
index 00000000..1df36841
--- /dev/null
+++ b/src/libnm-core-aux-intern/tests/test-libnm-core-utils.c
@@ -0,0 +1,364 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Advantech Czech s.r.o.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
+#include "libnm-glib-aux/nm-test-utils.h"
+
+#include "nm-libnm-core-utils.h"
+#include "nm-errors.h"
+
+static void
+empty_range_valid_for_null_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    result = nm_utils_validate_shared_dhcp_range("", addresses, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+empty_range_valid_for_empty_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+
+    result = nm_utils_validate_shared_dhcp_range("", addresses, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+valid_range_for_single_address(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.0.2,192.168.0.254", addresses, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+valid_range_for_second_address(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, &error));
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.1.254", 24, &error));
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.1.2,192.168.1.254", addresses, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+invalid_null_range_for_null_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    result = nm_utils_validate_shared_dhcp_range(NULL, addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_null_range_for_empty_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.1.2,192.168.1.254", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+any_range_invalid_for_null_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.1.2,192.168.1.254", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+any_range_invalid_for_empty_addresses(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.1.2,192.168.1.254", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_range_xyz(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("xyz", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_range_single_comma(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range(",", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_first_address_of_range(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("xyz,192.168.0.100", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_second_address_of_range(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.0.100,xyz", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_inverted_range(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.0.200,192.168.0.100", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+invalid_range_outside_address_space(void)
+{
+    gs_unref_ptrarray GPtrArray *addresses = NULL;
+    gs_free_error GError        *error     = NULL;
+    gboolean                     result;
+
+    addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref);
+    g_ptr_array_add(addresses, nm_ip_address_new(AF_INET, "192.168.0.1", 24, NULL));
+
+    result = nm_utils_validate_shared_dhcp_range("192.168.1.2,192.168.1.100", addresses, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+/*****************************************************************************/
+
+static void
+valid_zero_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(0, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+minimal_valid_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(NM_MIN_FINITE_LEASE_TIME, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+middle_valid_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(
+        (NM_MIN_FINITE_LEASE_TIME + NM_MAX_FINITE_LEASE_TIME) / 2,
+        &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+maximal_valid_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(NM_MAX_FINITE_LEASE_TIME, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+infinite_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(G_MAXINT32, &error);
+
+    g_assert(result);
+    g_assert_null(error);
+}
+
+static void
+too_small_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(1, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+static void
+too_large_lease_time(void)
+{
+    gs_free_error GError *error = NULL;
+    gboolean              result;
+
+    result = nm_utils_validate_shared_dhcp_lease_time(NM_MAX_FINITE_LEASE_TIME + 1, &error);
+
+    g_assert_false(result);
+    g_assert_error(error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init(&argc, &argv, TRUE);
+
+    g_test_add_func("/core/utils/shared_dhcp_range/empty_range_valid_for_null_addresses",
+                    empty_range_valid_for_null_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/empty_range_valid_for_empty_addresses",
+                    empty_range_valid_for_empty_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/valid_range_for_single_address",
+                    valid_range_for_single_address);
+    g_test_add_func("/core/utils/shared_dhcp_range/valid_range_for_second_address",
+                    valid_range_for_second_address);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_null_range_for_null_addresses",
+                    invalid_null_range_for_null_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_null_range_for_empty_addresses",
+                    invalid_null_range_for_empty_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/any_range_invalid_for_null_addresses",
+                    any_range_invalid_for_null_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/any_range_invalid_for_empty_addresses",
+                    any_range_invalid_for_empty_addresses);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_range_xyz", invalid_range_xyz);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_range_single_comma",
+                    invalid_range_single_comma);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_first_address_of_range",
+                    invalid_first_address_of_range);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_second_address_of_range",
+                    invalid_second_address_of_range);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_inverted_range", invalid_inverted_range);
+    g_test_add_func("/core/utils/shared_dhcp_range/invalid_range_outside_address_space",
+                    invalid_range_outside_address_space);
+
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/valid_zero_lease_time",
+                    valid_zero_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/minimal_valid_lease_time",
+                    minimal_valid_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/middle_valid_lease_time",
+                    middle_valid_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/maximal_valid_lease_time",
+                    maximal_valid_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/infinite_lease_time", infinite_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/too_small_lease_time",
+                    too_small_lease_time);
+    g_test_add_func("/core/utils/shared_dhcp_lease_time/too_large_lease_time",
+                    too_large_lease_time);
+
+    return g_test_run();
+}