Systemd/src/shared/dns-domain.c

1388 lines
39 KiB
C
Raw Normal View History

/* SPDX-License-Identifier: LGPL-2.1+ */
/***
This file is part of systemd.
Copyright 2014 Lennart Poettering
systemd is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
systemd is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with systemd; If not, see <http://www.gnu.org/licenses/>.
***/
#if HAVE_LIBIDN2
# include <idn2.h>
#elif HAVE_LIBIDN
# include <idna.h>
# 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"
2015-06-02 20:49:43 +02:00
#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;
char *d;
int r = 0;
assert(name);
assert(*name);
n = *name;
d = dest;
for (;;) {
if (*n == '.') {
n++;
break;
}
if (*n == 0)
break;
if (r >= DNS_LABEL_MAX)
return -EINVAL;
if (sz <= 0)
return -ENOBUFS;
if (*n == '\\') {
/* Escaped character */
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 (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 (d)
*(d++) = (char) k;
sz--;
r++;
n += 3;
} else
return -EINVAL;
} else if ((uint8_t) *n >= (uint8_t) ' ' && *n != 127) {
/* Normal character */
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 == '.')
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);
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];
assert(encoded);
assert(decoded);
/* Converts an U-label into an A-label */
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 = stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
if (!input)
return -ENOMEM;
if (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;
/* 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;
if (memcmp(encoded, IDNA_ACE_PREFIX, sizeof(IDNA_ACE_PREFIX) -1) != 0)
return 0;
input = stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
if (!input)
return -ENOMEM;
output_size = input_size;
output = newa(uint32_t, output_size);
idna_to_unicode_44i(input, input_size, output, &output_size, 0);
result = 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, 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 = b;
b = NULL;
} else
goto finish;
for (;;) {
char label[DNS_LABEL_MAX];
r = dns_label_unescape(&p, label, sizeof(label));
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 = b;
b = NULL;
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 = ret;
ret = NULL;
}
return 0;
}
void dns_name_hash_func(const void *s, struct siphash *state) {
const char *p = s;
int r;
assert(p);
for (;;) {
char label[DNS_LABEL_MAX+1];
r = dns_label_unescape(&p, label, sizeof(label));
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);
}
int dns_name_compare_func(const void *a, const void *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);
x = (const char *) a + strlen(a);
y = (const char *) 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 r - q;
r = ascii_strcasecmp_nn(la, r, lb, q);
if (r != 0)
return r;
}
}
const struct hash_ops dns_name_hash_ops = {
.hash = dns_name_hash_func,
.compare = 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];
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
r = dns_label_unescape(&x, la, sizeof(la));
if (r < 0)
return r;
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
q = dns_label_unescape(&y, lb, sizeof(lb));
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];
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
r = dns_label_unescape(&n, ln, sizeof(ln));
if (r < 0)
return r;
if (!saved_n)
saved_n = n;
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
q = dns_label_unescape(&s, ls, sizeof(ls));
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];
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
r = dns_label_unescape(&p, lp, sizeof(lp));
if (r < 0)
return r;
if (r == 0)
return true;
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
q = dns_label_unescape(&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;
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;
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
r = dns_label_unescape(&n, ln, sizeof(ln));
if (r < 0)
return r;
if (!saved_after)
saved_after = n;
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
q = dns_label_unescape(&s, ls, sizeof(ls));
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 = 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) {
/* 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 *family, union in_addr_union *address) {
int r;
assert(p);
assert(family);
assert(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));
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;
*family = AF_INET;
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));
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));
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;
*family = AF_INET6;
address->in6 = a;
return 1;
}
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);
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 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, 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, &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 = 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, &domain);
if (r < 0)
return r;
if (_domain) {
*_domain = domain;
domain = NULL;
}
if (_type) {
*_type = type;
type = NULL;
}
if (_name) {
*_name = name;
name = NULL;
}
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];
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
r = dns_label_unescape(&x, la, sizeof(la));
if (r < 0)
return r;
y = b_labels[m - 1 - k];
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
q = dns_label_unescape(&y, lb, sizeof(lb));
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
int r;
_cleanup_free_ char *t = NULL;
assert(name);
assert(ret);
r = idn2_lookup_u8((uint8_t*) name, (uint8_t**) &t,
IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL);
log_debug("idn2_lookup_u8: %s → %s", name, t);
if (r == IDN2_OK) {
if (!startswith(name, "xn--")) {
_cleanup_free_ char *s = NULL;
r = idn2_to_unicode_8z8z(t, &s, 0);
if (r != IDN2_OK) {
log_debug("idn2_to_unicode_8z8z(\"%s\") failed: %d/%s",
t, r, idn2_strerror(r));
return 0;
}
if (!streq_ptr(name, s)) {
log_debug("idn2 roundtrip failed: \"%s\"\"%s\"\"%s\", ignoring.",
name, t, s);
return 0;
}
}
*ret = t;
t = NULL;
return 1; /* *ret has been written */
}
log_debug("idn2_lookup_u8(\"%s\") failed: %d/%s", name, r, idn2_strerror(r));
if (r == IDN2_2HYPHEN)
/* The name has two hypens — 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));
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 = buf;
buf = NULL;
return 1;
#else
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);
}