Systemd/src/shared/dns-domain.c

1420 lines
41 KiB
C
Raw Normal View History

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <endian.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include "alloc-util.h"
2015-06-02 20:49:43 +02:00
#include "dns-domain.h"
#include "hashmap.h"
#include "hexdecoct.h"
#include "hostname-util.h"
#include "idn-util.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, DNSLabelFlags flags) {
const char *n;
char *d, last_char = 0;
int r = 0;
assert(name);
assert(*name);
n = *name;
d = dest;
for (;;) {
if (IN_SET(*n, 0, '.')) {
if (FLAGS_SET(flags, DNS_LABEL_LDH) && last_char == '-')
/* Trailing dash */
return -EINVAL;
if (n[0] == '.' && (n[1] != 0 || !FLAGS_SET(flags, DNS_LABEL_LEAVE_TRAILING_DOT)))
n++;
break;
}
if (r >= DNS_LABEL_MAX)
return -EINVAL;
if (sz <= 0)
return -ENOBUFS;
if (*n == '\\') {
/* Escaped character */
if (FLAGS_SET(flags, DNS_LABEL_NO_ESCAPES))
return -EINVAL;
n++;
if (*n == 0)
/* Ending NUL */
return -EINVAL;
2017-10-04 16:01:32 +02:00
else if (IN_SET(*n, '\\', '.')) {
/* Escaped backslash or dot */
if (FLAGS_SET(flags, DNS_LABEL_LDH))
return -EINVAL;
last_char = *n;
if (d)
*(d++) = *n;
sz--;
r++;
n++;
} else if (n[0] >= '0' && n[0] <= '9') {
unsigned k;
/* Escaped literal ASCII character */
if (!(n[1] >= '0' && n[1] <= '9') ||
!(n[2] >= '0' && n[2] <= '9'))
return -EINVAL;
k = ((unsigned) (n[0] - '0') * 100) +
((unsigned) (n[1] - '0') * 10) +
((unsigned) (n[2] - '0'));
/* 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;
if (FLAGS_SET(flags, DNS_LABEL_LDH) &&
!valid_ldh_char((char) k))
return -EINVAL;
last_char = (char) k;
if (d)
*(d++) = (char) k;
sz--;
r++;
n += 3;
} else
return -EINVAL;
} else if ((uint8_t) *n >= (uint8_t) ' ' && *n != 127) {
/* Normal character */
if (FLAGS_SET(flags, DNS_LABEL_LDH)) {
if (!valid_ldh_char(*n))
return -EINVAL;
if (r == 0 && *n == '-')
/* Leading dash */
return -EINVAL;
}
last_char = *n;
if (d)
*(d++) = *n;
sz--;
r++;
n++;
} else
return -EINVAL;
}
/* Empty label that is not at the end? */
if (r == 0 && *n)
return -EINVAL;
/* More than one trailing dot? */
if (n[0] == '.' && !FLAGS_SET(flags, DNS_LABEL_LEAVE_TRAILING_DOT))
return -EINVAL;
if (sz >= 1 && d)
*d = 0;
*name = n;
return r;
}
/* @label_terminal: terminal character of a label, updated to point to the terminal character of
* the previous label (always skipping one dot) or to NULL if there are no more
* labels. */
int dns_label_unescape_suffix(const char *name, const char **label_terminal, char *dest, size_t sz) {
const char *terminal;
int r;
assert(name);
assert(label_terminal);
assert(dest);
/* no more labels */
if (!*label_terminal) {
if (sz >= 1)
*dest = 0;
return 0;
}
terminal = *label_terminal;
2017-10-04 16:01:32 +02:00
assert(IN_SET(*terminal, 0, '.'));
/* 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) */
for (;;) {
if (terminal < name) {
/* Reached the first label, so indicate that there are no more */
terminal = NULL;
break;
}
/* Find the start of the last label */
if (*terminal == '.') {
const char *y;
unsigned slashes = 0;
for (y = terminal - 1; y >= name && *y == '\\'; y--)
slashes++;
if (slashes % 2 == 0) {
/* The '.' was not escaped */
name = terminal + 1;
break;
} else {
terminal = y;
continue;
}
}
terminal--;
}
r = dns_label_unescape(&name, dest, sz, 0);
if (r < 0)
return r;
*label_terminal = terminal;
return r;
}
int dns_label_escape(const char *p, size_t l, char *dest, size_t sz) {
char *q;
/* DNS labels must be between 1 and 63 characters long. A
* zero-length label does not exist. See RFC 2182, Section
* 11. */
if (l <= 0 || l > DNS_LABEL_MAX)
return -EINVAL;
if (sz < 1)
return -ENOBUFS;
assert(p);
assert(dest);
q = dest;
while (l > 0) {
2017-10-04 16:01:32 +02:00
if (IN_SET(*p, '.', '\\')) {
/* Dot or backslash */
if (sz < 3)
return -ENOBUFS;
*(q++) = '\\';
*(q++) = *p;
sz -= 2;
2017-10-04 16:01:32 +02:00
} else if (IN_SET(*p, '_', '-') ||
(*p >= '0' && *p <= '9') ||
(*p >= 'a' && *p <= 'z') ||
(*p >= 'A' && *p <= 'Z')) {
/* Proper character */
if (sz < 2)
return -ENOBUFS;
*(q++) = *p;
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);
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 = TAKE_PTR(s);
return r;
}
#if HAVE_LIBIDN
int dns_label_apply_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max) {
_cleanup_free_ uint32_t *input = NULL;
size_t input_size, l;
const char *p;
bool contains_8bit = false;
char buffer[DNS_LABEL_MAX+1];
int r;
assert(encoded);
assert(decoded);
/* Converts an U-label into an A-label */
r = dlopen_idn();
if (r < 0)
return r;
if (encoded_size <= 0)
return -EINVAL;
for (p = encoded; p < encoded + encoded_size; p++)
if ((uint8_t) *p > 127)
contains_8bit = true;
if (!contains_8bit) {
if (encoded_size > DNS_LABEL_MAX)
return -EINVAL;
return 0;
}
input = sym_stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
if (!input)
return -ENOMEM;
if (sym_idna_to_ascii_4i(input, input_size, buffer, 0) != 0)
return -EINVAL;
l = strlen(buffer);
2016-02-12 00:26:37 +01:00
/* Verify that the result is not longer than one DNS label. */
if (l <= 0 || l > DNS_LABEL_MAX)
return -EINVAL;
if (l > decoded_max)
return -ENOBUFS;
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;
}
int dns_label_undo_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max) {
size_t input_size, output_size;
_cleanup_free_ uint32_t *input = NULL;
_cleanup_free_ char *result = NULL;
uint32_t *output = NULL;
size_t w;
int r;
/* To be invoked after unescaping. Converts an A-label into an U-label. */
assert(encoded);
assert(decoded);
r = dlopen_idn();
if (r < 0)
return r;
if (encoded_size <= 0 || encoded_size > DNS_LABEL_MAX)
return -EINVAL;
if (!memory_startswith(encoded, encoded_size, IDNA_ACE_PREFIX))
return 0;
input = sym_stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
if (!input)
return -ENOMEM;
output_size = input_size;
output = newa(uint32_t, output_size);
sym_idna_to_unicode_44i(input, input_size, output, &output_size, 0);
result = sym_stringprep_ucs4_to_utf8(output, output_size, NULL, &w);
if (!result)
return -ENOMEM;
if (w <= 0)
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;
return w;
}
#endif
int dns_name_concat(const char *a, const char *b, DNSLabelFlags flags, char **_ret) {
_cleanup_free_ char *ret = NULL;
size_t n = 0, allocated = 0;
const char *p;
bool first = true;
int r;
if (a)
p = a;
else if (b)
p = TAKE_PTR(b);
else
goto finish;
for (;;) {
char label[DNS_LABEL_MAX];
r = dns_label_unescape(&p, label, sizeof label, flags);
if (r < 0)
return r;
if (r == 0) {
if (*p != 0)
return -EINVAL;
if (b) {
/* Now continue with the second string, if there is one */
p = TAKE_PTR(b);
continue;
}
break;
}
if (_ret) {
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 {
char escaped[DNS_LABEL_ESCAPED_MAX];
r = dns_label_escape(label, r, escaped, sizeof(escaped));
if (r < 0)
return r;
}
if (!first)
n++;
else
first = false;
n += r;
}
finish:
if (n > DNS_HOSTNAME_MAX)
return -EINVAL;
if (_ret) {
if (n == 0) {
/* Nothing appended? If so, generate at least a single dot, to indicate the DNS root domain */
if (!GREEDY_REALLOC(ret, allocated, 2))
return -ENOMEM;
ret[n++] = '.';
} else {
if (!GREEDY_REALLOC(ret, allocated, n + 1))
return -ENOMEM;
}
ret[n] = 0;
*_ret = TAKE_PTR(ret);
}
return 0;
}
2018-11-27 14:25:20 +01:00
void dns_name_hash_func(const char *p, struct siphash *state) {
int r;
assert(p);
for (;;) {
char label[DNS_LABEL_MAX+1];
r = dns_label_unescape(&p, label, sizeof label, 0);
if (r < 0)
break;
if (r == 0)
break;
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 */
string_hash_func("", state);
}
2018-11-27 14:25:20 +01:00
int dns_name_compare_func(const char *a, const char *b) {
const char *x, *y;
resolved: rework IDNA logic Move IDNA logic out of the normal domain name processing, and into the bus frontend calls. Previously whenever comparing two domain names we'd implicitly do IDNA conversion so that "pöttering.de" and "xn--pttering-n4a.de" would be considered equal. This is problematic not only for DNSSEC, but actually also against he IDNA specs. Moreover it creates problems when encoding DNS-SD services in classic DNS. There, the specification suggests using UTF8 encoding for the actual service name, but apply IDNA encoding to the domain suffix. With this change IDNA conversion is done only: - When the user passes a non-ASCII hostname when resolving a host name using ResolveHostname() - When the user passes a non-ASCII domain suffix when resolving a service using ResolveService() No IDNA encoding is done anymore: - When the user does raw ResolveRecord() RR resolving - On the service part of a DNS-SD service name Previously, IDNA encoding was done when serializing names into packets, at a point where information whether something is a label that needs IDNA encoding or not was not available, but at a point whether it was known whether to generate a classic DNS packet (where IDNA applies), or an mDNS/LLMNR packet (where IDNA does not apply, and UTF8 is used instead for all host names). With this change each DnsQuery object will now maintain two copies of the DnsQuestion to ask: one encoded in IDNA for use with classic DNS, and one encoded in UTF8 for use with LLMNR and MulticastDNS.
2016-01-18 20:31:39 +01:00
int r, q;
assert(a);
assert(b);
2018-11-27 14:25:20 +01:00
x = a + strlen(a);
y = b + strlen(b);
for (;;) {
char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
if (x == NULL && y == NULL)
return 0;
r = dns_label_unescape_suffix(a, &x, la, sizeof(la));
q = dns_label_unescape_suffix(b, &y, lb, sizeof(lb));
if (r < 0 || q < 0)
return CMP(r, q);
r = ascii_strcasecmp_nn(la, r, lb, q);
if (r != 0)
return r;
}
}
2018-11-27 14:25:20 +01:00
DEFINE_HASH_OPS(dns_name_hash_ops, char, dns_name_hash_func, dns_name_compare_func);
int dns_name_equal(const char *x, const char *y) {
int r, q;
assert(x);
assert(y);
for (;;) {
char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
r = dns_label_unescape(&x, la, sizeof la, 0);
if (r < 0)
return r;
q = dns_label_unescape(&y, lb, sizeof lb, 0);
if (q < 0)
return q;
if (r != q)
return false;
if (r == 0)
return true;
if (ascii_strcasecmp_n(la, lb, r) != 0)
return false;
}
}
int dns_name_endswith(const char *name, const char *suffix) {
const char *n, *s, *saved_n = NULL;
int r, q;
assert(name);
assert(suffix);
n = name;
s = suffix;
for (;;) {
char ln[DNS_LABEL_MAX], ls[DNS_LABEL_MAX];
r = dns_label_unescape(&n, ln, sizeof ln, 0);
if (r < 0)
return r;
if (!saved_n)
saved_n = n;
q = dns_label_unescape(&s, ls, sizeof ls, 0);
if (q < 0)
return q;
if (r == 0 && q == 0)
return true;
if (r == 0 && saved_n == n)
return false;
if (r != q || ascii_strcasecmp_n(ln, ls, r) != 0) {
/* Not the same, let's jump back, and try with the next label again */
s = suffix;
n = TAKE_PTR(saved_n);
}
}
}
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(&p, lp, sizeof lp, 0);
if (r < 0)
return r;
if (r == 0)
return true;
q = dns_label_unescape(&n, ln, sizeof ln, 0);
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;
assert(name);
assert(old_suffix);
assert(new_suffix);
assert(ret);
n = name;
s = old_suffix;
for (;;) {
char ln[DNS_LABEL_MAX], ls[DNS_LABEL_MAX];
if (!saved_before)
saved_before = n;
r = dns_label_unescape(&n, ln, sizeof ln, 0);
if (r < 0)
return r;
if (!saved_after)
saved_after = n;
q = dns_label_unescape(&s, ls, sizeof ls, 0);
if (q < 0)
return q;
if (r == 0 && q == 0)
break;
if (r == 0 && saved_after == n) {
*ret = NULL; /* doesn't match */
return 0;
}
if (r != q || ascii_strcasecmp_n(ln, ls, r) != 0) {
/* Not the same, let's jump back, and try with the next label again */
s = old_suffix;
n = TAKE_PTR(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, 0, ret);
if (r < 0)
return r;
return 1;
}
int dns_name_between(const char *a, const char *b, const char *c) {
/* Determine if b is strictly greater than a and strictly smaller than c.
We consider the order of names to be circular, so that if a is
strictly greater than c, we consider b to be between them if it is
either greater than a or smaller than c. This is how the canonical
DNS name order used in NSEC records work. */
if (dns_name_compare_func(a, c) < 0)
/*
a and c are properly ordered:
a<---b--->c
*/
return dns_name_compare_func(a, b) < 0 &&
dns_name_compare_func(b, c) < 0;
else
/*
a and c are equal or 'reversed':
<--b--c a----->
or:
<-----c a--b-->
*/
return dns_name_compare_func(b, c) < 0 ||
dns_name_compare_func(a, b) < 0;
}
int dns_name_reverse(int family, const union in_addr_union *a, char **ret) {
const uint8_t *p;
int r;
assert(a);
assert(ret);
p = (const uint8_t*) a;
if (family == AF_INET)
r = asprintf(ret, "%u.%u.%u.%u.in-addr.arpa", p[3], p[2], p[1], p[0]);
else if (family == AF_INET6)
r = asprintf(ret, "%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.ip6.arpa",
hexchar(p[15] & 0xF), hexchar(p[15] >> 4), hexchar(p[14] & 0xF), hexchar(p[14] >> 4),
hexchar(p[13] & 0xF), hexchar(p[13] >> 4), hexchar(p[12] & 0xF), hexchar(p[12] >> 4),
hexchar(p[11] & 0xF), hexchar(p[11] >> 4), hexchar(p[10] & 0xF), hexchar(p[10] >> 4),
hexchar(p[ 9] & 0xF), hexchar(p[ 9] >> 4), hexchar(p[ 8] & 0xF), hexchar(p[ 8] >> 4),
hexchar(p[ 7] & 0xF), hexchar(p[ 7] >> 4), hexchar(p[ 6] & 0xF), hexchar(p[ 6] >> 4),
hexchar(p[ 5] & 0xF), hexchar(p[ 5] >> 4), hexchar(p[ 4] & 0xF), hexchar(p[ 4] >> 4),
hexchar(p[ 3] & 0xF), hexchar(p[ 3] >> 4), hexchar(p[ 2] & 0xF), hexchar(p[ 2] >> 4),
hexchar(p[ 1] & 0xF), hexchar(p[ 1] >> 4), hexchar(p[ 0] & 0xF), hexchar(p[ 0] >> 4));
else
return -EAFNOSUPPORT;
if (r < 0)
return -ENOMEM;
return 0;
}
int dns_name_address(const char *p, int *ret_family, union in_addr_union *ret_address) {
int r;
assert(p);
assert(ret_family);
assert(ret_address);
r = dns_name_endswith(p, "in-addr.arpa");
if (r < 0)
return r;
if (r > 0) {
uint8_t a[4];
unsigned i;
for (i = 0; i < ELEMENTSOF(a); i++) {
char label[DNS_LABEL_MAX+1];
r = dns_label_unescape(&p, label, sizeof label, 0);
if (r < 0)
return r;
if (r == 0)
return -EINVAL;
if (r > 3)
return -EINVAL;
r = safe_atou8(label, &a[i]);
if (r < 0)
return r;
}
r = dns_name_equal(p, "in-addr.arpa");
if (r <= 0)
return r;
*ret_family = AF_INET;
ret_address->in.s_addr = htobe32(((uint32_t) a[3] << 24) |
((uint32_t) a[2] << 16) |
((uint32_t) a[1] << 8) |
(uint32_t) a[0]);
return 1;
}
r = dns_name_endswith(p, "ip6.arpa");
if (r < 0)
return r;
if (r > 0) {
struct in6_addr a;
unsigned i;
for (i = 0; i < ELEMENTSOF(a.s6_addr); i++) {
char label[DNS_LABEL_MAX+1];
int x, y;
r = dns_label_unescape(&p, label, sizeof label, 0);
if (r <= 0)
return r;
if (r != 1)
return -EINVAL;
x = unhexchar(label[0]);
if (x < 0)
return -EINVAL;
r = dns_label_unescape(&p, label, sizeof label, 0);
if (r <= 0)
return r;
if (r != 1)
return -EINVAL;
y = unhexchar(label[0]);
if (y < 0)
return -EINVAL;
a.s6_addr[ELEMENTSOF(a.s6_addr) - i - 1] = (uint8_t) y << 4 | (uint8_t) x;
}
r = dns_name_equal(p, "ip6.arpa");
if (r <= 0)
return r;
*ret_family = AF_INET6;
ret_address->in6 = a;
return 1;
}
*ret_family = AF_UNSPEC;
*ret_address = IN_ADDR_NULL;
return 0;
}
bool dns_name_is_root(const char *name) {
assert(name);
/* There are exactly two ways to encode the root domain name:
* as empty string, or with a single dot. */
return STR_IN_SET(name, "", ".");
}
bool dns_name_is_single_label(const char *name) {
int r;
assert(name);
r = dns_name_parent(&name);
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, 0);
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, 0);
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 dnssd_srv_type_is_valid(const char *name) {
return dns_srv_type_is_valid(name) &&
((dns_name_endswith(name, "_tcp") > 0) ||
(dns_name_endswith(name, "_udp") > 0)); /* Specific to DNS-SD. RFC 6763, Section 7 */
}
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, 0, 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;
r = dns_name_concat(type, domain, 0, &n);
if (r < 0)
return r;
return dns_name_concat(escaped, n, 0, 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), 0);
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), 0);
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), 0);
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 = strjoin(b, ".", c);
if (!type)
return -ENOMEM;
d = p;
goto finish;
}
} else if (srv_type_label_is_valid(a, an)) {
/* OK, got <type> . <type2> . <domain> */
name = NULL;
type = strjoin(a, ".", b);
if (!type)
return -ENOMEM;
d = q;
goto finish;
}
}
name = NULL;
type = NULL;
d = joined;
finish:
r = dns_name_normalize(d, 0, &domain);
if (r < 0)
return r;
if (_domain)
*_domain = TAKE_PTR(domain);
if (_type)
*_type = TAKE_PTR(type);
if (_name)
*_name = TAKE_PTR(name);
return 0;
}
static int dns_name_build_suffix_table(const char *name, const char *table[]) {
const char *p;
unsigned n = 0;
int r;
assert(name);
assert(table);
p = name;
for (;;) {
if (n > DNS_N_LABELS_MAX)
return -EINVAL;
table[n] = p;
r = dns_name_parent(&p);
if (r < 0)
return r;
if (r == 0)
break;
n++;
}
return (int) n;
}
int dns_name_suffix(const char *name, unsigned n_labels, const char **ret) {
const char* labels[DNS_N_LABELS_MAX+1];
int n;
assert(name);
assert(ret);
n = dns_name_build_suffix_table(name, labels);
if (n < 0)
return n;
if ((unsigned) n < n_labels)
return -EINVAL;
*ret = labels[n - n_labels];
return (int) (n - n_labels);
}
int dns_name_skip(const char *a, unsigned n_labels, const char **ret) {
int r;
assert(a);
assert(ret);
for (; n_labels > 0; n_labels--) {
r = dns_name_parent(&a);
if (r < 0)
return r;
if (r == 0) {
*ret = "";
return 0;
}
}
*ret = a;
return 1;
}
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);
r = dns_name_skip(a, n_labels, &a);
if (r <= 0)
return r;
return dns_name_equal(a, b);
}
int dns_name_common_suffix(const char *a, const char *b, const char **ret) {
const char *a_labels[DNS_N_LABELS_MAX+1], *b_labels[DNS_N_LABELS_MAX+1];
int n = 0, m = 0, k = 0, r, q;
assert(a);
assert(b);
assert(ret);
/* Determines the common suffix of domain names a and b */
n = dns_name_build_suffix_table(a, a_labels);
if (n < 0)
return n;
m = dns_name_build_suffix_table(b, b_labels);
if (m < 0)
return m;
for (;;) {
char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
const char *x, *y;
if (k >= n || k >= m) {
*ret = a_labels[n - k];
return 0;
}
x = a_labels[n - 1 - k];
r = dns_label_unescape(&x, la, sizeof la, 0);
if (r < 0)
return r;
y = b_labels[m - 1 - k];
q = dns_label_unescape(&y, lb, sizeof lb, 0);
if (q < 0)
return q;
if (r != q || ascii_strcasecmp_n(la, lb, r) != 0) {
*ret = a_labels[n - k];
return 0;
}
k++;
}
}
int dns_name_apply_idna(const char *name, char **ret) {
/* Return negative on error, 0 if not implemented, positive on success. */
#if HAVE_LIBIDN2 || HAVE_LIBIDN2
int r;
r = dlopen_idn();
if (r == -EOPNOTSUPP) {
*ret = NULL;
return 0;
}
if (r < 0)
return r;
#endif
#if HAVE_LIBIDN2
_cleanup_free_ char *t = NULL;
assert(name);
assert(ret);
/* First, try non-transitional mode (i.e. IDN2008 rules) */
r = sym_idn2_lookup_u8((uint8_t*) name, (uint8_t**) &t,
IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL);
if (r == IDN2_DISALLOWED) /* If that failed, because of disallowed characters, try transitional mode.
* (i.e. IDN2003 rules which supports some unicode chars IDN2008 doesn't allow). */
r = sym_idn2_lookup_u8((uint8_t*) name, (uint8_t**) &t,
IDN2_NFC_INPUT | IDN2_TRANSITIONAL);
log_debug("idn2_lookup_u8: %s → %s", name, t);
if (r == IDN2_OK) {
if (!startswith(name, "xn--")) {
_cleanup_free_ char *s = NULL;
r = sym_idn2_to_unicode_8z8z(t, &s, 0);
if (r != IDN2_OK) {
log_debug("idn2_to_unicode_8z8z(\"%s\") failed: %d/%s",
t, r, sym_idn2_strerror(r));
*ret = NULL;
return 0;
}
if (!streq_ptr(name, s)) {
log_debug("idn2 roundtrip failed: \"%s\"\"%s\"\"%s\", ignoring.",
name, t, s);
*ret = NULL;
return 0;
}
}
*ret = TAKE_PTR(t);
return 1; /* *ret has been written */
}
log_debug("idn2_lookup_u8(\"%s\") failed: %d/%s", name, r, sym_idn2_strerror(r));
if (r == IDN2_2HYPHEN)
/* The name has two hyphens — forbidden by IDNA2008 in some cases */
return 0;
if (IN_SET(r, IDN2_TOO_BIG_DOMAIN, IDN2_TOO_BIG_LABEL))
return -ENOSPC;
return -EINVAL;
#elif HAVE_LIBIDN
_cleanup_free_ char *buf = NULL;
size_t n = 0, allocated = 0;
bool first = true;
int r, q;
assert(name);
assert(ret);
for (;;) {
char label[DNS_LABEL_MAX];
r = dns_label_unescape(&name, label, sizeof label, 0);
if (r < 0)
return r;
if (r == 0)
break;
q = dns_label_apply_idna(label, r, label, sizeof label);
if (q < 0)
return q;
if (q > 0)
r = q;
if (!GREEDY_REALLOC(buf, allocated, n + !first + DNS_LABEL_ESCAPED_MAX))
return -ENOMEM;
r = dns_label_escape(label, r, buf + n + !first, DNS_LABEL_ESCAPED_MAX);
if (r < 0)
return r;
if (first)
first = false;
else
buf[n++] = '.';
n += r;
}
if (n > DNS_HOSTNAME_MAX)
return -EINVAL;
if (!GREEDY_REALLOC(buf, allocated, n + 1))
return -ENOMEM;
buf[n] = 0;
*ret = TAKE_PTR(buf);
return 1;
#else
*ret = NULL;
return 0;
#endif
}
int dns_name_is_valid_or_address(const char *name) {
/* Returns > 0 if the specified name is either a valid IP address formatted as string or a valid DNS name */
if (isempty(name))
return 0;
if (in_addr_from_string_auto(name, NULL, NULL) >= 0)
return 1;
return dns_name_is_valid(name);
}
int dns_name_dot_suffixed(const char *name) {
const char *p = name;
int r;
for (;;) {
if (streq(p, "."))
return true;
r = dns_label_unescape(&p, NULL, DNS_LABEL_MAX, DNS_LABEL_LEAVE_TRAILING_DOT);
if (r < 0)
return r;
if (r == 0)
return false;
}
}