]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/shared/dns-domain.c
shared: add new dns_name_startswith() call
[thirdparty/systemd.git] / src / shared / dns-domain.c
index 5680f01bd91ded78a632c107645458685d905077..e777badb6b0e06e43b2f92cb27ca6b0ceb513ceb 100644 (file)
 #include <stringprep.h>
 #endif
 
+#include <endian.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include "alloc-util.h"
 #include "dns-domain.h"
+#include "hashmap.h"
+#include "hexdecoct.h"
+#include "in-addr-util.h"
+#include "macro.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "utf8.h"
 
 int dns_label_unescape(const char **name, char *dest, size_t sz) {
         const char *n;
@@ -33,7 +48,6 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
 
         assert(name);
         assert(*name);
-        assert(dest);
 
         n = *name;
         d = dest;
@@ -47,12 +61,12 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
                 if (*n == 0)
                         break;
 
-                if (sz <= 0)
-                        return -ENOSPC;
-
                 if (r >= DNS_LABEL_MAX)
                         return -EINVAL;
 
+                if (sz <= 0)
+                        return -ENOBUFS;
+
                 if (*n == '\\') {
                         /* Escaped character */
 
@@ -64,9 +78,12 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
 
                         else if (*n == '\\' || *n == '.') {
                                 /* Escaped backslash or dot */
-                                *(d++) = *(n++);
+
+                                if (d)
+                                        *(d++) = *n;
                                 sz--;
                                 r++;
+                                n++;
 
                         } else if (n[0] >= '0' && n[0] <= '9') {
                                 unsigned k;
@@ -81,11 +98,17 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
                                         ((unsigned) (n[1] - '0') * 10) +
                                         ((unsigned) (n[2] - '0'));
 
-                                /* Don't allow CC characters or anything that doesn't fit in 8bit */
-                                if (k < ' ' || k > 255 || k == 127)
+                                /* Don't allow anything that doesn't
+                                 * fit in 8bit. Note that we do allow
+                                 * control characters, as some servers
+                                 * (e.g. cloudflare) are happy to
+                                 * generate labels with them
+                                 * inside. */
+                                if (k > 255)
                                         return -EINVAL;
 
-                                *(d++) = (char) k;
+                                if (d)
+                                        *(d++) = (char) k;
                                 sz--;
                                 r++;
 
@@ -96,9 +119,12 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
                 } else if ((uint8_t) *n >= (uint8_t) ' ' && *n != 127) {
 
                         /* Normal character */
-                        *(d++) = *(n++);
+
+                        if (d)
+                                *(d++) = *n;
                         sz--;
                         r++;
+                        n++;
                 } else
                         return -EINVAL;
         }
@@ -107,7 +133,7 @@ int dns_label_unescape(const char **name, char *dest, size_t sz) {
         if (r == 0 && *n)
                 return -EINVAL;
 
-        if (sz >= 1)
+        if (sz >= 1 && d)
                 *d = 0;
 
         *name = n;
@@ -133,20 +159,24 @@ int dns_label_unescape_suffix(const char *name, const char **label_terminal, cha
                 return 0;
         }
 
-        assert(**label_terminal == '.' || **label_terminal == 0);
+        terminal = *label_terminal;
+        assert(*terminal == '.' || *terminal == 0);
 
-        /* skip current terminal character */
-        terminal = *label_terminal - 1;
+        /* Skip current terminal character (and accept domain names ending it ".") */
+        if (*terminal == 0)
+                terminal--;
+        if (terminal >= name && *terminal == '.')
+                terminal--;
 
-        /* point name to the last label, and terminal to the preceding terminal symbol (or make it a NULL pointer) */
+        /* Point name to the last label, and terminal to the preceding terminal symbol (or make it a NULL pointer) */
         for (;;) {
                 if (terminal < name) {
-                        /* reached the first label, so indicate that there are no more */
+                        /* Reached the first label, so indicate that there are no more */
                         terminal = NULL;
                         break;
                 }
 
-                /* find the start of the last label */
+                /* Find the start of the last label */
                 if (*terminal == '.') {
                         const char *y;
                         unsigned slashes = 0;
@@ -155,7 +185,7 @@ int dns_label_unescape_suffix(const char *name, const char **label_terminal, cha
                                 slashes ++;
 
                         if (slashes % 2 == 0) {
-                                /* the '.' was not escaped */
+                                /* The '.' was not escaped */
                                 name = terminal + 1;
                                 break;
                         } else {
@@ -176,30 +206,36 @@ int dns_label_unescape_suffix(const char *name, const char **label_terminal, cha
         return r;
 }
 
-int dns_label_escape(const char *p, size_t l, char **ret) {
-        _cleanup_free_ char *s = NULL;
+int dns_label_escape(const char *p, size_t l, char *dest, size_t sz) {
         char *q;
-        int r;
 
-        assert(p);
-        assert(ret);
+        /* DNS labels must be between 1 and 63 characters long. A
+         * zero-length label does not exist. See RFC 2182, Section
+         * 11. */
 
-        if (l > DNS_LABEL_MAX)
+        if (l <= 0 || l > DNS_LABEL_MAX)
                 return -EINVAL;
+        if (sz < 1)
+                return -ENOBUFS;
 
-        s = malloc(l * 4 + 1);
-        if (!s)
-                return -ENOMEM;
+        assert(p);
+        assert(dest);
 
-        q = s;
+        q = dest;
         while (l > 0) {
 
                 if (*p == '.' || *p == '\\') {
 
                         /* Dot or backslash */
+
+                        if (sz < 3)
+                                return -ENOBUFS;
+
                         *(q++) = '\\';
                         *(q++) = *p;
 
+                        sz -= 2;
+
                 } else if (*p == '_' ||
                            *p == '-' ||
                            (*p >= '0' && *p <= '9') ||
@@ -207,25 +243,55 @@ int dns_label_escape(const char *p, size_t l, char **ret) {
                            (*p >= 'A' && *p <= 'Z')) {
 
                         /* Proper character */
+
+                        if (sz < 2)
+                                return -ENOBUFS;
+
                         *(q++) = *p;
-                } else if ((uint8_t) *p >= (uint8_t) ' ' && *p != 127) {
+                        sz -= 1;
+
+                } else {
 
                         /* Everything else */
+
+                        if (sz < 5)
+                                return -ENOBUFS;
+
                         *(q++) = '\\';
                         *(q++) = '0' + (char) ((uint8_t) *p / 100);
                         *(q++) = '0' + (char) (((uint8_t) *p / 10) % 10);
                         *(q++) = '0' + (char) ((uint8_t) *p % 10);
 
-                } else
-                        return -EINVAL;
+                        sz -= 4;
+                }
 
                 p++;
                 l--;
         }
 
         *q = 0;
+        return (int) (q - dest);
+}
+
+int dns_label_escape_new(const char *p, size_t l, char **ret) {
+        _cleanup_free_ char *s = NULL;
+        int r;
+
+        assert(p);
+        assert(ret);
+
+        if (l <= 0 || l > DNS_LABEL_MAX)
+                return -EINVAL;
+
+        s = new(char, DNS_LABEL_ESCAPED_MAX);
+        if (!s)
+                return -ENOMEM;
+
+        r = dns_label_escape(p, l, s, DNS_LABEL_ESCAPED_MAX);
+        if (r < 0)
+                return r;
+
         *ret = s;
-        r = q - s;
         s = NULL;
 
         return r;
@@ -234,32 +300,52 @@ int dns_label_escape(const char *p, size_t l, char **ret) {
 int dns_label_apply_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max) {
 #ifdef HAVE_LIBIDN
         _cleanup_free_ uint32_t *input = NULL;
-        size_t input_size;
+        size_t input_size, l;
         const char *p;
         bool contains_8bit = false;
+        char buffer[DNS_LABEL_MAX+1];
 
         assert(encoded);
         assert(decoded);
-        assert(decoded_max >= DNS_LABEL_MAX);
+
+        /* Converts an U-label into an A-label */
 
         if (encoded_size <= 0)
-                return 0;
+                return -EINVAL;
 
         for (p = encoded; p < encoded + encoded_size; p++)
                 if ((uint8_t) *p > 127)
                         contains_8bit = true;
 
-        if (!contains_8bit)
+        if (!contains_8bit) {
+                if (encoded_size > DNS_LABEL_MAX)
+                        return -EINVAL;
+
                 return 0;
+        }
 
         input = stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
         if (!input)
                 return -ENOMEM;
 
-        if (idna_to_ascii_4i(input, input_size, decoded, 0) != 0)
+        if (idna_to_ascii_4i(input, input_size, buffer, 0) != 0)
+                return -EINVAL;
+
+        l = strlen(buffer);
+
+        /* Verify that the the result is not longer than one DNS label. */
+        if (l <= 0 || l > DNS_LABEL_MAX)
                 return -EINVAL;
+        if (l > decoded_max)
+                return -ENOBUFS;
 
-        return strlen(decoded);
+        memcpy(decoded, buffer, l);
+
+        /* If there's room, append a trailing NUL byte, but only then */
+        if (decoded_max > l)
+                decoded[l] = 0;
+
+        return (int) l;
 #else
         return 0;
 #endif
@@ -273,11 +359,14 @@ int dns_label_undo_idna(const char *encoded, size_t encoded_size, char *decoded,
         uint32_t *output = NULL;
         size_t w;
 
-        /* To be invoked after unescaping */
+        /* To be invoked after unescaping. Converts an A-label into an U-label. */
 
         assert(encoded);
         assert(decoded);
 
+        if (encoded_size <= 0 || encoded_size > DNS_LABEL_MAX)
+                return -EINVAL;
+
         if (encoded_size < sizeof(IDNA_ACE_PREFIX)-1)
                 return 0;
 
@@ -297,11 +386,16 @@ int dns_label_undo_idna(const char *encoded, size_t encoded_size, char *decoded,
         if (!result)
                 return -ENOMEM;
         if (w <= 0)
-                return 0;
-        if (w+1 > decoded_max)
                 return -EINVAL;
+        if (w > decoded_max)
+                return -ENOBUFS;
+
+        memcpy(decoded, result, w);
+
+        /* Append trailing NUL byte if there's space, but only then. */
+        if (decoded_max > w)
+                decoded[w] = 0;
 
-        memcpy(decoded, result, w+1);
         return w;
 #else
         return 0;
@@ -318,7 +412,6 @@ int dns_name_concat(const char *a, const char *b, char **_ret) {
         assert(a);
 
         for (;;) {
-                _cleanup_free_ char *t = NULL;
                 char label[DNS_LABEL_MAX];
                 int k;
 
@@ -345,26 +438,33 @@ int dns_name_concat(const char *a, const char *b, char **_ret) {
                 if (k > 0)
                         r = k;
 
-                r = dns_label_escape(label, r, &t);
-                if (r < 0)
-                        return r;
-
                 if (_ret) {
-                        if (!GREEDY_REALLOC(ret, allocated, n + !first + strlen(t) + 1))
+                        if (!GREEDY_REALLOC(ret, allocated, n + !first + DNS_LABEL_ESCAPED_MAX))
                                 return -ENOMEM;
 
+                        r = dns_label_escape(label, r, ret + n + !first, DNS_LABEL_ESCAPED_MAX);
+                        if (r < 0)
+                                return r;
+
                         if (!first)
-                                ret[n++] = '.';
-                        else
-                                first = false;
+                                ret[n] = '.';
+                } else {
+                        char escaped[DNS_LABEL_ESCAPED_MAX];
 
-                        memcpy(ret + n, t, r);
+                        r = dns_label_escape(label, r, escaped, sizeof(escaped));
+                        if (r < 0)
+                                return r;
                 }
 
+                if (!first)
+                        n++;
+                else
+                        first = false;
+
                 n += r;
         }
 
-        if (n > DNS_NAME_MAX)
+        if (n > DNS_HOSTNAME_MAX)
                 return -EINVAL;
 
         if (_ret) {
@@ -385,13 +485,15 @@ void dns_name_hash_func(const void *s, struct siphash *state) {
 
         assert(p);
 
-        while (*p) {
+        for (;;) {
                 char label[DNS_LABEL_MAX+1];
                 int k;
 
                 r = dns_label_unescape(&p, label, sizeof(label));
                 if (r < 0)
                         break;
+                if (r == 0)
+                        break;
 
                 k = dns_label_undo_idna(label, r, label, sizeof(label));
                 if (k < 0)
@@ -399,13 +501,9 @@ void dns_name_hash_func(const void *s, struct siphash *state) {
                 if (k > 0)
                         r = k;
 
-                if (r == 0)
-                        break;
-
-                label[r] = 0;
-                ascii_strlower(label);
-
-                string_hash_func(label, state);
+                ascii_strlower_n(label, r);
+                siphash24_compress(label, r, state);
+                siphash24_compress_byte(0, state); /* make sure foobar and foo.bar result in different hashes */
         }
 
         /* enforce that all names are terminated by the empty label */
@@ -440,7 +538,7 @@ int dns_name_compare_func(const void *a, const void *b) {
                 if (k > 0)
                         r = k;
                 if (w > 0)
-                        r = w;
+                        q = w;
 
                 la[r] = lb[q] = 0;
                 r = strcasecmp(la, lb);
@@ -469,24 +567,32 @@ int dns_name_equal(const char *x, const char *y) {
                 r = dns_label_unescape(&x, la, sizeof(la));
                 if (r < 0)
                         return r;
-
-                k = dns_label_undo_idna(la, r, la, sizeof(la));
-                if (k < 0)
-                        return k;
-                if (k > 0)
-                        r = k;
+                if (r > 0) {
+                        k = dns_label_undo_idna(la, r, la, sizeof(la));
+                        if (k < 0)
+                                return k;
+                        if (k > 0)
+                                r = k;
+                }
 
                 q = dns_label_unescape(&y, lb, sizeof(lb));
                 if (q < 0)
                         return q;
-                w = dns_label_undo_idna(lb, q, lb, sizeof(lb));
-                if (w < 0)
-                        return w;
-                if (w > 0)
-                        q = w;
+                if (q > 0) {
+                        w = dns_label_undo_idna(lb, q, lb, sizeof(lb));
+                        if (w < 0)
+                                return w;
+                        if (w > 0)
+                                q = w;
+                }
+
+                /* If one name had fewer labels than the other, this
+                 * will show up as empty label here, which the
+                 * strcasecmp() below will properly consider different
+                 * from a non-empty label. */
 
                 la[r] = lb[q] = 0;
-                if (strcasecmp(la, lb))
+                if (strcasecmp(la, lb) != 0)
                         return false;
         }
 }
@@ -507,11 +613,13 @@ int dns_name_endswith(const char *name, const char *suffix) {
                 r = dns_label_unescape(&n, ln, sizeof(ln));
                 if (r < 0)
                         return r;
-                k = dns_label_undo_idna(ln, r, ln, sizeof(ln));
-                if (k < 0)
-                        return k;
-                if (k > 0)
-                        r = k;
+                if (r > 0) {
+                        k = dns_label_undo_idna(ln, r, ln, sizeof(ln));
+                        if (k < 0)
+                                return k;
+                        if (k > 0)
+                                r = k;
+                }
 
                 if (!saved_n)
                         saved_n = n;
@@ -519,11 +627,13 @@ int dns_name_endswith(const char *name, const char *suffix) {
                 q = dns_label_unescape(&s, ls, sizeof(ls));
                 if (q < 0)
                         return q;
-                w = dns_label_undo_idna(ls, q, ls, sizeof(ls));
-                if (w < 0)
-                        return w;
-                if (w > 0)
-                        q = w;
+                if (q > 0) {
+                        w = dns_label_undo_idna(ls, q, ls, sizeof(ls));
+                        if (w < 0)
+                                return w;
+                        if (w > 0)
+                                q = w;
+                }
 
                 if (r == 0 && q == 0)
                         return true;
@@ -542,6 +652,125 @@ int dns_name_endswith(const char *name, const char *suffix) {
         }
 }
 
+static int dns_label_unescape_undo_idna(const char **name, char *dest, size_t sz) {
+        int r, k;
+
+        /* Clobbers all arguments on failure... */
+
+        r = dns_label_unescape(name, dest, sz);
+        if (r <= 0)
+                return r;
+
+        k = dns_label_undo_idna(dest, r, dest, sz);
+        if (k < 0)
+                return k;
+        if (k == 0) /* not an IDNA name */
+                return r;
+
+        return k;
+}
+
+int dns_name_startswith(const char *name, const char *prefix) {
+        const char *n, *p;
+        int r, q;
+
+        assert(name);
+        assert(prefix);
+
+        n = name;
+        p = prefix;
+
+        for (;;) {
+                char ln[DNS_LABEL_MAX], lp[DNS_LABEL_MAX];
+
+                r = dns_label_unescape_undo_idna(&p, lp, sizeof(lp));
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return true;
+
+                q = dns_label_unescape_undo_idna(&n, ln, sizeof(ln));
+                if (q < 0)
+                        return q;
+
+                if (r != q)
+                        return false;
+                if (ascii_strcasecmp_n(ln, lp, r) != 0)
+                        return false;
+        }
+}
+
+int dns_name_change_suffix(const char *name, const char *old_suffix, const char *new_suffix, char **ret) {
+        const char *n, *s, *saved_before = NULL, *saved_after = NULL, *prefix;
+        int r, q, k, w;
+
+        assert(name);
+        assert(old_suffix);
+        assert(new_suffix);
+        assert(ret);
+
+        n = name;
+        s = old_suffix;
+
+        for (;;) {
+                char ln[DNS_LABEL_MAX+1], ls[DNS_LABEL_MAX+1];
+
+                if (!saved_before)
+                        saved_before = n;
+
+                r = dns_label_unescape(&n, ln, sizeof(ln));
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        k = dns_label_undo_idna(ln, r, ln, sizeof(ln));
+                        if (k < 0)
+                                return k;
+                        if (k > 0)
+                                r = k;
+                }
+
+                if (!saved_after)
+                        saved_after = n;
+
+                q = dns_label_unescape(&s, ls, sizeof(ls));
+                if (q < 0)
+                        return q;
+                if (q > 0) {
+                        w = dns_label_undo_idna(ls, q, ls, sizeof(ls));
+                        if (w < 0)
+                                return w;
+                        if (w > 0)
+                                q = w;
+                }
+
+                if (r == 0 && q == 0)
+                        break;
+                if (r == 0 && saved_after == n) {
+                        *ret = NULL; /* doesn't match */
+                        return 0;
+                }
+
+                ln[r] = ls[q] = 0;
+
+                if (r != q || strcasecmp(ln, ls)) {
+
+                        /* Not the same, let's jump back, and try with the next label again */
+                        s = old_suffix;
+                        n = saved_after;
+                        saved_after = saved_before = NULL;
+                }
+        }
+
+        /* Found it! Now generate the new name */
+        prefix = strndupa(name, saved_before - name);
+
+        r = dns_name_concat(prefix, new_suffix, ret);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
 int dns_name_between(const char *a, const char *b, const char *c) {
         int n;
 
@@ -680,34 +909,372 @@ int dns_name_address(const char *p, int *family, union in_addr_union *address) {
         return 0;
 }
 
-int dns_name_root(const char *name) {
-        char label[DNS_LABEL_MAX+1];
-        int r;
+bool dns_name_is_root(const char *name) {
 
         assert(name);
 
-        r = dns_label_unescape(&name, label, sizeof(label));
-        if (r < 0)
-                return r;
+        /* There are exactly two ways to encode the root domain name:
+         * as empty string, or with a single dot. */
 
-        return r == 0 && *name == 0;
+        return STR_IN_SET(name, "", ".");
 }
 
-int dns_name_single_label(const char *name) {
+bool dns_name_is_single_label(const char *name) {
         char label[DNS_LABEL_MAX+1];
         int r;
 
         assert(name);
 
         r = dns_label_unescape(&name, label, sizeof(label));
+        if (r <= 0)
+                return false;
+
+        return dns_name_is_root(name);
+}
+
+/* Encode a domain name according to RFC 1035 Section 3.1, without compression */
+int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, bool canonical) {
+        uint8_t *label_length, *out;
+        int r;
+
+        assert(domain);
+        assert(buffer);
+
+        out = buffer;
+
+        do {
+                /* Reserve a byte for label length */
+                if (len <= 0)
+                        return -ENOBUFS;
+                len--;
+                label_length = out;
+                out++;
+
+                /* Convert and copy a single label. Note that
+                 * dns_label_unescape() returns 0 when it hits the end
+                 * of the domain name, which we rely on here to encode
+                 * the trailing NUL byte. */
+                r = dns_label_unescape(&domain, (char *) out, len);
+                if (r < 0)
+                        return r;
+
+                /* Optionally, output the name in DNSSEC canonical
+                 * format, as described in RFC 4034, section 6.2. Or
+                 * in other words: in lower-case. */
+                if (canonical)
+                        ascii_strlower_n((char*) out, (size_t) r);
+
+                /* Fill label length, move forward */
+                *label_length = r;
+                out += r;
+                len -= r;
+
+        } while (r != 0);
+
+        /* Verify the maximum size of the encoded name. The trailing
+         * dot + NUL byte account are included this time, hence
+         * compare against DNS_HOSTNAME_MAX + 2 (which is 255) this
+         * time. */
+        if (out - buffer > DNS_HOSTNAME_MAX + 2)
+                return -EINVAL;
+
+        return out - buffer;
+}
+
+static bool srv_type_label_is_valid(const char *label, size_t n) {
+        size_t k;
+
+        assert(label);
+
+        if (n < 2) /* Label needs to be at least 2 chars long */
+                return false;
+
+        if (label[0] != '_') /* First label char needs to be underscore */
+                return false;
+
+        /* Second char must be a letter */
+        if (!(label[1] >= 'A' && label[1] <= 'Z') &&
+            !(label[1] >= 'a' && label[1] <= 'z'))
+                return false;
+
+        /* Third and further chars must be alphanumeric or a hyphen */
+        for (k = 2; k < n; k++) {
+                if (!(label[k] >= 'A' && label[k] <= 'Z') &&
+                    !(label[k] >= 'a' && label[k] <= 'z') &&
+                    !(label[k] >= '0' && label[k] <= '9') &&
+                    label[k] != '-')
+                        return false;
+        }
+
+        return true;
+}
+
+bool dns_srv_type_is_valid(const char *name) {
+        unsigned c = 0;
+        int r;
+
+        if (!name)
+                return false;
+
+        for (;;) {
+                char label[DNS_LABEL_MAX];
+
+                /* This more or less implements RFC 6335, Section 5.1 */
+
+                r = dns_label_unescape(&name, label, sizeof(label));
+                if (r < 0)
+                        return false;
+                if (r == 0)
+                        break;
+
+                if (c >= 2)
+                        return false;
+
+                if (!srv_type_label_is_valid(label, r))
+                        return false;
+
+                c++;
+        }
+
+        return c == 2; /* exactly two labels */
+}
+
+bool dns_service_name_is_valid(const char *name) {
+        size_t l;
+
+        /* This more or less implements RFC 6763, Section 4.1.1 */
+
+        if (!name)
+                return false;
+
+        if (!utf8_is_valid(name))
+                return false;
+
+        if (string_has_cc(name, NULL))
+                return false;
+
+        l = strlen(name);
+        if (l <= 0)
+                return false;
+        if (l > 63)
+                return false;
+
+        return true;
+}
+
+int dns_service_join(const char *name, const char *type, const char *domain, char **ret) {
+        char escaped[DNS_LABEL_ESCAPED_MAX];
+        _cleanup_free_ char *n = NULL;
+        int r;
+
+        assert(type);
+        assert(domain);
+        assert(ret);
+
+        if (!dns_srv_type_is_valid(type))
+                return -EINVAL;
+
+        if (!name)
+                return dns_name_concat(type, domain, ret);
+
+        if (!dns_service_name_is_valid(name))
+                return -EINVAL;
+
+        r = dns_label_escape(name, strlen(name), escaped, sizeof(escaped));
         if (r < 0)
                 return r;
-        if (r == 0)
-                return 0;
 
-        r = dns_label_unescape(&name, label, sizeof(label));
+        r = dns_name_concat(type, domain, &n);
+        if (r < 0)
+                return r;
+
+        return dns_name_concat(escaped, n, ret);
+}
+
+static bool dns_service_name_label_is_valid(const char *label, size_t n) {
+        char *s;
+
+        assert(label);
+
+        if (memchr(label, 0, n))
+                return false;
+
+        s = strndupa(label, n);
+        return dns_service_name_is_valid(s);
+}
+
+int dns_service_split(const char *joined, char **_name, char **_type, char **_domain) {
+        _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL;
+        const char *p = joined, *q = NULL, *d = NULL;
+        char a[DNS_LABEL_MAX], b[DNS_LABEL_MAX], c[DNS_LABEL_MAX];
+        int an, bn, cn, r;
+        unsigned x = 0;
+
+        assert(joined);
+
+        /* Get first label from the full name */
+        an = dns_label_unescape(&p, a, sizeof(a));
+        if (an < 0)
+                return an;
+
+        if (an > 0) {
+                x++;
+
+                /* If there was a first label, try to get the second one */
+                bn = dns_label_unescape(&p, b, sizeof(b));
+                if (bn < 0)
+                        return bn;
+
+                if (bn > 0) {
+                        x++;
+
+                        /* If there was a second label, try to get the third one */
+                        q = p;
+                        cn = dns_label_unescape(&p, c, sizeof(c));
+                        if (cn < 0)
+                                return cn;
+
+                        if (cn > 0)
+                                x++;
+                } else
+                        cn = 0;
+        } else
+                an = 0;
+
+        if (x >= 2 && srv_type_label_is_valid(b, bn)) {
+
+                if (x >= 3 && srv_type_label_is_valid(c, cn)) {
+
+                        if (dns_service_name_label_is_valid(a, an)) {
+
+                                /* OK, got <name> . <type> . <type2> . <domain> */
+
+                                name = strndup(a, an);
+                                if (!name)
+                                        return -ENOMEM;
+
+                                type = new(char, bn+1+cn+1);
+                                if (!type)
+                                        return -ENOMEM;
+                                strcpy(stpcpy(stpcpy(type, b), "."), c);
+
+                                d = p;
+                                goto finish;
+                        }
+
+                } else if (srv_type_label_is_valid(a, an)) {
+
+                        /* OK, got <type> . <type2> . <domain> */
+
+                        name = NULL;
+
+                        type = new(char, an+1+bn+1);
+                        if (!type)
+                                return -ENOMEM;
+                        strcpy(stpcpy(stpcpy(type, a), "."), b);
+
+                        d = q;
+                        goto finish;
+                }
+        }
+
+        name = NULL;
+        type = NULL;
+        d = joined;
+
+finish:
+        r = dns_name_normalize(d, &domain);
         if (r < 0)
                 return r;
 
-        return r == 0 && *name == 0;
+        if (_domain) {
+                *_domain = domain;
+                domain = NULL;
+        }
+
+        if (_type) {
+                *_type = type;
+                type = NULL;
+        }
+
+        if (_name) {
+                *_name = name;
+                name = NULL;
+        }
+
+        return 0;
+}
+
+int dns_name_suffix(const char *name, unsigned n_labels, const char **ret) {
+        const char* labels[DNS_N_LABELS_MAX+1];
+        unsigned n = 0;
+        const char *p;
+        int r;
+
+        assert(name);
+        assert(ret);
+
+        p = name;
+        for (;;) {
+                if (n > DNS_N_LABELS_MAX)
+                        return -EINVAL;
+
+                labels[n] = p;
+
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                n++;
+        }
+
+        if (n < n_labels)
+                return -EINVAL;
+
+        *ret = labels[n - n_labels];
+        return (int) (n - n_labels);
+}
+
+int dns_name_count_labels(const char *name) {
+        unsigned n = 0;
+        const char *p;
+        int r;
+
+        assert(name);
+
+        p = name;
+        for (;;) {
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                if (n >= DNS_N_LABELS_MAX)
+                        return -EINVAL;
+
+                n++;
+        }
+
+        return (int) n;
+}
+
+int dns_name_equal_skip(const char *a, unsigned n_labels, const char *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        while (n_labels > 0) {
+
+                r = dns_name_parent(&a);
+                if (r <= 0)
+                        return r;
+
+                n_labels --;
+        }
+
+        return dns_name_equal(a, b);
 }